← blog · 16 Eylül 2026

Konteynerde PID 1 Problemi: Zombi Süreçler ve Init Ne Zaman Gerekir

Bir konteynerin ilk süreci çekirdek için sıradan bir süreç değildir. Bu farkın nasıl zombi süreç birikimine yol açtığını, cgroup pid limitine nasıl çarptığını ve tini, dumb-init ya da tam bir süreç yöneticisi arasında nasıl seçim yapılacağını anlatıyoruz.

PID 1'in çekirdek için farklı bir anlamı var

Bir konteyner başlatıldığında içindeki ilk süreç Linux çekirdeği için PID 1 olur ve PID 1, normal bir sürecin sahip olmadığı iki sorumluluk taşır. Birincisi sinyal davranışı: çekirdek, bir sürece SIGTERM veya SIGINT gibi bir sinyal gönderildiğinde o süreç kendi işleyicisini (handler) tanımlamamışsa varsayılan işleyiciyi uygular, ama bu kural PID 1 için geçerli değildir. Normal bir süreç SIGTERM aldığında varsayılan davranışı sonlanmaktır; PID 1 için varsayılan davranış hiçbir şey yapmamaktır. İkincisi süreç toplama (reaping): bir alt süreç sonlandığında ebeveyni onun çıkış koduyla wait() çağırana kadar çekirdek o süreci zombi (defunct) olarak tutar. Normal bir süreç ağacında yetim kalan alt süreçler PID 1'e miras kalır ve init sistemleri (systemd, SysV init) tam olarak bu mirası toplamak için yazılmıştır. Konteyner içindeki PID 1 ise çoğunlukla bir web sunucusu, bir uygulama süreci ya da bir kabuk betiğidir ve bu iş için tasarlanmamıştır.

Bu iki sorumluluk göz ardı edilebilir gibi görünür çünkü çoğu konteyner tek bir süreç çalıştırır ve hiç alt süreç doğurmaz. Sorun tam olarak alt süreç doğurulduğu ama düzgün toplanmadığı yerde başlar.

Zombi süreç nasıl birikir

Tipik birikim deseni şöyledir: uygulama periyodik bir görevi arka planda çalıştırmak için bir kabuk çağırır, kabuk komutu & ile arka plana alır ve hemen çıkar. Kabuk çıktığında arkada bıraktığı süreç yetim kalır ve PID 1'e miras kalır. PID 1 bu süreci wait() ile toplamıyorsa süreç işi bitince zombi olarak kalır. Tek seferlik bir arka plan görevi bunu fark ettirmez; dakikada bir ya da saatte onlarca kez çalışan bir zamanlayıcı görevi bunu hızla biriktirir.

Zombiler bellek veya CPU tüketmez, ps çıktısında <defunct> olarak görünür ve zararsız gibi durur. Asıl sınır Linux'un cgroup pids denetleyicisidir: her zombi bir PID slotunu işgal etmeye devam eder ve pids.max sınırına yaklaşıldığında yeni süreç oluşturma (fork()) başarısız olmaya başlar. Bu noktada arıza zombilerin kendisinde değil tamamen ilgisiz bir yerde çıkar: konteyner içinde yeni bir komut çalıştırılamaz, yeni bir istek işleyici forklanamaz, zamanlayıcı sessizce durur. Sağlık kontrolü genelde ana sürecin canlılığına bakar ve ana süreç hâlâ ayaktaysa yeşil kalmaya devam eder; arızayı gösteren tek işaret cgroup metriklerinde artan pids.current değeri ya da günlükteki tek satırlık bir fork: Resource temporarily unavailable hatasıdır.

Üç yaklaşım

Bu sorunu çözmenin üç yolu var ve hangisinin doğru olduğu konteynerin ne çalıştırdığına bağlı.

Birinci yol hafif bir init süreci kullanmaktır. tini ve dumb-init tam olarak bu iş için yazılmış, birkaç yüz satırlık, tek görevi PID 1 olup sinyalleri gerçek uygulamaya iletmek ve yetim kalan süreçleri toplamak olan ikili dosyalardır. Docker'da docker run --init bayrağı, Docker'ın kendi gömdüğü tini'yi konteynerin PID 1'i yapar ve asıl komutunuzu onun altında alt süreç olarak başlatır; Compose'da aynı davranış servis tanımına init: true eklenerek elde edilir. Kendi imajınıza tini'yi gömüp ENTRYPOINT ["/usr/bin/tini", "--", "/uygulama"] yazmak da aynı sonucu verir ve imajın Docker'ın init desteğine bağımlı kalmamasını sağlar.

