Container'daki Go worker'ım SIGTERM almıyor ve graceful kapanmıyor, bu bir PID 1 sorunu mu?
Soru
Go ile yazdığım bir queue worker'ını Docker container'ında çalıştırıyorum ve Kubernetes üzerinde deploy ediyorum. Kodda `os.Interrupt` dinleyip kanal geldiğinde context'i iptal eden bir graceful shutdown mantığım var. Ama her deploy'da worker grace period boyunca hiçbir şey yapmıyor ve sonunda SIGKILL yiyor. Loglara bakınca SIGTERM'ü hiç görmediğini fark ettim; sanki sinyal worker'a hiç ulaşmıyor. Bu bir PID 1 sorunu mu, yoksa kodumda mı bir şey eksik?
Cevap
Kısa cevap: Büyük olasılıkla evet, bu bir PID 1 sorunu.
Kısa cevap
Ama iki ayrı hata aynı anda var: sinyal büyük ihtimalle shell-form CMD yüzünden worker’a hiç ulaşmıyor, ayrıca kodunuz os.Interrupt (SIGINT) dinleyip SIGTERM’ü kaçırıyor.
Neden
-
Shell form sizi /bin/sh -c’nin çocuğu yapar.
CMD ./worker(shell form) worker’ısh’nin altında çalıştırır; PID 1 olanshise SIGTERM’ü çocuğuna iletmez. Bu, sinyalin worker’a hiç ulaşmamasının bir numaralı sebebidir. Çözüm exec form:CMD ["./worker"]ya daENTRYPOINT ["/app/worker"].# SIGTERM'ü doğrudan binary'ye iletir ENTRYPOINT ["/app/worker"] -
PID 1’in varsayılan sinyal davranışı yoktur. Doğrudan PID 1 olsanız bile kernel, PID 1 için varsayılan sinyal dispozisyonlarını uygulamaz; handler’ı açıkça kurmak zorundasınız. Sadece
os.Interruptdinliyorsanız SIGTERM’ü kaçırırsınız — Kubernetes’in deploy’da gönderdiği sinyal tam olarak SIGTERM’dür.ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) defer stop() // ctx'i worker döngüsüne geçirin; iptal gelince yeni iş çekmeyi durdurun, // devam eden işi bitirin, çıkın. -
entrypoint script’i de bunu bozar. Kabuk bir entrypoint kullanıyorsanız son satır mutlaka
exec "$@"olmalı.execolmadan"$@"çalıştırırsanız kabuk PID 1 olarak kalır ve sinyali yine yutar.
Ne yapmalı
-
Sinyal → context iptal → drain zincirini kurun. SIGTERM’de worker context’ini iptal edin, yeni job çekmeyi bırakın, in-flight işi bitirin ve çıkın. Bu,
terminationGracePeriodSeconds(varsayılan 30s) içinde tamamlanmalı; aşarsanız SIGKILL yersiniz. Uzun işler varsa grace period’u buna göre ayarlayın. -
Çocuk process açıyorsanız zombie reaping gerekir. Worker başka process’ler fork ediyorsa PID 1 olarak onları reap etmeniz gerekir; bu durumda
tini’yi image’a kendiniz eklemeniz gerekir (Docker’da bunudocker run --initsağlar, Kubernetes’in buna denk yerleşik bir bayrağı yoktur). Tek bir Go binary için genellikle exec form + doğru sinyal yakalama yeter, tini’ye ihtiyaç olmaz. -
Önce teşhisi doğrulayın, tahmin etmeyin. Container içinde PID 1’in gerçekten binary’niz mi yoksa
shmi olduğunu görmek beş saniyelik iştir:docker exec <container> ps -o pid,comm. PID 1’deshgörüyorsanız teşhis kesindir. Ayrıca image’ın SIGTERM beklediğinden emin olun;STOPSIGNAL SIGTERMzaten varsayılandır ama farklı bir şeye ayarlanmışsa worker’ınız hiç beklemediği bir sinyal alıyor olabilir. -
Kubernetes’te trafiği önce kesin. Graceful shutdown sadece sinyal yakalamak değildir. Pod sonlandırılırken readiness probe’u düşürüp bir
preStophook (kısa bir sleep) koyarak, SIGTERM gelmeden önce pod’un Service endpoint’lerinden çıkarılmasını sağlayın. Aksi halde drain ederken bile yeni istek/job almaya devam edebilirsiniz.
Sonuç: Ben olsam önce docker exec ... ps ile PID 1’i doğrular, Dockerfile’ı exec form’a çevirir, kodda os.Interrupt yerine signal.NotifyContext(..., syscall.SIGTERM, syscall.SIGINT) kullanır ve grace period içinde drain edildiğini bir deploy ile doğrulardım. tini’yi yalnızca worker gerçekten çocuk process fork ediyorsa eklerdim; aksi halde gereksiz katman olur. Sıralama: önce sinyal ulaşsın, sonra kod onu yakalasın, en son da trafiği zamanında kesin.
Yorumlar
Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.