open atlas
↑ Back to track
Docker, containers as a system DOCK · 10 · 02

Health and lifecycle: honest probes, the SIGTERM grace window, and preStop

A health check that pings a port while the app is wedged is worse than none. Honest health means liveness, readiness and startup are distinct, and the SIGTERM → grace → SIGKILL lifecycle with a preStop hook is what makes a rollout drop zero requests.

DOCK Senior ◷ 16 min
Level
FoundationsJuniorMiddleSenior

The dashboards were all green while the checkout API was down. Every pod reported healthy, the load balancer kept routing, and customers got timeouts. The health check was a TCP probe on the listening port: the Go process was still alive and the socket still open, so the probe passed — but every request was blocked on a connection-pool deadlock against a database that had failed over. The app was a corpse with a heartbeat. The fix was not more monitoring; it was an honest health check. The readiness probe was changed to hit a /readyz endpoint that actually ran a 200 ms query against the pool, and it went red within one interval. The load balancer pulled the pod, traffic shifted to healthy replicas, and the on-call alert fired on the cause instead of the symptom. A health check that cannot fail when the app is wedged is not a safety net — it is a green light painted over a brick wall.

Three probes, three different questions

The cardinal mistake is one health check answering “is the process up?” when production needs three different answers. Docker’s HEALTHCHECK and Kubernetes’ probes formalize the distinction:

HEALTHCHECK --interval=10s --timeout=2s --start-period=30s --retries=3 \
  CMD curl -f http://localhost:8080/healthz || exit 1
#   --interval: run every 10s   --retries=3: 3 consecutive fails → unhealthy
#   --start-period=30s: failures during the first 30s don't count (slow boot grace)
  • Liveness answers “is this process wedged and unrecoverable?” — if it fails, the orchestrator restarts the container. Make it cheap and dependency-free: a wedged event loop or a deadlocked process should fail it, but a transient database blip should not, or you turn a downstream outage into a restart storm that makes everything worse.
  • Readiness answers “should this instance receive traffic right now?” — if it fails, the load balancer stops routing to it but does not restart it. This is where you check real dependencies: a 200 ms query against the connection pool, as in the Hook. A pod that loses its database should go NotReady (drain traffic) without going non-live (restart won’t fix a database outage).
  • Startup answers “has the slow boot finished?” — it gates the other two during initialization so a JVM that takes 40 s to warm up isn’t liveness-killed at second 10. The Docker equivalent is --start-period.

The dangerous default is a probe that passes while the app is wedged — a TCP-port check, or a /healthz that returns 200 from a static handler that never touches the work path. A health check is only honest if it can go red for the same reason a user’s request would fail. The classic failure mode runs the other way too: a liveness probe that calls the database turns a 30-second database hiccup into every pod being restarted at once, deleting your warm caches and connection pools exactly when the system is already stressed.

Quiz

A service's database fails over for 20 seconds. Which probe configuration handles this best?

The shutdown lifecycle: SIGTERM, grace, SIGKILL, preStop

Getting the probe split right buys you nothing if the shutdown sequence drops requests on the way out. That is the second half of honest lifecycle design.

A rollout terminates old pods, and how it does that decides whether in-flight requests survive. The contract is a fixed sequence. When the orchestrator decides to stop a pod it (1) optionally runs a preStop hook, then (2) sends SIGTERM to PID 1, then (3) waits up to terminationGracePeriodSeconds (default 30 s), and finally (4) sends SIGKILL to anything still running. Your job is to fully drain within the grace window so step 4 never fires on a live request.

lifecycle:
  preStop:
    exec: { command: ["sh", "-c", "sleep 5"] }   # bridge the LB de-registration race
terminationGracePeriodSeconds: 45                # > worst-case drain (p99 + LB propagation)

The subtle bug is a race that has nothing to do with your signal handler. The moment a pod is marked Terminating, two things happen concurrently: SIGTERM is delivered, and the endpoint is removed from the Service. But endpoint removal propagates asynchronously to every kube-proxy and load balancer — for a second or two, traffic is still being routed to a pod that has already started shutting down. If your SIGTERM handler closes the listener immediately, those in-transit requests hit a closed socket and get connection-refused. The fix is the preStop hook: a short sleep (commonly 5–15 s) that delays SIGTERM, keeping the pod fully serving while the de-registration propagates, so no new traffic is dropped. Then SIGTERM arrives, your handler flips readiness off (belt and braces), drains in-flight requests, and exits 0 — all comfortably inside a grace period sized above your p99 plus that propagation delay. Set the grace too low and SIGKILL severs live connections mid-response (the client sees an RST); forget preStop and the LB race drops requests even with a perfect handler.

Quiz

Your SIGTERM handler is correct — it drains in-flight requests and exits cleanly — yet a few requests still fail at the start of every rollout. Why, and what closes the gap?

Recall before you leave
  1. 01
    Distinguish liveness, readiness and startup probes by the question each answers and the action each triggers — and give the failure mode of putting a database check in the wrong one.
  2. 02
    Walk the pod shutdown sequence and explain why a correct SIGTERM handler still drops requests without a preStop hook.
Recap

Production health is two separate disciplines that teams routinely collapse into one. The first is honest probing: a health check must be able to go red for the same reason a real request would fail, so a TCP-port check or a static 200 handler that passes while the app is wedged on a dead database is worthless — it kept dashboards green through a full outage. Split the three questions the orchestrator actually asks. Liveness asks whether the process is wedged and unrecoverable and restarts the container on failure, so keep it cheap and free of external dependencies, or a transient database blip becomes a synchronized restart storm that wipes warm caches and pools. Readiness asks whether this instance should take traffic right now and merely stops the load balancer routing to it, so this is where genuine dependency checks live — a real query against the pool. Startup gates the other two through a slow boot (Docker’s —start-period) so a 40 s JVM warm-up is not liveness-killed prematurely. The second discipline is the shutdown lifecycle: a rollout runs an optional preStop hook, sends SIGTERM to PID 1, waits up to terminationGracePeriodSeconds (default 30 s), then SIGKILLs the rest. Sizing that grace above worst-case drain (p99 plus LB de-registration propagation) keeps SIGKILL from severing a live connection and sending the client an RST. And the subtle race — endpoint removal propagates asynchronously while SIGTERM is delivered, so the LB keeps routing for a second or two — is bridged by a short preStop sleep that delays SIGTERM until de-registration lands, so a correct handler drains every in-flight and in-transit request to zero loss. Now when you see requests failing at rollout boundaries, ask two questions before adding more retries: are you routing liveness checks through a dependency that can be temporarily down, and is preStop buying the LB time to de-register before the listener closes?

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.