Kubernetes Health Probe'ları: Startup, Liveness, Readiness ve ASP.NET Core Health Check'leri
Startup probe açılışı bekler, liveness takılan process'i yeniden başlatır, readiness pod'u trafikten çeker; ASP.NET Core'da tag'li health check'lerle her probe'a ayrı endpoint verilir. 40 saniyelik ısınmanın liveness ile sonsuz restart döngüsüne girmesini, startupProbe ile sıfır restart'ı, liveness'a veritabanı koyunca iki dakikada dörder restart'lık zinciri ve probe sürelerinin hesabını gerçek çıktılarla görüyoruz.
Sürüm notu: Bu yazıdaki örnekler kind 0.33 ile Kubernetes 1.37, kubectl 1.37, .NET 10 (ASP.NET Core 10.0.14), Npgsql 10.0 ve PostgreSQL 18 ile denenmiştir.
Önce Tahmin Et
Bookstore API'sinin (Application Programming Interface) iki ayrı Deployment'ı var. İkisinin de readiness probe'u PostgreSQL'i kontrol ediyor. Birincisinin liveness probe'u yalnız process'in ayakta olduğuna bakan /health/live'a, ikincisininki veritabanını da kontrol eden /health'e gidiyor. PostgreSQL iki dakikalığına gidiyor. Bu iki dakikanın sonunda iki Deployment'ın pod'ları kaç kez yeniden başlamış olur? İkinci soru: açılışta 40 saniye ısınan bir uygulamaya periodSeconds: 5, failureThreshold: 3 olan bir liveness probe'u verirsen ne olur?
Üç Probe, Üç Ayrı Soru
kubelet her container için üç farklı soruyu sorabilir ve her birinin cevabına farklı tepki verir:
| Probe | Sorduğu soru | Başarısız olursa |
|---|---|---|
startupProbe | Uygulama açılışını bitirdi mi? | Süre dolarsa container yeniden başlatılır; başarılı olana kadar diğer iki probe çalışmaz |
livenessProbe | Process kurtarılamaz biçimde takıldı mı? | Container öldürülür ve yeniden başlatılır |
readinessProbe | Şu an trafik alabilir mi? | Pod Service'in EndpointSlice'ında ready: false olur; restart yok |
Readiness pod'un bütün ömrü boyunca çalışır; pod hazır ve hazır değil durumları arasında gidip gelebilir. Beşinci yazıda gördüğümüz gibi readiness'ı düşen pod trafikten çekilir ama yaşamaya devam eder. Liveness ise geri dönüşü olmayan bir karar verir: process'i öldürmek. Probe'ların zamanlama alanları ve varsayılanları, cluster'ın kendi şemasına göre (kubectl explain pod.spec.containers.livenessProbe): periodSeconds 10, timeoutSeconds 1, failureThreshold 3, successThreshold 1 (liveness ve startup için 1 olmak zorunda), initialDelaySeconds 0.
ASP.NET Core Tarafı: Tag'li Health Check'ler
Health check'lerin mekaniği ASP.NET Core Web API serisinin Health Check yazısında; burada yalnız Kubernetes'in ihtiyaç duyduğu ayrımı kuruyoruz. Her check'e bir tag veriyor, her probe için yalnız ilgili tag'leri çalıştıran ayrı bir endpoint açıyoruz:
builder.Services.AddHealthChecks()
.AddCheck<LivenessSwitchCheck>("self", tags: ["live"])
.AddCheck<DatabaseHealthCheck>("database", tags: ["ready"]);
app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = c => c.Tags.Contains("live") });
app.MapHealthChecks("/health/ready", new HealthCheckOptions { Predicate = c => c.Tags.Contains("ready") });
app.MapHealthChecks("/health");
LivenessSwitchCheck process içindeki bir bayrağa bakıyor; bayrağı birazdan bir diagnostics endpoint'iyle bozacağız ve gerçek hayattaki bir deadlock'u taklit edeceğiz. DatabaseHealthCheck PostgreSQL'e 2 saniyelik timeout'la bağlanıp SELECT 1 çalıştırıyor. Predicate'siz /health ise bütün check'leri çalıştırıyor. Deployment'ta üç probe:
startupProbe:
httpGet:
path: /health/live
port: http
periodSeconds: 2
failureThreshold: 30
livenessProbe:
httpGet:
path: /health/live
port: http
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: http
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
Startup ve liveness aynı endpoint'e gidiyor, ama farklı sorular soruyor: startup "açıldın mı?" diye sık ve sabırla (2 saniyede bir, 30 deneme, yani 60 saniye bütçe), liveness "hâlâ yaşıyor musun?" diye seyrek ve çabuk karar vererek. Readiness ayrı bir endpoint'e gidiyor çünkü ayrı bir soru soruyor. Cluster içinden üç endpoint:
/health/live -> Healthy HTTP/1.1 200 OK
/health/ready -> Healthy HTTP/1.1 200 OK
/health -> HTTP/1.1 200 OK Healthy
Yavaş Başlangıç ve startupProbe
Gerçek uygulamalar her zaman bir saniyede açılmaz: EF (Entity Framework) Core model'inin ilk kez kurulması, büyük bir cache'in ısıtılması, uzak bir configuration kaynağından okuma. Bookstore API'sinde bunu Startup__WarmupSeconds ayarıyla taklit ediyoruz: uygulama port'u açmadan önce verilen süre kadar bekliyor. 40 saniyelik ısınmayı yalnız bir liveness probe'u (periodSeconds: 5, failureThreshold: 3) ile çalıştırdım:
+0s 0/1 ContainerCreating 0
+4s 1/1 Running 0
+16s 1/1 Running 1 (1s ago)
+33s 1/1 Running 2 (3s ago)
+49s 1/1 Running 3 (4s ago)
+61s 0/1 CrashLoopBackOff 3 (1s ago)
+85s 1/1 Running 4 (25s ago)
+98s 0/1 CrashLoopBackOff 4 (2s ago)
+155s 1/1 Running 5 (59s ago)
$ kubectl get events --sort-by=.lastTimestamp
Normal Killing pod/api-slow-7b8669f69b-568zt Container api failed liveness probe, will be restarted
Warning Unhealthy pod/api-slow-7b8669f69b-568zt Liveness probe failed: Get "http://10.244.1.27:8080/health/live": dial tcp 10.244.1.27:8080: connect: connection refused
İkinci sorunun cevabı: uygulama hiçbir zaman açılamaz. Liveness 15 saniye (3 × 5) sonra pes edip container'ı öldürüyor, uygulama ısınmasını bitiremeden baştan başlıyor ve araya giren CrashLoopBackOff gecikmeleriyle sonsuz bir döngüye giriyor. Dikkat et, READY sütunu çoğu zaman 1/1: bu Deployment'ta readiness probe'u yok, bu yüzden port'u bile açmamış bir container "hazır" sayılıyor. İkinci bir hata.
Klasik çözüm liveness'a initialDelaySeconds: 60 vermekti. Sorunu: 60 saniye her container başlangıcında beklenir, uygulama 5 saniyede açılsa bile; ve açılış bir gün 61 saniye sürerse döngü geri döner. startupProbe bu ikisini ayırır. Aynı Deployment'a periodSeconds: 2, failureThreshold: 45 (90 saniye bütçe) olan bir startup probe ekledim:
+0s 0/1 ContainerCreating 0
+4s 0/1 Running 0
+45s 1/1 Running 0
$ kubectl get events --sort-by=.lastTimestamp
Warning Unhealthy pod/api-slow-7cbb485f6-gcdcj Startup probe failed: Get "http://10.244.1.28:8080/health/live": dial tcp 10.244.1.28:8080: connect: connection refused
Startup probe 40 saniye boyunca başarısız oldu ama bütçesi dolmadığı için kimse container'ı öldürmedi; ilk başarılı yanıtta liveness devreye girdi ve pod 45. saniyede hazır oldu, sıfır restart. Bütçeyi gözlemlediğin en uzun açılış süresinin rahatça üstünde tut; liveness ise açılıştan bağımsız olarak hızlı karar vermeye devam eder.
Liveness'a Veritabanı Koymak: Zincirleme Restart
Şimdi baştaki ilk soru. api Deployment'ının liveness'ı /health/live'a, aynısının kopyası api-badlive'ınki /health'e gidiyor. Her ikisi de iki replica. PostgreSQL'i sıfıra indirdim ve dört saniyede bir pod'ları kaydettim (sütunlar: Deployment, READY, STATUS, RESTARTS):
postgres down at 04:42:11
04:42:11 api 1/1 Running 0 | api 1/1 Running 0 | api-badlive 1/1 Running 0 | api-badlive 1/1 Running 0 |
04:42:20 api 1/1 Running 0 | api 0/1 Running 0 | api-badlive 0/1 Running 0 | api-badlive 0/1 Running 0 |
04:42:24 api 0/1 Running 0 | api 0/1 Running 0 | api-badlive 0/1 Running 0 | api-badlive 0/1 Running 0 |
04:42:36 api 0/1 Running 0 | api 0/1 Running 0 | api-badlive 0/1 Running 1 | api-badlive 0/1 Running 1 |
04:43:08 api 0/1 Running 0 | api 0/1 Running 0 | api-badlive 0/1 Running 2 | api-badlive 0/1 Running 2 |
04:43:36 api 0/1 Running 0 | api 0/1 Running 0 | api-badlive 0/1 Running 3 | api-badlive 0/1 Running 3 |
04:44:09 api 0/1 Running 0 | api 0/1 Running 0 | api-badlive 0/1 Running 4 | api-badlive 0/1 Running 4 |
$ kubectl describe pod api-badlive-76f9678986-gptqn
Normal Killing 15s (x4 over 105s) kubelet spec.containers{api-badlive}: Container api-badlive failed liveness probe, will be restarted
İki dakikada api'nin pod'ları sıfır, api-badlive'ınkiler dörder kez yeniden başladı. Dört pod'un hepsi 10 saniye içinde trafikten çekildi; bu readiness'ın işi ve iki Deployment'ta da aynı. Fark liveness'ta: veritabanı yokken api-badlive'ın her container'ı 30 saniyede bir öldürülüp yeniden başlatıldı. Bu restart'lar hiçbir şeyi düzeltmez, çünkü sorun pod'da değil. Üstelik zarar verir: her restart bağlantı havuzlarını, cache'leri ve uçuştaki istekleri çöpe atar, veritabanı geri geldiğinde onlarca pod aynı anda soğuk başlayıp ona yüklenir ve restart sayısı arttıkça CrashLoopBackOff gecikmeleri toparlanmayı geciktirir. Kubernetes dokümantasyonu da liveness probe'larının bağımlılıkları değil yalnız process'in kendisini kontrol etmesini öneriyor.
Pod'un içinden üç endpoint'e bakınca ayrım net:
/health/live -> Healthy HTTP/1.1 200 OK
/health/ready -> HTTP/1.1 503 Service Unavailable wget: server returned error: HTTP/1.1 503 Service Unavailable
/health -> HTTP/1.1 503 Service Unavailable wget: server returned error: HTTP/1.1 503 Service Unavailable
Bir de readiness tarafındaki karar var: veritabanı gidince bütün replica'ların trafikten çekilmesi doğru mu? Bookstore'da kitap listesi veritabanı olmadan çalışamıyor, ama /api/info ya da cache'ten dönen bir katalog çalışabilir. Readiness'ı düşen bir pod 503 yerine "connection refused" üretir (beşinci yazıda gördük); uygulama ayakta kalıp anlamlı bir 503 ya da kısmi bir yanıt dönebiliyorsa, readiness'a veritabanını koymamak ve hatayı uygulama içinde ele almak daha iyi bir kullanıcı deneyimi verebilir. Kural: readiness'a yalnız pod'un tek başına çözemeyeceği ve onsuz hiçbir isteğe anlamlı cevap veremeyeceği bağımlılıkları koy.
Liveness Ne Zaman İşe Yarar?
Liveness'ın asıl işi, process'in yaşadığı ama hiçbir zaman kendiliğinden düzelmeyeceği durumları yakalamak: bir deadlock, tükenmiş bir thread pool, bozulmuş bir iç durum. Bunu taklit etmek için bir pod'da liveness bayrağını bozdum:
04:44:30 break
{"pod":"api-65986689d6-n9fzz","live":false}
04:44:58 1/1 Running 0 3m
04:45:01 1/1 Running 1 (3s ago)
Normal Killing 33s kubelet spec.containers{api}: Container api failed liveness probe, will be restarted
28 saniye sonra container yeniden başladı ve temiz bir process'le geri geldi. Tespit süresi kabaca periodSeconds × failureThreshold, yani 10 × 3 = 30 saniye. Bu arada pod 1/1 görünmeye devam etti, çünkü readiness yalnız veritabanına bakıyor; gerçek bir deadlock'ta HTTP (Hypertext Transfer Protocol) isteklerine de cevap gelmeyeceği için readiness da düşerdi.
Süre Hesabı: period, timeout ve failureThreshold
Probe ayarları birer sayı değil, birer karar. Her biri için sorman gereken soru farklı:
- Startup bütçesi =
periodSeconds × failureThreshold. En yavaş gözlenen açılışın (yük altında, soğuk node'da) en az iki katı. - Liveness tespit süresi ≈
periodSeconds × failureThreshold(+ son denemenintimeoutSeconds'ı). Takılmış bir pod'un ne kadar süre kapasite işgal etmesine razısın? Çok kısa tutarsan GC (Garbage Collector) duraklaması ya da anlık CPU (Central Processing Unit) sıkışması gibi geçici yavaşlıklar restart üretir. - Readiness tepki süresi ≈
periodSeconds × failureThreshold. Burada daha agresif olabilirsin, çünkü yanlış alarmın bedeli yalnız geçici olarak trafikten çıkmak. timeoutSeconds'ın varsayılanı 1 saniye. CPU limit'i olan ve throttle edilen bir .NET pod'unda (dokuzuncu yazı) bir saniye kolayca aşılır; health endpoint'leri hafif olsa bile 2–3 saniye daha gerçekçi.
gRPC Probe'ları ve Ayrı Management Port'u
HTTP dışında üç probe türü daha var: tcpSocket (port açık mı), exec (container içinde bir komut, PostgreSQL'de pg_isready gibi) ve grpc. gRPC (gRPC Remote Procedure Calls) probe'u standart gRPC Health Checking Protocol'ünü kullanır ve 1.27'den beri stable; ASP.NET Core'da Grpc.AspNetCore.HealthChecks paketi bu servisi health check'lerinize bağlar. Shell'siz chiseled image'larda exec probe'ları zaten seçenek değil; HTTP ya da gRPC kullan.
Health endpoint'lerini uygulamanın trafiğiyle aynı port'tan sunmak iki soru doğurur: Gateway üzerinden internete açılıyorlar mı, ve uygulamanın authentication (kimlik doğrulama) middleware'i probe isteklerini engelliyor mu? Yaygın çözüm ayrı bir management port'u: Kestrel'i ASPNETCORE_HTTP_PORTS="8080;8081" ile iki port dinleyecek şekilde açıp health endpoint'lerini RequireHost("*:8081") ile yalnız 8081'e bağlamak, Service'e ve Gateway'e yalnız 8080'i vermek. Probe'lar container port'una doğrudan gittiği için Service'te tanımlı olmayan bir port'a da probe atabilirsin.
Üretimde Dikkat
Liveness'ı tanımlamamak da bir seçenek. Process çöktüğünde kubelet onu liveness olmadan da yeniden başlatır; liveness yalnız "yaşıyor ama işe yaramıyor" durumları içindir. Uygulamanın böyle bir durumu güvenilir biçimde tespit edemiyorsa, kötü bir liveness probe'u hiç olmamasından daha tehlikelidir.
Health endpoint'lerini hafif tut. Her probe isteği bir HTTP isteği; her 5 saniyede bir veritabanına sorgu atan readiness, 50 replica'da saniyede 10 sorgu demek. Pahalı kontrolleri cache'le ya da arka planda periyodik olarak yapıp sonucu oku.
Probe başarısızlıklarını izle. Unhealthy event'leri ve RESTARTS sayacı metrik sistemine gitmeli; zincirleme restart başladığında bunu kullanıcıdan önce görmek istersin. Ölçüm tarafı ilerideki Observability serisinde.
Sık Düşülen Tuzaklar
Aynı endpoint'i üç probe'a vermek. Özellikle veritabanını da kontrol eden bir /health'i liveness'a vermek, yukarıdaki zincirleme restart'ın ta kendisi.
Readiness probe'u tanımlamamak. Yavaş açılış deneyinde gördüğün gibi, readiness yoksa port'u bile açmamış container hazır sayılır ve rolling update ona trafik gönderir.
initialDelaySeconds ile yavaş açılışı çözmeye çalışmak. Her açılışta boşa beklenir ve tahmin bir gün yetmez. startupProbe kullan.
Probe'ları authentication'ın arkasında bırakmak. Global bir authorization policy'si health endpoint'lerine 401 döndürürse pod hiç hazır olmaz. Health endpoint'lerinde AllowAnonymous kullan ya da ayrı port'a al.
Küçük Bir Deneme
★☆☆ api Deployment'ının readiness'ını /health/live'a çevir, PostgreSQL'i sıfıra indir ve /api/books isteğinin artık "connection refused" yerine ne döndürdüğüne bak. Hangisi kullanıcı için daha iyi?
★★☆ Liveness'a timeoutSeconds: 1 ver ve /api/diag/burn?seconds=30 ile pod'un CPU'sunu meşgul ederken (pod'a limits.cpu: 200m vererek) restart olup olmadığını ölç.
★★★ Health endpoint'lerini 8081 port'una taşı: ASPNETCORE_HTTP_PORTS ile iki port aç, MapHealthChecks(...).RequireHost("*:8081") kullan, probe'ları 8081'e çevir ve Service üzerinden /health/ready'nin artık 404 döndüğünü doğrula.
Probe'lar Kubernetes'e uygulamanın durumunu söylüyor. Ama Kubernetes'in pod'u nereye yerleştireceğine ve ne kadar kaynak ayıracağına karar verebilmesi için başka bir bilgiye ihtiyacı var. Bir sonraki yazıda resource request ve limit'leri, QoS (Quality of Service) class'larını, OOMKilled'ı, CPU throttling'i ve .NET GC'nin container'daki davranışını ölçüyoruz.