Kubernetes Service Lifecycle
Purpose
Make a Java service correct at the two moments the orchestrator controls: when it is
declared ready, and when it is told to stop. Deploy-time 502s can come from lifecycle,
routing, resource pressure or application failures. Correlate request failures with pod,
probe, endpoint and shutdown events before attributing the cause.
The failure this prevents is the probe that answers the wrong question. A liveness probe
that checks a downstream dependency converts a partial degradation into a total outage:
the database wobbles, every replica fails liveness, the kubelet restarts all of them at
once, and now nothing is serving even after the database recovers.
Compatibility and evidence
Inspect the deployed JDK, resolved Boot/Framework/Kafka versions, image entrypoint and
signal forwarding, cluster version/feature gates, and effective Deployment/Service settings.
The references use partial Java 17-compatible sketches; virtual-thread APIs require Java 21.
Do not upgrade the project to apply them. Missing runtime evidence permits a conditional
configuration finding, not a confirmed incident diagnosis.
Workflow
- Assign each probe its own question. Liveness = "restart me, I am unrecoverable in
process". Readiness = "send me traffic now". Startup = "I am still booting, do not judge
me yet". Distinct semantics need not mean three endpoints: startup may reuse liveness
with a different budget. Verify what each check actually observes.
- Strip dependencies out of liveness. Liveness must depend on nothing outside the
process. If restarting the process cannot fix the condition, it does not belong in
liveness.
- Do bounded probe arithmetic. Detection includes initial delay, probe scheduling,
execution/timeout and consecutive thresholds; readiness may run more often while unready.
Treat
period × threshold as an approximation, write best/worst expectations, and test
under throttling and pauses. See references/probe-and-shutdown-configuration.md.
- Replace guessed startup delays with a startup probe.
startupProbe (GA since
Kubernetes 1.20) suspends liveness and readiness until it first succeeds, so a slow boot
gets a long budget without making crash detection slow forever.
- Budget the shutdown as a sum.
terminationGracePeriodSeconds must exceed preStop
plus the application's own drain, with margin. It is one countdown, not one per stage;
overrun means SIGKILL mid-request.
- Enumerate the in-flight work that is not an HTTP request — Kafka consumers,
@Scheduled jobs, executors, queue leases — and give each an explicit stop. See
references/draining-non-http-work.md. Then check the disruption path: a
minAvailable: 1 budget with one healthy replica blocks compliant eviction; allowing
eviction instead can create an availability gap until a replacement is ready.
Probe decision block
Use a liveness probe when:
- the process has a reachable state that only a restart clears — a deadlock, an
exhausted internal thread pool, a wedged event loop — and you can detect it in process.
Avoid a liveness probe when:
- the check touches a database, a cache, a broker or another service. A shared dependency
makes every replica fail simultaneously, which is a correlated failure you built.
- you cannot name the in-process condition it detects. Then it has no signal, only risk;
omitting liveness entirely is a legitimate configuration.
Use a readiness probe when:
- the pod can be temporarily unable to serve while still being worth keeping — warming a
cache, reconnecting, or shedding under local overload.
Avoid putting a hard dependency in readiness when:
- the whole fleet shares it. All replicas leave the endpoint list at once and the Service
has no backends, which is worse than serving degraded responses.
Prefer a startup probe instead when:
- boot time varies with data volume, cluster load or CPU throttling, i.e. whenever you
would otherwise have guessed initialDelaySeconds.
Rules
- Readiness failure makes the pod unready and excludes it from normal ready-endpoint routing;
it does not itself restart the container. Check
publishNotReadyAddresses, custom consumers
and existing connections before assuming traffic stops. Liveness failure at its threshold
triggers container restart handling. Choosing the wrong probe turns a routing
decision into a restart storm.
- The probe endpoint must do no business work and have no side effect. It runs on every
pod every
periodSeconds forever: a query inside it is permanent background load, and a
write inside it is a bug the kubelet triggers on a schedule.
timeoutSeconds is part of the failure-detection budget. Derive it from a lightweight
local check's measured tail plus jitter, then decide how many consecutive misses justify
action; "greater than worst case" is unusable when the worst case is unbounded.
successThreshold must be 1 for liveness and startup probes.
- Local termination and data-plane convergence are concurrent. Terminating EndpointSlice
endpoints normally become not ready (check
publishNotReadyAddresses), but proxies,
ingresses, clients and persistent connections
converge on their own timelines. A measured preStop sleep can bridge legacy data planes;
explicit readiness refusal, connection draining and load-balancer behavior are preferable
when supported. Sleeping is a workaround, not a universal protocol.
- The native
sleep lifecycle handler is version-dependent. On clusters without it,
preStop.exec needs a real binary in the image; distroless/scratch images often lack one.
A failed hook is observable through pod events (FailedPreStopHook) but termination
continues, so alerting must not rely on application logs.
preStop runs inside terminationGracePeriodSeconds, not before it. A 30 s grace
period with a 20 s preStop leaves the application 10 s, then SIGKILL.
- Spring Boot enables graceful web shutdown by default from 3.4; earlier supported lines
require
server.shutdown=graceful.
Pin the service's Boot version and verify effective behavior; the window is governed by
spring.lifecycle.timeout-per-shutdown-phase, which is not a total shutdown deadline.
- A container killed after grace expiry and one killed for memory can both surface as 137.
Correlate terminated reason/signal, events, cgroup counters and timestamps;
OOMKilled is
strong orchestrator evidence, not the only possible record. Heap sizing belongs to
container-awareness.
- CPU quota often affects JVM startup because class loading, verification and compilation
create bursts, but "hardest" is workload-dependent. Measure cold-start distribution in
the same quota and node conditions used in production.
- A rolling update runs two versions concurrently by design. The API contract consequence is
rpc-and-api-contracts; the data consequence is yours — a schema change must be readable
by both versions at once.
- A PodDisruptionBudget constrains only disruptions routed through the Eviction API. It does
not prevent a node crash or direct delete, and workload controllers are not constrained by
it during rollout.
minAvailable: 1 with one healthy replica blocks compliant eviction;
operators can still bypass it or time out, so call it unavailable by policy, not immortal.
- Never claim a rolling update is zero-downtime because the manifest has a readiness probe.
Validate the stated SLO with an open-loop client through repeated deploys: record offered
and completed requests, timeouts, resets, unexpected status codes and latency. Zero errors
in a finite run is evidence for those conditions, not a universal guarantee.
Output
Return the observed failure timeline or configuration risk, the smallest justified change,
the total shutdown budget including sequential phases, and a validation with explicit pass
criteria. Separate executed checks from rollout or fault tests still needed.
References
- Probe and shutdown configuration — a
correct three-probe manifest fragment with the timing arithmetic derived, the
preStop/grace-period/drain budget as one sum, and the Spring Boot properties and Actuator
health groups behind it. Read when writing or reviewing a Deployment, or when a probe
setting is being changed.
- Draining work that is not an HTTP request —
Kafka consumers,
@Scheduled, executor shutdown, Spring's stop ordering, and a concrete
test that proves a shutdown actually drains. Read when the service consumes a queue, runs
scheduled work, or owns its own threads.
1---2name: kubernetes-service-lifecycle3description: A Java service at the edges of its life under Kubernetes: liveness, readiness and startup probes as three different questions, probe timing arithmetic, graceful shutdown as a sequence where endpoint removal races SIGTERM, terminationGracePeriodSeconds as a budget, draining non-HTTP work such as Kafka consumers and scheduled jobs, PodDisruptionBudgets, and limits as availability decisions. Use when 502s appear only during a rolling update, when a liveness probe checks a database and a blip restarts every healthy pod, when initialDelaySeconds was guessed instead of a startupProbe, when a pod exits 137 or loops in CrashLoopBackOff, when a node drain hangs, or when in-flight Kafka or scheduled work is lost on redeploy. Does not cover what the JVM detects in a cgroup (container-awareness), host kernel behaviour (linux-for-jvm), faster startup (startup-cds-crac-leyden), replica disposability (stateless-service-design), routing (load-balancing-and-routing), or API compatibility (rpc-and-api-contracts).4---56# Kubernetes Service Lifecycle78## Purpose910Make a Java service correct at the two moments the orchestrator controls: when it is11declared ready, and when it is told to stop. Deploy-time 502s can come from lifecycle,12routing, resource pressure or application failures. Correlate request failures with pod,13probe, endpoint and shutdown events before attributing the cause.1415The failure this prevents is the probe that answers the wrong question. A liveness probe16that checks a downstream dependency converts a partial degradation into a total outage:17the database wobbles, every replica fails liveness, the kubelet restarts all of them at18once, and now nothing is serving even after the database recovers.1920## Compatibility and evidence2122Inspect the deployed JDK, resolved Boot/Framework/Kafka versions, image entrypoint and23signal forwarding, cluster version/feature gates, and effective Deployment/Service settings.24The references use partial Java 17-compatible sketches; virtual-thread APIs require Java 21.25Do not upgrade the project to apply them. Missing runtime evidence permits a conditional26configuration finding, not a confirmed incident diagnosis.2728## Workflow29301. **Assign each probe its own question.** Liveness = "restart me, I am unrecoverable in31 process". Readiness = "send me traffic now". Startup = "I am still booting, do not judge32 me yet". Distinct semantics need not mean three endpoints: startup may reuse liveness33 with a different budget. Verify what each check actually observes.342. **Strip dependencies out of liveness.** Liveness must depend on nothing outside the35 process. If restarting the process cannot fix the condition, it does not belong in36 liveness.373. **Do bounded probe arithmetic.** Detection includes initial delay, probe scheduling,38 execution/timeout and consecutive thresholds; readiness may run more often while unready.39 Treat `period × threshold` as an approximation, write best/worst expectations, and test40 under throttling and pauses. See `references/probe-and-shutdown-configuration.md`.414. **Replace guessed startup delays with a startup probe.** `startupProbe` (GA since42 Kubernetes 1.20) suspends liveness and readiness until it first succeeds, so a slow boot43 gets a long budget without making crash detection slow forever.445. **Budget the shutdown as a sum.** `terminationGracePeriodSeconds` must exceed `preStop`45 plus the application's own drain, with margin. It is one countdown, not one per stage;46 overrun means SIGKILL mid-request.476. **Enumerate the in-flight work that is not an HTTP request** — Kafka consumers,48 `@Scheduled` jobs, executors, queue leases — and give each an explicit stop. See49 `references/draining-non-http-work.md`. Then check the disruption path: a50 `minAvailable: 1` budget with one healthy replica blocks compliant eviction; allowing51 eviction instead can create an availability gap until a replacement is ready.5253## Probe decision block5455```text56Use a liveness probe when:57- the process has a reachable state that only a restart clears — a deadlock, an58 exhausted internal thread pool, a wedged event loop — and you can detect it in process.59Avoid a liveness probe when:60- the check touches a database, a cache, a broker or another service. A shared dependency61 makes every replica fail simultaneously, which is a correlated failure you built.62- you cannot name the in-process condition it detects. Then it has no signal, only risk;63 omitting liveness entirely is a legitimate configuration.64Use a readiness probe when:65- the pod can be temporarily unable to serve while still being worth keeping — warming a66 cache, reconnecting, or shedding under local overload.67Avoid putting a hard dependency in readiness when:68- the whole fleet shares it. All replicas leave the endpoint list at once and the Service69 has no backends, which is worse than serving degraded responses.70Prefer a startup probe instead when:71- boot time varies with data volume, cluster load or CPU throttling, i.e. whenever you72 would otherwise have guessed initialDelaySeconds.73```7475## Rules7677- Readiness failure makes the pod unready and excludes it from normal ready-endpoint routing;78 it does not itself restart the container. Check `publishNotReadyAddresses`, custom consumers79 and existing connections before assuming traffic stops. Liveness failure at its threshold80 triggers container restart handling. Choosing the wrong probe turns a routing81 decision into a restart storm.82- The probe endpoint must do no business work and have **no side effect**. It runs on every83 pod every `periodSeconds` forever: a query inside it is permanent background load, and a84 write inside it is a bug the kubelet triggers on a schedule.85- `timeoutSeconds` is part of the failure-detection budget. Derive it from a lightweight86 local check's measured tail plus jitter, then decide how many consecutive misses justify87 action; "greater than worst case" is unusable when the worst case is unbounded.88 `successThreshold` must be 1 for liveness and startup probes.89- **Local termination and data-plane convergence are concurrent.** Terminating EndpointSlice90 endpoints normally become not ready (check `publishNotReadyAddresses`), but proxies,91 ingresses, clients and persistent connections92 converge on their own timelines. A measured `preStop` sleep can bridge legacy data planes;93 explicit readiness refusal, connection draining and load-balancer behavior are preferable94 when supported. Sleeping is a workaround, not a universal protocol.95- The native `sleep` lifecycle handler is version-dependent. On clusters without it,96 `preStop.exec` needs a real binary in the image; distroless/scratch images often lack one.97 A failed hook is observable through pod events (`FailedPreStopHook`) but termination98 continues, so alerting must not rely on application logs.99- `preStop` runs **inside** `terminationGracePeriodSeconds`, not before it. A 30 s grace100 period with a 20 s preStop leaves the application 10 s, then SIGKILL.101- Spring Boot enables graceful web shutdown by default from 3.4; earlier supported lines102 require `server.shutdown=graceful`.103 Pin the service's Boot version and verify effective behavior; the window is governed by104 `spring.lifecycle.timeout-per-shutdown-phase`, which is not a total shutdown deadline.105- A container killed after grace expiry and one killed for memory can both surface as 137.106 Correlate terminated reason/signal, events, cgroup counters and timestamps; `OOMKilled` is107 strong orchestrator evidence, not the only possible record. Heap sizing belongs to108 `container-awareness`.109- CPU quota often affects JVM startup because class loading, verification and compilation110 create bursts, but "hardest" is workload-dependent. Measure cold-start distribution in111 the same quota and node conditions used in production.112- A rolling update runs two versions concurrently by design. The API contract consequence is113 `rpc-and-api-contracts`; the _data_ consequence is yours — a schema change must be readable114 by both versions at once.115- A PodDisruptionBudget constrains only disruptions routed through the Eviction API. It does116 not prevent a node crash or direct delete, and workload controllers are not constrained by117 it during rollout. `minAvailable: 1` with one healthy replica blocks compliant eviction;118 operators can still bypass it or time out, so call it unavailable by policy, not immortal.119- Never claim a rolling update is zero-downtime because the manifest has a readiness probe.120 Validate the stated SLO with an open-loop client through repeated deploys: record offered121 and completed requests, timeouts, resets, unexpected status codes and latency. Zero errors122 in a finite run is evidence for those conditions, not a universal guarantee.123124## Output125126Return the observed failure timeline or configuration risk, the smallest justified change,127the total shutdown budget including sequential phases, and a validation with explicit pass128criteria. Separate executed checks from rollout or fault tests still needed.129130## References131132- [Probe and shutdown configuration](references/probe-and-shutdown-configuration.md) — a133 correct three-probe manifest fragment with the timing arithmetic derived, the134 preStop/grace-period/drain budget as one sum, and the Spring Boot properties and Actuator135 health groups behind it. Read when writing or reviewing a Deployment, or when a probe136 setting is being changed.137- [Draining work that is not an HTTP request](references/draining-non-http-work.md) —138 Kafka consumers, `@Scheduled`, executor shutdown, Spring's stop ordering, and a concrete139 test that proves a shutdown actually drains. Read when the service consumes a queue, runs140 scheduled work, or owns its own threads.