İkinci yol tam bir süreç yöneticisi kullanmaktır: s6-overlay ya da supervisord gibi araçlar hem PID 1 sorumluluklarını üstlenir hem de aynı konteynerde birden fazla süreci yönetip birinin çökmesi durumunda onu yeniden başlatabilir. Bu, tek konteynerde gerçekten birden fazla uzun ömürlü süreç çalıştırmanız gerektiğinde (örneğin bir uygulama sunucusu ve yanında bir log toplayıcı) anlamlıdır; tek süreçli bir konteynere sadece zombi toplamak için tam bir süreç yöneticisi koymak gereksiz karmaşıklıktır.

Üçüncü yol hiçbir şey eklememek ve bunun bilinçli bir karar olmasını sağlamaktır. Uygulama hiç alt süreç doğurmuyorsa (çoğu modern web sunucusu ve dil çalışma zamanı bunu yapmaz) zombi birikme riski zaten yoktur ve init eklemek sinyal iletimine bir katman daha eklemekten başka bir şey yapmaz.

Kubernetes'te durum farklı

Docker'ın --init bayrağının Kubernetes'te birebir karşılığı yoktur; Pod spesifikasyonunda bir konteynerin PID 1'ine init sarmalayıcısı ekleyen bir alan bulunmaz. Bu yüzden Kubernetes'te çalışacak imajlar için init'i imajın kendisine gömmek gerekir: ENTRYPOINT ["tini", "--", ...] deseni container runtime'dan bağımsız çalışır, çünkü PID 1'i belirleyen containerd ya da CRI-O değil imajın kendi ENTRYPOINT'idir. shareProcessNamespace: true ile bir pod içindeki konteynerlerin süreç isim alanını paylaştırmak farklı bir sorunu çözer (bir sidecar'ın ana konteynerin süreçlerini görebilmesi) ve zombi toplama sorununu kendiliğinden çözmez; paylaşılan isim alanındaki PID 1 yine pause konteyneri ya da ilk konteynerin kendisidir.

Sık düşülen tuzaklar

Dockerfile'da CMD ya da ENTRYPOINT kabuk biçiminde yazıldığında (CMD myapp --flag) Docker bunu örtük olarak /bin/sh -c "myapp --flag" şeklinde çalıştırır. Bu durumda PID 1 uygulamanız değil sh olur, sinyaller sh'ye gider ve sh çoğu sinyali alt sürecine iletmez; konteyner docker stop ile durdurulmaya çalışıldığında zarif kapanma süresi hiç kullanılmadan SIGKILL ile sonlanır. Çözüm exec biçimini kullanmaktır: CMD ["myapp", "--flag"]. tini veya dumb-init eklerken de aynı hataya düşülebilir; sarmalayıcıyı kabuk biçiminde çağırmak onu da bir sh katmanının arkasına gizler.

İkinci tuzak, gosu veya su-exec gibi ayrıcalık düşürme araçlarını init ile birlikte kullanırken sırayı karıştırmaktır. Doğru sıra PID 1'in tini olması, tini'nin ayrıcalık düşürme aracını çağırması, onun da asıl uygulamayı exec etmesidir; tersi sıralamada tini uygulamanın üstünde değil bir ara kabuğun üstünde durur ve toplama zinciri yine kopar.

Üçüncü tuzak, zombi birikimini yalnızca bellek ve CPU grafiklerinden izlemeye çalışmaktır. Zombiler bu metriklerde görünmez; doğru ölçüt ps -eo stat,ppid | awk '$1 ~ /^Z/' | wc -l ile zombi sayısını saymak ya da cgroup v2'de /sys/fs/cgroup/pids.current değerinin pids.max'e ne kadar yaklaştığını izlemektir. Bu iki metrik izlenmediği sürece sorun ancak pids.max sınırına gerçekten çarpılınca, çoğunlukla en yoğun anda ortaya çıkar.

Ne zaman uğraşmaya değmez

Tek bir uzun ömürlü sürecin çalıştığı, o sürecin kendisi hiç fork etmediği ve içeride cron benzeri bir zamanlayıcı bulunmayan bir konteynerde init eklemek gereksiz bir katmandır. Riski değerlendirmenin pratik yolu imajın içine girip ps -ef ile hangi süreçlerin çalıştığına bakmak ve uygulamanın kod tabanında exec, fork, subprocess, Process.Start gibi çağrıları aramaktır. Hiçbiri yoksa PID 1'in özel davranışı hiç devreye girmez. Varsa, birkaç megabaytlık bir ikili dosya eklemenin maliyeti, üretimde bir gün ansızın "yeni süreç oluşturulamıyor" hatasıyla karşılaşmanın maliyetinden çok daha düşüktür.