Step 1: read the exit code — it is a taxonomy
In `kubectl describe pod`, the Last State block has two key fields: Reason and Exit Code. The exit code is a ready-made taxonomy: 137 = killed by SIGKILL (usually OOMKilled); 143 = SIGTERM graceful exit (the app failed to finish within the grace period); 1 = the application errored out (go read the logs); 0 yet restarting = the process finished its job and exited (a one-shot task run as a long-lived service).
Classify first, then drill down — it avoids detours on the most common case, OOM, where the container log usually shows nothing abnormal.
kubectl describe pod <pod> -n <ns>
# Last State: Terminated, Reason: OOMKilled, Exit Code: 137Step 2: read the logs of the previous crash
Once the container restarts, plain `kubectl logs` shows only the current (short, often empty) instance. Add `--previous` for the real crash-site output: missing config, unreachable database, port conflicts — startup-phase errors live there.
For multi-container Pods, pass `-c <container>`. When the app logs to a file instead of stdout (common with Tomcat-style Java apps), fix the log output or collect via a sidecar, otherwise kubectl logs will never see it.
kubectl logs <pod> -n <ns> --previous
kubectl logs <pod> -n <ns> -c <container> --previousStep 3: check the probes — friendly fire is common
A failing liveness probe restarts the container, looking exactly like a real crash — describe shows the probe as the Reason (e.g. Liveness probe failed). Typical misconfigurations: liveness wired to a hard dependency (slow-starting apps judged dead before ready), initialDelaySeconds too short, wrong probe port/path.
Principle: liveness answers "is it dead", readiness answers "can it take traffic". For slow starters add a startupProbe or raise initialDelaySeconds, separating "still booting" from "actually dead".
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30 # 启动慢的应用别配太短
periodSeconds: 10Step 4: resource limits — two sources of OOMKilled
Exceeding the memory limit gets the container killed by the kernel (exit code 137). Check two places: ① the limit itself is too small — for Java, modern JVMs are cgroup-aware (JDK 8u191+ / JDK 10+), but a hand-set -Xmx above the limit still gets killed; ② an app memory leak — shows as OOM after running a while; the container memory curve distinguishes "pegged from the start" from "slow climb".
Raising the limit is a stopgap; leaks need a heap dump on the app side. A wide requests/limits gap also invites node overcommit and eviction — do not look at the container alone.
resources:
requests: { memory: "256Mi", cpu: "100m" }
limits: { memory: "512Mi" }Step 5: dependencies not ready — the ordering problem
The app connects to a database/Redis at boot, crashes on refusal, and retries — the log shows a clean connection refused. This CrashLoopBackOff is really an overly hard retry policy. Instead of relying on Kubernetes restarts, add app-side retry with backoff, or block with initContainers (a wait-for-db script) before the main container starts.
Projects ported from Docker Compose hit this most: compose has depends_on + condition; Kubernetes has no direct equivalent, so you must supply the readiness logic yourself.
initContainers:
- name: wait-for-db
image: busybox
command: ["sh", "-c", "until nc -z db-svc 5432; do sleep 2; done"]Wrap-up: a quick reference
describe for the exit code (137=OOM / 1=error / 0=clean exit yet restarting) → logs --previous for the crash scene → rule out probe kills → verify limits against the memory curve → add dependency-readiness logic. Most CrashLoopBackOffs close within these five steps; whatever remains is usually an image bug — reproduce locally with the same image.
kubectl describe pod <pod> -n <ns> # 1 退出码
kubectl logs <pod> -n <ns> --previous # 2 崩溃现场