3 puan yazan GN⁺ 2025-05-06 | 1 yorum | WhatsApp'ta paylaş
  • 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, SIGTERM ve SIGINT gibi kapatma sinyallerini os/signal veya Go 1.16 ve üzerindeki signal.NotifyContext ile 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; preStop gecikmesi veya readiness probe hatasıyla, trafik durdurma durumunun harici load balancer’a kadar yayılması için zaman ayrılmalıdır
  • http.Server.Shutdown yeni 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
    • SIGKILL gibi bazı sinyaller yakalanamaz veya yok sayılamaz ve süreci sonlandırır
  • Go runtime, main fonksiyonu çalışmadan önce SIGTERM, SIGQUIT, SIGILL, SIGTRAP gibi 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 sinyaldir
    • SIGINT: Kullanıcı terminalde Ctrl+C ile süreci durdurmaya çalıştığında gönderilir
    • SIGHUP: 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, SIGHUP alı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 select ve default kullanı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
  • signal.Notify aynı sinyal için birden fazla kez çağrılabilir; Go ilgili sinyali kayıtlı tüm kanallara gönderir
  • Ctrl+Cye birden çok kez basılsa da genelde ikinci giriş otomatik olarak SIGKILLe yükseltilmez
    • Çoğu bash veya Linux shell’i otomatik yükseltme yapmaz
    • Zorla sonlandırma için kill -9 ile doğrudan SIGKILL gönderilmelidir
  • Yerel geliştirmede ikinci Ctrl+Cnin zorla sonlandırma yapmasını istiyorsanız ilk sinyali aldıktan hemen sonra signal.Stop ile ek sinyal alımını durdurabilirsiniz
  • Go 1.16’dan itibaren signal.NotifyContext ile sinyal işlemeyi context cancellation’a bağlayabilirsiniz
    • ctx.Done() sonrasında da stop() çağrılmalıdır; böylece ikinci Ctrl+C uygulamayı 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, terminationGracePeriodSeconds ayrıca belirtilmediyse 30 saniyedir
  • Bu süre geçince Kubernetes uygulamayı zorla durdurmak için SIGKILL gönderir
    • SIGKILL yakalanamaz 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/http paketinde Graceful Shutdown, http.Server.Shutdown ile 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 refused hatası 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 durumunun Terminatinge 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
    • preStop hook’unda bir süre sleep ederek harici load balancer’ın pod’un kapanış durumunu fark etmesine zaman tanımak
      • preStopta geçen süre terminationGracePeriodSeconds kapsamı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 isShuttingDown gibi bir atomic.Bool kullanarak /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

Devam eden istekleri işleme

  • Shutdown budget’a uygun şekilde context.WithTimeout ile bir süre sınırı oluşturup server.Shutdown(ctx)ye iletin
  • server.Shutdown iki 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ın BaseContextiyle tüm bağlantılarda paylaşılan global context sağlamak
  • HTTP sunucusunda özelleştirilebilen context’ler BaseContext ve ConnContexttir
    • Graceful Shutdown için sunucu genelinde geçerli, iptal edilebilir bir global context oluşturmayı sağlayan BaseContext daha uygundur
  • Graceful Shutdown, fonksiyonlar context iptaline saygı duyduğunda etkilidir
    • context.Background(), time.Sleep() gibi iptali yok sayan kullanımlardan kaçınılmalıdır
    • time.Sleep(duration), select ile time.After(duration) ve ctx.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

Shutdown ve Close farkı

  • Aynı ilke yalnızca HTTP sunucuları için değil, üçüncü taraf servisler için de geçerlidir
  • database/sqlde DB.Close veritabanı 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 ECONNRESET veya socket hang up gibi 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 sonra server.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
  • 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.NotifyContext ile SIGINT ve SIGTERM alan bir root context oluşturur
  • /healthz endpoint’i isShuttingDown true ise HTTP 503 ve Shutting down döndürür; değilse OK dö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
  • BaseContexte ongoingCtx bağlanarak in-flight isteklerin SIGTERMden 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
    • _readinessDrainDelay olan 5 saniye boyunca readiness check yayılımı beklenir
    • _shutdownPeriod olan 15 saniyelik süre sınırıyla server.Shutdown çağrılır
    • stopOngoingGracefully() ile devam eden context iptal edilir
    • Shutdown başarısız olursa _shutdownHardPeriod olan 3 saniye boyunca zorunlu iptal bekleme süresi tanınır

