Kubernetes graceful shutdown ako kontrakt: nula 502 počas rolloutov (HTTP + gRPC)
Ak ste niekedy spravili rollout Deploymentu a videli:
- burst 502/504 z ingressu,
- ECONNRESET / “connection reset by peer” v klientoch,
- gRPC UNAVAILABLE špičky,
- a potom sa to “nejako” ustálilo…
…tak poznáte nepríjemnú pravdu: “graceful shutdown” nie je boolean. Je to kontrakt medzi:
- klientom (keepalive, retry, reuse spojení),
- LB/ingress/sidecarom (draining správanie),
- Kubernetes propagáciou endpointov (EndpointSlice → kube-proxy),
- a aplikáciou (SIGTERM handling, odmietnutie novej práce, dobehnutie in-flight práce).
Tento článok je praktický, reprodukovateľný postup ako spraviť rollouty nudné (v dobrom).
Testované na: Kubernetes 1.27–1.30, NGINX Ingress a Envoy-based proxy, Go HTTP servery a gRPC služby.
Čo musí “graceful shutdown” garantovať
Definujte si Drain Contract s jasnými invariantmi:
- Najprv zastav routing novej prevádzky (z pohľadu Kubernetes)
- Prestaň prijímať novú prácu (v procese)
- DobeHni alebo ukonči in-flight prácu v ohraničenom čase
- Až potom skonči proces, skôr než príde SIGKILL
Ak chýba hociktorý bod, rollouty budú produkovať chyby aj keď “správne” chytáte SIGTERM.
Ako v skutočnosti prebieha terminácia Podu
Keď sa Pod terminujete, Kubernetes (zjednodušene) spraví:
- Označí Pod deletion timestampom.
- Spustí
preStophook (ak existuje). - Pošle kontajnerom SIGTERM.
- Čaká do
terminationGracePeriodSeconds. - Ak proces stále žije, pošle SIGKILL.
Routing sa však vypína oddelene – závisí od:
- readiness stavu a aktualizácie EndpointSlices,
- propagácie cez dataplane (kube-proxy/IPVS/eBPF),
- a od reuse spojení v klientoch (keepalive).
To znamená: počas terminácie môžete stále dostávať requesty, ak nerobíte aktívny drain.
Kľúčová myšlienka: readiness-driven draining
Najspoľahlivejší pattern:
- Readiness endpoint začne vracať not ready hneď ako začne draining.
- Pri terminácii prepnete aplikáciu do “draining módu” skôr než začnete vypínať server.
Draining môžete spustiť:
- cez
preStop(lokálne HTTP/exec volanie – konzistentné a testovateľné), - alebo cez SIGTERM handler (tiež OK, len si odmerajte timing).
Drain budget matematika (nehádajte)
Grace period musí pokryť:
grace >= endpoint_propagation + drain_delay + worst_case_request_time + safety_margin
Kde:
- endpoint_propagation: čas na update EndpointSlice + stop routing v dataplane
- drain_delay: krátke čakanie po prepnutí na NotReady (nech sa routing “rozleje”)
- worst_case_request_time: reálny upper bound requestu (alebo enforce deadline)
- safety_margin: buffer na jitter
Nemusíte trafiť presné čísla. Potrebujete odmerané čísla.
Referenčná implementácia: Kubernetes YAML
1) Readiness probe (musí reflektovať draining)
readinessProbe:
httpGet:
path: /health/ready
port: 8080
periodSeconds: 2
timeoutSeconds: 1
failureThreshold: 1
Poznámky:
- Probe má byť rýchly.
failureThreshold: 1spraví readiness veľmi citlivý (na liveness to často nechcete).
2) preStop hook: spusti draining
lifecycle:
preStop:
httpGet:
path: /admin/drain
port: 8080
A terminationGracePeriodSeconds nastavte podľa budgetu:
terminationGracePeriodSeconds: 60
Prečo nie preStop: sleep 10?
- Sleep mimo aplikácie nezabráni prijímaniu nových requestov.
- Stratíte kontrolu aj observability.
Referenčná implementácia: draining v aplikácii (Go príklady)
Implementovať sa to dá v každom jazyku. Toto je logika.
HTTP server: odmietni nové requesty, dobehni in-flight
// Ilustračný Go-like pseudokód.
// Readiness závisí od drain flagu a terminácia má hard deadline.
var draining atomic.Bool
func readyHandler(w http.ResponseWriter, r *http.Request) {
if draining.Load() {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
}
func drainHandler(w http.ResponseWriter, r *http.Request) {
draining.Store(true)
w.WriteHeader(http.StatusOK)
}
func main() {
srv := &http.Server{Addr: \":8080\", Handler: mux()}
go func() {
<-sigterm
draining.Store(true)
// Daj čas na konvergenciu routingu.
time.Sleep(5 * time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
}()
_ = srv.ListenAndServe()
}
gRPC server: GracefulStop s hard capom
gRPC (hlavne streaming) vie blokovať dlho. Chcete:
- skúsiť graceful stop,
- ale s maximálnym drain časom (potom hard stop).
go func() {
<-sigterm
draining.Store(true)
time.Sleep(5 * time.Second)
done := make(chan struct{})
go func() {
grpcServer.GracefulStop()
close(done)
}()
select {
case <-done:
case <-time.After(45 * time.Second):
grpcServer.Stop()
}
}()
Repro lab: dokážte to číslami (pred/po)
Krok 1: generujte load
hey -z 2m -c 50 http://my-service.default.svc.cluster.local/
Na gRPC sa často používa ghz.
Krok 2: opakujte rollout
kubectl rollout restart deploy/my-service
kubectl rollout status deploy/my-service
Krok 3: sledujte propagáciu endpointov
kubectl get endpointslices -l kubernetes.io/service-name=my-service -w
Hľadajte:
- či endpointy miznú rýchlo po draining začiatku,
- či load stále naráža na chyby počas okna.
Krok 4: porovnajte error rate
Sledujte:
- HTTP 5xx / reset
- gRPC UNAVAILABLE / CANCELLED
- p95/p99 latenciu počas rollout okna
Najčastejšie failure módy
“Máme preStop sleep” a stále dropujeme requesty
Symptóm:
- chyby pretrvávajú
- readiness je stále OK počas sleep
Príčina:
- app stále prijíma requesty
Fix:
- readiness-driven drain, nie externý sleep
Krátky grace period → SIGKILL → rozbité in-flight
Fix:
- upravte
terminationGracePeriodSecondspodľa budgetu - enforce request deadline (aby ste mali reálny upper bound)
Long-lived streams (gRPC streaming, WebSockets)
Fix:
- max drain window
- stream limity / keepalive policy
- klientská reconnect stratégia
Retry amplifikuje rollout
Fix:
- retry budget a deadline alignment
- bounded retries + backoff
Čo by som spravil v produkcii
- Readiness reflektuje draining (nikdy “Ready” pri shutdown)
preStopspúšťa draining (nie sleep)- Krátky, odmeraný routing convergence delay
- Bounded timeouts na shutdown
terminationGracePeriodSecondsz drain budgetu- Rollout SLO: “error rate počas deploy okna”
- Rehearsal: rollout lab aspoň na jednej kritickej službe
FAQ
Prečo vidím chyby aj keď chytám SIGTERM?
Lebo stop-routing nie je okamžitý: EndpointSlice + dataplane propagácia + reuse spojení znamenajú krátke “doznievanie”.
Mám hneď failnúť readiness pri SIGTERM?
Zvyčajne áno – ak readiness znamená “môžem bezpečne prijímať nové requesty”.
Kedy je preStop: sleep OK?
Len ako núdzové riešenie a len ak app okamžite odmieta novú prácu. Inak je to “čakaj, ale stále prijímaj traffic”.
Súvisiace články
/sk/blog/conntrack-stale-nat-mapovanie/(deploy 503, ktoré nie sú graceful shutdown)/sk/blog/kubernetes-ghost-pod-conntrack/(prečo traffic vie ísť aj na “mŕtvy” Pod)/sk/blog/k8s-postgresql-connection-storm/(rollout ako systémový event)
Further reading
- https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/
- https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/
- https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
- https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/
- https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#rolling-update-deployment
- https://pkg.go.dev/net/http#Server.Shutdown
- https://pkg.go.dev/google.golang.org/grpc#Server.GracefulStop
- https://grpc.io/docs/what-is-grpc/core-concepts/
Súvisiace články
Pod zaseknutý v Terminating: produkčný rozhodovací strom pre finalizery, volume a mŕtve nody
Konzervatívny runbook na bezpečné odblokovanie Terminating Podov: finalizery, CSI/volume cleanup, mŕtve nody a kedy (a ako) použiť force delete.
Kubernetes APF vyhladovanie: keď jeden controller zablokuje kubectl
APF vie vyhladovať Kubernetes API: kubectl visí, controllery timeoutujú a rastú 429. Runbook na izoláciu klienta, úpravu FlowSchema a verifikáciu.
OpenTelemetry Collector backpressure: dropy, memory_limiter a queue ako guardrails
OpenTelemetry Collector pri loade dropuje spany kvôli backpressure exportérov. Oprava cez memory_limiter, queue a batch tuning + verifikácia.
Envoy outlier detection brownouty: keď mesh vyhodí zdravé pody
Debug Istio/Envoy outlier detection brownoutov: prečo mesh vyhadzuje zdravé pody a rastú 503 v produkcii. Obsahuje xDS checks, bezpečné fixy a alerty.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.