Go Dilinde Graceful Shutdown Uygulamak İçin Pratik Kalıplar
(victoriametrics.com)- Go uygulamalarında Graceful Shutdown, yeni istekleri engelleyen, devam eden işlerin bitmesini bekleyen ve ardından veritabanı bağlantıları, dosya kilitleri, ağ listener’ları gibi kaynakları temizleyen bir kapanış prosedürüdür
- Kapanış işlemi,
SIGTERMveSIGINTgibi kapatma sinyallerinios/signalveya Go 1.16 ve üzerindekisignal.NotifyContextile alıp varsayılan anında sonlandırma davranışını değiştirmekle başlar - Kubernetes’te kapanışın varsayılan 30 saniyelik grace period içinde tamamlanması gerekir;
preStopgecikmesi veya readiness probe hatasıyla, trafik durdurma durumunun harici load balancer’a kadar yayılması için zaman ayrılmalıdır http.Server.Shutdownyeni bağlantıları engeller ve aktif isteklerin tamamlanmasını bekler; ancak handler context cancellation’a uymuyorsa kısmi yazma, veri kaybı, açık transaction gibi sorunlar oluşabilir- Kritik kaynaklar kapatma sinyalinden hemen sonra değil, istekler tamamlandıktan sonra veya süre sınırı dolduktan sonra temizlenmelidir; başlatmanın ters sırasıyla kapatmak, bileşen bağımlılıklarını korumayı kolaylaştırır
Graceful Shutdown’ın asgari koşulları
- Graceful Shutdown genellikle üç koşulu karşılamalıdır
- HTTP, pub/sub gibi giriş noktalarında artık yeni istek veya mesaj alınmaması
- Halihazırda devam eden isteklerin bitmesinin beklenmesi; çok uzun sürerse graceful error ile yanıt verilmesi
- Veritabanı bağlantıları, dosya kilitleri, ağ listener’ları gibi kritik kaynakların serbest bırakılması ve son temizliğin yapılması
- Harici servislere giden veritabanı veya cache bağlantıları, yeni istekleri engelleme aşamasında hemen kesilmez
- Odak HTTP sunucuları ve container uygulamaları olsa da temel ilkeler başka uygulamalara da uygulanabilir
Kapatma sinyallerini işleme
- Unix benzeri sistemlerde sinyal, bir süreçte belirli bir durum oluştuğunu bildiren yazılımsal bir interrupt’tır
- Bir süreç belirli sinyaller için handler kaydedebilir; handler yoksa varsayılan davranışı izler
- Varsayılan davranış sonlandırma, durdurma, çalışmaya devam etme, yok sayma vb. olabilir
SIGKILLgibi bazı sinyaller yakalanamaz veya yok sayılamaz ve süreci sonlandırır
- Go runtime,
mainfonksiyonu çalışmadan önceSIGTERM,SIGQUIT,SIGILL,SIGTRAPgibi birçok sinyal handler’ını otomatik olarak kaydeder - Graceful Shutdown’da genellikle önemli olan üç kapatma sinyali vardır
SIGTERM: Sürecin sonlandırılmasını istemenin standart ve nazik yoludur; Kubernetes’in zorla sonlandırmadan önce uygulamaya gönderdiği sinyaldirSIGINT: Kullanıcı terminaldeCtrl+Cile süreci durdurmaya çalıştığında gönderilirSIGHUP: Başlangıçta terminal bağlantısının kesilmesi için kullanılırdı; günümüzde ayarları yeniden yükleme sinyali olarak da sıkça kullanılır
- Ayrı bir işlem yapılmadan
SIGTERM,SIGINT,SIGHUPalınırsa Go runtime uygulamayı sonlandırır
os/signal ve NotifyContext
signal.Notify, belirtilen sinyali varsayılan davranış yerine bir kanal üzerinden iletmesi için Go runtime’a talimat verir- Sinyal kanalını buffer boyutu 1 olacak şekilde oluşturmak daha güvenilirdir
- Go içinde kanal gönderimi için
selectvedefaultkullanılır - Buffer’da yer varsa sinyal iletilir; buffer doluysa sinyal atılır
- Buffersız bir kanalda alım yapan goroutine yoksa sinyal kaçırılabilir
- Go içinde kanal gönderimi için
signal.Notifyaynı sinyal için birden fazla kez çağrılabilir; Go ilgili sinyali kayıtlı tüm kanallara gönderirCtrl+Cye birden çok kez basılsa da genelde ikinci giriş otomatik olarakSIGKILLe yükseltilmez- Çoğu bash veya Linux shell’i otomatik yükseltme yapmaz
- Zorla sonlandırma için
kill -9ile doğrudanSIGKILLgönderilmelidir
- Yerel geliştirmede ikinci
Ctrl+Cnin zorla sonlandırma yapmasını istiyorsanız ilk sinyali aldıktan hemen sonrasignal.Stopile ek sinyal alımını durdurabilirsiniz - Go 1.16’dan itibaren
signal.NotifyContextile sinyal işlemeyi context cancellation’a bağlayabilirsinizctx.Done()sonrasında dastop()çağrılmalıdır; böylece ikinciCtrl+Cuygulamayı zorla sonlandırabilir
Kapanış süre sınırı ve Kubernetes davranışı
- Kapatma sinyali alındıktan sonra uygulamanın fiilen kullanabileceği kapanış süresini önce bilmek gerekir
- Kubernetes’in varsayılan grace period’u,
terminationGracePeriodSecondsayrıca belirtilmediyse 30 saniyedir - Bu süre geçince Kubernetes uygulamayı zorla durdurmak için
SIGKILLgönderirSIGKILLyakalanamaz veya işlenemez
- Kalan isteklerin işlenmesi ve kaynakların serbest bırakılması dahil tüm kapanış mantığı bu süre içinde bitmelidir
- Varsayılan 30 saniye temel alınırsa yaklaşık %20 güvenlik payı bırakmak için tüm kapanışı 25 saniye içinde bitirmek iyi olur
Yeni istekleri engelleme ve readiness işleme
- Go’nun
net/httppaketinde Graceful Shutdown,http.Server.Shutdownile gerçekleştirilebilir- Yeni bağlantı kabulünü durdurur
- Aktif istekler tamamlanana kadar bekler
- Ardından idle connection’ları kapatır
- Zaten devam eden istekler tamamlanabilir; tamamlandıktan sonra ilgili bağlantı idle duruma geçer ve kapatılır
- Kapanış sırasında yeni bağlantı denemesi yapan client’lar, listener zaten kapandığı için genellikle
connection refusedhatası alır - Container ortamlarında veya harici load balancer bulunan orkestrasyon ortamlarında yeni istek kabulünü hemen durdurmamak önemlidir
- pod kapanış hedefi olarak işaretlendikten sonra kısa bir süre daha trafik alabilir
- Kubernetes’in iç bileşeni
kube-proxy, pod durumununTerminatinge değiştiğini hızlıca algılar - Harici load balancer Kubernetes’ten bağımsız olarak kendi health check’lerini kullandığı için durumun yayılması zaman ister
- Trafik kesme bilgisinin yayılmasını beklemenin iki yolu vardır
preStophook’unda bir süresleepederek harici load balancer’ın pod’un kapanış durumunu fark etmesine zaman tanımakpreStopta geçen süreterminationGracePeriodSecondskapsamına dahildir
- Kod düzeyinde readiness probe’u başarısız hale getirip bir süre beklemek
- Bu yaklaşım yalnızca Kubernetes’e değil, load balancer’ın hazır olma durumunu bilmesi gereken diğer ortamlara da uygulanabilir
- Readiness probe, container’ın trafik almaya hazır olup olmadığını periyodik olarak kontrol eder
- HTTP isteği, TCP bağlantısı, komut çalıştırma gibi yöntemlerle health check yapabilir
- Probe başarısız olursa Kubernetes pod’u service endpoint’lerinden kaldırır ve trafik almasını engeller
- Kapanışa hazırlanırken
isShuttingDowngibi biratomic.Boolkullanarak/healthzin HTTP 503 döndürmesini sağlayabilirsiniz - Readiness durumunu başarısız olarak değiştirdikten sonra değişikliğin yayılması için birkaç saniye beklemek gerekir
- Örnek ayar
periodSeconds: 5tir; metindeki örnek 5 saniye bekleme kullanır - Kesin bekleme süresi readiness probe ayarlarına göre değişir
- Örnek ayar
Devam eden istekleri işleme
- Shutdown budget’a uygun şekilde
context.WithTimeoutile bir süre sınırı oluşturupserver.Shutdown(ctx)ye iletin server.Shutdowniki durumda döner- Tüm aktif bağlantılar kapanmış ve tüm handler işlemleri bitmiştir
- Verilen context, handler’lar tamamlanmadan önce sona ermiş ve sunucu beklemekten vazgeçmiştir
- Her iki durumda da
Shutdown, sunucu istek işlemeyi tamamen durdurduktan sonra döner - Handler’lar hızlı ve context-aware çalışmalıdır
- Aksi halde süre sınırı dolduğunda iş ortasında kesilebilirler
- Kısmi yazma, veri kaybı, tutarsız durum, açık transaction, bozulmuş veri gibi sorunlar oluşabilir
- Handler’lara kapatma sinyalini iletmenin iki yaygın yolu vardır
- Middleware ile her isteğin context’ine iptal mantığı enjekte etmek
http.ServerınBaseContextiyle tüm bağlantılarda paylaşılan global context sağlamak
- HTTP sunucusunda özelleştirilebilen context’ler
BaseContextveConnContexttir- Graceful Shutdown için sunucu genelinde geçerli, iptal edilebilir bir global context oluşturmayı sağlayan
BaseContextdaha uygundur
- Graceful Shutdown için sunucu genelinde geçerli, iptal edilebilir bir global context oluşturmayı sağlayan
- Graceful Shutdown, fonksiyonlar context iptaline saygı duyduğunda etkilidir
context.Background(),time.Sleep()gibi iptali yok sayan kullanımlardan kaçınılmalıdırtime.Sleep(duration),selectiletime.After(duration)vectx.Done()ı birlikte bekleyen bir yaklaşımla değiştirilebilir
- Eski Go sürümlerinde
time.After, timer çalışana kadar bellek sızıntısına yol açabilir- Bu sorun Go 1.23 ve sonrasında düzeltilmiştir
- Sürümden emin değilseniz
time.NewTimerveStop, gerekirse de<-t.Ckontrolünü kullanabilirsiniz - İlgili issue: time: stop requiring Timer/Ticker.Stop for prompt GC
Shutdown ve Close farkı
- Aynı ilke yalnızca HTTP sunucuları için değil, üçüncü taraf servisler için de geçerlidir
database/sqldeDB.Closeveritabanı bağlantılarını kapatır, yeni query başlatılmasını engeller ve devam eden query’lerin bitmesini bekler- Esas nokta, artık yeni istek veya mesaj almamak ve mevcut işlere tanımlı grace period içinde bitmeleri için zaman vermektir
server.Close()devam eden bağlantıları beklemeden hemen kapatır- Ağı kullanan handler’lar okuma/yazma sırasında hata alır
- Client’lar
ECONNRESETveyasocket hang upgibi bağlantı hatalarını hemen alabilir - Ağ ile etkileşime girmeyen uzun süreli handler’lar arka planda çalışmaya devam edebilir
server.Shutdown()hata döndürdükten sonraserver.Close()kullanılabilir; ancak bu, kapanış stratejisine bağlıdır- Kapatma sinyalini context üzerinden yaymak daha güvenilir ve graceful bir yaklaşımdır
Kritik kaynakları serbest bırakma sırası
- Yaygın hata, kapatma sinyali alınır alınmaz kritik kaynakları serbest bırakmaktır
- Bu anda handler’lar ve in-flight istekler hâlâ bu kaynakları kullanıyor olabileceğinden, kaynak temizliği shutdown timeout geçene veya tüm istekler bitene kadar ertelenmelidir
- Çoğu durumda yalnızca sürecin sonlanması bile işletim sisteminin kaynakları geri alması için yeterlidir
- Go’nun ayırdığı bellek süreç sonlandığında serbest bırakılır
- Dosya descriptor’ları işletim sistemi tarafından kapatılır
- Process handle gibi OS düzeyindeki kaynaklar da geri alınır
- Açık temizlik gerektiren durumlar da vardır
- Veritabanı bağlantıları düzgün kapatılmalı; açık transaction’lar commit veya rollback edilmelidir
- Mesaj kuyrukları ve broker’lar mesaj flush, offset commit, client kapanış bildirimi gerektirebilir
- Harici servisler bağlantı kopmasını hemen algılamayabilir; bu yüzden bağlantıyı elle kapatmak TCP timeout beklemekten daha hızlı temizlik sağlayabilir
- Bileşenleri başlatmanın ters sırasıyla kapatmak iyi bir kuraldır
- Go’nun
deferi, en son kaydedilen fonksiyonu önce çalıştırdığı için bu kalıba iyi uyar
- Go’nun
- Bellek cache verilerini diske yazmak gerektiğinde olduğu gibi bazı bileşenler için ayrı bir shutdown routine tasarlamak gerekir
Tam örneğin akışı
- Tam örnek,
signal.NotifyContextileSIGINTveSIGTERMalan bir root context oluşturur /healthzendpoint’iisShuttingDowntrue ise HTTP 503 veShutting downdöndürür; değilseOKdöndürür- Örnek istek handler’ı 2 saniye sonra
Hello, world!döndürür veya istek context’i iptal edilirse HTTP request timeout ile yanıt verir BaseContexteongoingCtxbağlanarak in-flight isteklerinSIGTERMden hemen sonra iptal edilmemesi sağlanır- Kapatma sinyali alındığında şu sırayla ilerlenir
- Ek varsayılan işlemeye izin vermek için
stop()çağrılır isShuttingDown.Store(true)ile readiness başarısız duruma getirilir_readinessDrainDelayolan 5 saniye boyunca readiness check yayılımı beklenir_shutdownPeriodolan 15 saniyelik süre sınırıylaserver.ShutdownçağrılırstopOngoingGracefully()ile devam eden context iptal edilirShutdownbaşarısız olursa_shutdownHardPeriodolan 3 saniye boyunca zorunlu iptal bekleme süresi tanınır
- Ek varsayılan işlemeye izin vermek için
1 yorum
Hacker News yorumları
Kubernetes’in bazı yapılandırmalarda load balancer hedef IP’lerini güncellemesinin sandığımdan uzun sürdüğü bir durum başıma geldi. Benim durumumda graceful shutdown’ın %90’ı, pod sonlandırılmadan önce trafiğin gerçekten drain edildiğini garanti etmekti.
Global
preStophook’una 15 saniye sleep koyunca HTTP 503 oranı ciddi biçimde azaldı; load balancer kaydının kaldırılması başladıktan sonra uygulamayaSIGTERMiletilene kadar zaman kazandırdı ve uygulama tarafındaki işlemeyi çok daha basit hale getirdi.preStopsleep, kaliteli rolling deployment sırasında SLO’yu korumak için sihirli bir çözüm gibi.Kubernetes’in iyileştirebileceği iki şey olduğunu düşünüyorum. Pod’lar sonlandırma sekansını başlatmadan önce Endpoints’ten kaldırılmalı ve termination grace gibi bir termination delay seçeneği olmalı. Ayrıca PDB, eviction öncesinde yeniden oluşturmayı mümkün kılan bir seçeneğe sahip olmalı.
Tipik bir Prometheus
/metricsendpoint’ini her N saniyede bir scrape ediyorsanız, son scrape ile gerçek süreç sonlanması arasında kaydedilen metriklerin aktarılmadığı bir aralık oluşur. Bu yüzden sonlandırma sekansında hata olup olmadığına dair yanlış bir izlenim edinebilirsiniz.Dikkatli olmazsanız, servisin kapanmasından hemen önceki birkaç saniyelik logları da kaybedebilirsiniz. Örneğin log dosyası Promtail veya Vector gibi bir sidecar tarafından izleniyorsa ve servis başlangıçta aynı yolu truncate edip yeniden yazıyorsa, kapanış sırasında logların kaybolduğu bir race condition oluşur.
Bu kadar çabaya rağmen verilerin çoğu tamamen görmezden geliniyor; iş içgörüsü de çoğu zaman sunucuya
sshile girip log dosyasınıgreplemenin getto versiyonundan pek daha iyi olmuyor. Bu ekosisteme harcanan emeğin uptime, performans ve kullanılabilirliği anlamlı ölçüde iyileştirip iyileştirmediğinden pek emin değilim.“Log senkronizasyonu”, “ingress’in liveness handler’a yetişmesini bekleme” gibi şeyleri ele almayı planlıyorum.
https://github.com/utrack/caisson-go/blob/main/caiapp/caiapp...
https://github.com/utrack/caisson-go/tree/main/closer
Dokümantasyon hâlâ yetersiz ve eksikler var, ama tatilden döndüğümde ilk sürümü çıkarmayı planlıyorum. Nihayetinde yaygın k8s/otel/grpc+http altyapısını ele alan bir meta platform ve referans platform kütüphanesi olacak.
Bu davranış yüzünden servisimiz hâlâ statsd kullanıyor. Çünkü push tabanlı modelde bu sorun yok.
Sık gördüğüm küçük bir tuzak var:
log.Fatalçağrıldığındadefer’ın çalışacağını sanmak. Aslında çalışmaz.log.Fatal("fatal")içerideos.Exitçağırdığı için hemen çıkar vedeferçalışmaz. Buna karşılıkpanic("fatal"), hemfatalhem dein deferçıktısını gösterir.Dağıtık bir sistemin düzgün çalışması, istemcinin zarifçe kapanması gerektiği varsayımına dayanıyorsa, eninde sonunda büyük bir arıza yaşanması kaçınılmazdır.
Sistemin bileşenlerin hard crash yaşamasına dayanıp dayanmadığını doğrulamanın tek yolu, hard crash’lerin sürekli yaşanan normal bir şey haline gelmesini sağlamaktır. Chaos Monkey’e şan olsun.
Uygulamanın
sig intile kapanması ilekillile öldürülmesi arasında büyük fark var. Örneğin blue-green migration için zarif kapanış davranışı gerekir.Bir daha düşününce belki de gerekiyordur. Bu varsayımın doğru olduğunu garanti etmenin tek yolu bu olabilir. Birkaç yıl önce Netflix’in chaos monkey yaklaşımı gibi.
Yeni servis instance’ının mevcut instance’tan listening socket’i devralıp, gelen hiçbir bağlantıyı kesmeden uygulamayı yeniden başlatma yöntemini ele alacağını sanmıştım.
systemd’de bunu uygulamak nispeten kolay ve nginx de 20 yılı aşkın süredir destekliyor. Ne yazık ki Kubernetes ve Docker bunun load balancer veya reverse proxy tarafından halledildiğini varsaydığı için desteklemiyor.
İş arkadaşım, bir program
ctrl cve birkaç kapatma komutunu düzgün şekilde işleyemiyorsa bunun kötü yazılmış bir program olduğunu hep söylerdi.Elixir’in bu tür şeyleri gerçekten akıllıca ele aldığını düşünüyorum. Çok deneyimli değilim ama küçük VM süreçleri panikleyip sonlanacak ve yeniden oluşturulacak şekilde tasarlandığı için, kasıtlı olarak graceful shutdown rutini yazma ihtiyacı azalıyor gibi görünüyor
Çünkü bu özellik zaten uygulama mimarisine gömülü
Kendi projemde graceful shutdown’ı ele almak için küçük bir kütüphane yaptım: https://github.com/eberkund/graceful
Genelde başlatılması gereken birkaç servis olur ve her birinin başlatma ve kapatma biçimi çoğu zaman farklıdır. Bazen önce nesneyi örneklemek gerekir, bazen iptal etmek istediğiniz bir context vardır, bazen de çağırmanız gereken bir
Stopmetodu olur. Tüm bunları tek tip bir API altında tek bir yerde toplamak için tasarladımhttps://pkg.go.dev/git.sr.ht/~mariusor/wrapper#example-Regis...
Kapanmakta olan bir pod, tanım gereği ready değildir. Service de endpoint’i terminating ve not ready olarak işaretler. Bu, Terminating durumuna geçildiğinde gerçekleştiği için özellikle readiness check’i başarısız kılmaya gerek yoktur
SIGTERMilePod.statusya da endpoint slice gibi nesne güncellemelerinin sırasını tam olarak bilmiyorum.SIGTERMsonrasında da bağlantıların gelebileceği küçük bir pencere olabilir; ancak yazının ima ettiği gibi “readiness check başarısız olana kadar” süren büyük bir aralık değildir. Bir kümeyi yöneten biri olarak, o son derece küçük pencerenin pek önemli olmadığını düşünüyorum. Yeni bağlantı kabul etmeyin, mevcut bağlantıları düzgünce kapatın ve makul ölçüde hızlı sonlanın. Yine de ilgilendiğim uygulamaların yarısı,SIGTERM’i işleyip kapanması uzun süren ya daSIGTERM’i hiç işleyemediği hâlde kapanması uzun süren tarafta yer alıyorJustWatch’ın bazı projelerinde Google Wire’ı benimsedik ve bu oyunun kurallarını değiştirdi. Şaşırtıcı biçimde az biliniyor, ama Kubernetes’te dağınık kapanış mantığını ortadan kaldırmaya yardımcı oluyor
Wire temiz bir dependency injection yaklaşımını zorunlu kıldığı için artık her şey bilinmeyen bir sırayla değil, belirli bir sırayla kapanıyor
https://go.dev/blog/wire
https://github.com/google/wire