1 yorum

 
GN⁺ 2025-05-06
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 preStop hook’una 15 saniye sleep koyunca HTTP 503 oranı ciddi biçimde azaldı; load balancer kaydının kaldırılması başladıktan sonra uygulamaya SIGTERM iletilene kadar zaman kazandırdı ve uygulama tarafındaki işlemeyi çok daha basit hale getirdi.

    • Doğru. preStop sleep, 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 /metrics endpoint’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.

    • Observability stack’i biraz saçma görünüyor. Loglar, metrikler ve trace’lerin her birinin kendi veritabanı, sidecar’ı ve görselleştirme stack’i var; dillere göre entegrasyon kütüphaneleri birbirinden farklı ve bulut maliyeti de muazzam.
      Bu kadar çabaya rağmen verilerin çoğu tamamen görmezden geliniyor; iş içgörüsü de çoğu zaman sunucuya ssh ile 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.
    • Son 8+ yıldır Go ile yüksek yük altındaki uygulamalarda yaşadığım sorunları platform kütüphanesinde tam da bu şekilde ele alıyorum. Şirketten şirkete platform ve rolling deployment geliştirip iyileştirmek hobim olmuştu.
      “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.
    • Prometheus ve ilgili araçların neden pull model kullandığını hiçbir zaman anlayamadım. Çoğu şey push model kullanıyor sonuçta.
    • Bu sorun için pratik bir çözüm görüp görmediğinizi merak ediyorum. Scrape aralığı 15 saniyeyse, metrikleri iki kez kaydetmek için 30 saniye bekleyemezsiniz.
      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ığında defer’ın çalışacağını sanmak. Aslında çalışmaz.
    log.Fatal("fatal") içeride os.Exit çağırdığı için hemen çıkar ve defer çalışmaz. Buna karşılık panic("fatal"), hem fatal hem de in 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.

    • Bu inanca o kadar bağlıyım ki tasarım yaparken graceful shutdown’ın kendisini dikkate almıyorum. Bileşenler güvenli biçimde, hatta sık sık hard crash yaşayabilmeli; sistemin önemli bir oranı amaçlandığı gibi çalışıyorsa bunun tüm sistem üzerinde anlamlı bir etkisi olmamalı.
      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.
    • İstemcilere veya workflow’lara nazik davranmak için graceful shutdown yapmak ile sistemin çalışması için istemcilerin buna dayanmak zorunda olması arasında büyük fark var.
    • Eski fiziksel sunucu günlerinde bunun için STONITH kullanırdık: https://smcleod.net/2015/07/delayed-serial-stonith/
    • Kurtarılabilir bir durumda bile normal bir kapanışın felaket niteliğinde bir kapanış gibi görünmemesi için geçerli nedenler var.
      Uygulamanın sig int ile kapanması ile kill ile öldürülmesi arasında büyük fark var. Örneğin blue-green migration için zarif kapanış davranışı gerekir.
    • Doğru. Yine de yazılımın fişi çekildiğinde dayanacak şekilde tasarlanmış olması, kapatırken gerçekten fişini çekmeniz gerektiği anlamına gelmez.
      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 c ve birkaç kapatma komutunu düzgün şekilde işleyemiyorsa bunun kötü yazılmış bir program olduğunu hep söylerdi.

    • Ctrl-C panoya kopyalama için ayrılmıştır. Programı durdurma eylemi olarak kullanmak son derece sezgi dışıdır ve kullanıcıları kızdıracaktır.
  • 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ü

    • Bunun, yazarın ele aldığı graceful shutdown ihtiyacını nasıl ortadan kaldırdığını merak ediyorum
  • 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 Stop metodu olur. Tüm bunları tek tip bir API altında tek bir yerde toplamak için tasarladım

  • 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
    SIGTERM ile Pod.status ya da endpoint slice gibi nesne güncellemelerinin sırasını tam olarak bilmiyorum. SIGTERM sonrası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 da SIGTERM’i hiç işleyemediği hâlde kapanması uzun süren tarafta yer alıyor

  • JustWatch’ı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