06 · Probes & Health at Scale¶
Not run against a live cluster
Manifests and output below follow documented kubelet/PDB behavior; not executed against a live cluster in this session.
Beyond a single probe: health as a system property¶
Level 2, Module 04 covered liveness/readiness/startup probes on one Pod. At scale, the questions shift: how do you avoid an entire fleet failing its health check simultaneously (a "thundering herd" of restarts), how do you protect availability during voluntary disruptions (node drains, rollouts), and how do you make probes cheap enough to run against hundreds of replicas without becoming load themselves.
Cascading probe failures: the thundering herd problem¶
If every replica's readiness probe depends on a shared downstream (a database, a cache), a brief blip in that dependency can flip every replica unready at once — the Service loses all backends simultaneously, which is strictly worse than a single Pod being briefly unavailable.
A shallow /ready that only checks "is the HTTP server listening" avoids
this coupling; a deep /ready that pings the database on every probe
couples the entire fleet's availability to that single dependency's
health. The common resolution: keep readiness shallow (or check a
locally-cached "last known good" status updated asynchronously) and handle
downstream failures with retries/circuit breakers in application code
instead of via the probe.
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 10
failureThreshold: 3
# jitter isn't a native field -- stagger initialDelaySeconds per replica via a Helm range or similar
Jittering initialDelaySeconds (or accepting the natural staggering from
rolling-update timing) avoids every replica's probe firing in the exact
same tick, which matters more as fleet size grows and probes themselves
start to add measurable load.
PodDisruptionBudgets: protecting availability during voluntary disruption¶
Probes protect against involuntary failure (a crash). A PodDisruptionBudget (PDB) protects against voluntary disruption — a node drain, a cluster upgrade — ensuring the cluster won't evict so many replicas at once that the app effectively goes down.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-pdb
spec:
minAvailable: 2 # or maxUnavailable: 1
selector:
matchLabels: { app: web }
kubectl apply -f web-pdb.yaml
kubectl drain node-1 --ignore-daemonsets
# evicting pod default/web-abc123
# evicting pod default/web-def456
# error when evicting pod "web-ghi789": Cannot evict pod as it would violate the pod's disruption budget.
drain (and any tool using the Eviction API — cluster autoscalers, managed
node upgrades) respects the PDB automatically; it will evict Pods one at a
time, waiting for replacements to become Ready, and outright refuses an
eviction that would breach minAvailable. A PDB has zero effect on
involuntary disruption — a node crashing outright doesn't ask permission.
Probe cost at scale¶
readinessProbe:
httpGet: { path: /ready, port: 8080 }
periodSeconds: 15 # not 1s -- 200 replicas probing every second is real, avoidable load
timeoutSeconds: 2
failureThreshold: 2
At 200 replicas with periodSeconds: 1, that's 200 requests/second of pure
overhead against the app before it serves a single real user — tuning
periodSeconds upward (with a correspondingly lower failureThreshold if
faster reaction is needed) is a real, easily overlooked cost lever.
Worked example: PDB stops a drain from taking down a service¶
kubectl get deploy web
# 3/3 ready
kubectl apply -f web-pdb.yaml # minAvailable: 2
kubectl drain node-1 --ignore-daemonsets
kubectl get pods -l app=web -o wide -w
# one Pod evicted and rescheduled at a time -- PDB blocks the second eviction
# until the first replacement Pod is Ready, never dropping below 2 available
How It Actually Works¶
- A PDB is enforced by the Eviction subresource's admission check, not
by intercepting
kubectl delete.kubectl drain(and the cluster autoscaler, and managed-node-group upgrade tooling) calls thepods/evictionsubresource rather than a plain delete; the API server's eviction handling consults thedisruption-controller's continuously maintainedPodDisruptionBudget.status.disruptionsAllowedcount for any PDB selecting that Pod and rejects the eviction request outright if granting it would pushdisruptionsAllowedbelow zero — a plainkubectl delete podbypasses this entirely, which is why PDBs only ever protect eviction-based workflows, never direct deletion. disruptionsAllowedis computed the same way readiness feeds into EndpointSlices — the disruption controller counts currently-Ready Pods matching the PDB's selector and subtracts theminAvailable(or appliesmaxUnavailable) on every reconcile tick, independent of any actual drain in progress. This is why a PDB can already showdisruptionsAllowed: 0even before anyone tries to drain anything — if a replica is already unready for unrelated reasons (a bad rollout, a crash), the budget for voluntary disruption is already spent by an involuntary one.- Readiness-probe coupling failures cascade because EndpointSlice updates are eventually consistent and identical for every replica hitting the same downstream at the same moment — there's no built-in jitter or circuit-breaking at the platform level. kubelet on every node independently runs its own probe schedule against its own Pods with no cross-node coordination; if all replicas share a probe implementation hitting the same failing dependency, they will, absent deliberate staggering, converge on failing within one probe period of each other — Kubernetes provides the mechanism (readiness gates traffic) but no protection against a badly designed probe body.
kubectl drain's eviction loop is a client-side retry loop, not a server-side queued operation.drainrepeatedly attempts eviction of remaining Pods on the node, backing off and retrying onTooManyRequests/PDB-violation responses, until either all Pods are evicted or a timeout is hit — there is no server-side "drain job" object; killing thekubectl drainprocess mid-drain simply stops further evictions with the node left partially drained and no in-cluster memory that a drain was ever in progress.
Exercise¶
Deploy a 4-replica web Deployment with a minAvailable: 3 PDB, then
attempt kubectl drain on a node hosting 2 of its replicas. Observe that
the drain evicts one Pod, waits for its replacement to become Ready, then
proceeds to the second — never allowing available replicas to drop below
3. Then scale web down to 3 replicas first (so all 3 must stay available)
and confirm the same drain now fails outright with a PDB violation error
for any of the two Pods being drained.