Skip to content

RCA guidata — Pod in CrashLoop

RCA guidata — Pod in CrashLoop

Runbook operativo per la root cause analysis di un pod che restarta ripetutamente. Struttura a “5 whys”: ogni step approfondisce il precedente, e alla fine si arriva alla vera causa (config, memory, dipendenza, node).

Applicabile sia a CrashLoopBackOff che a OOMKilled che a Error puro.

Prerequisiti: accesso cluster-admin o RBAC che permetta oc get events, oc logs, oc debug node. Console Observe → Metrics accessibile.


Fase 0 — Sintomo iniziale

Un pod è stato segnalato come “non funziona”. Alla prima verifica:

Terminal window
oc get pod -n <ns> <pod>

Se vedi:

NAME READY STATUS RESTARTS AGE
myapp-xxx-yyy 0/1 CrashLoopBackOff 17 2h

Sei nel caso classico. Procedi.


Fase 1 — Chi, dove, quante volte

1.1 Su quale node gira?

Terminal window
oc get pod -n <ns> <pod> -o jsonpath='{.spec.nodeName}{"\n"}'

Salva il nome del node in una variabile: NODE=$(oc get pod -n <ns> <pod> -o jsonpath='{.spec.nodeName}')

1.2 Quanti restart?

Terminal window
oc get pod -n <ns> <pod> \
-o custom-columns=NAME:.metadata.name,\
RESTARTS:.status.containerStatuses[*].restartCount,\
STATUS:.status.containerStatuses[*].state,\
LAST_STATE:.status.containerStatuses[*].lastState

L’output .lastState mostra il motivo dell’ultimo exit. Se è terminated con reason=OOMKilled, salta direttamente alla Fase 3 — OOM.

1.3 Alternativa PromQL (utile per screenshot / audit)

Chi è, dove sta, quanti restart nelle ultime 6 ore:

sum by (namespace, pod, container, node) (
increase(kube_pod_container_status_restarts_total{
namespace="<ns>", pod=~"<pod>.*"
}[6h])
* on(pod, namespace) group_left(node) kube_pod_info
)

Fase 2 — Perché è morto (log e events)

2.1 Log del container morto (crucial: --previous)

Il container attualmente in stato waiting/CrashLoopBackOff non ha log (non sta girando). Ti serve il log del ciclo precedente:

Terminal window
oc logs -n <ns> <pod> -c <container> --previous
oc logs -n <ns> <pod> -c <container> --previous --tail=200

Cosa cerchi qui:

  • Stack trace applicativo (Java: Exception in thread, Python: Traceback, Go: panic:)
  • Errori di connessione a dipendenze (DB, cache, altri servizi)
  • OutOfMemoryError (in Java può essere sia JVM OOM sia container OOM)
  • Errori di parsing config (variabili d’ambiente mancanti, file mount errati)

2.2 Events Kubernetes recenti sul pod

Terminal window
oc get events -n <ns> --field-selector=involvedObject.name=<pod> --sort-by=.lastTimestamp

Cosa cerchi:

  • Failed to pull image → problema registry / pull-secret
  • Back-off restarting failed container → conferma CLBO
  • Readiness probe failed → probe troppo aggressive o app troppo lenta a partire
  • Liveness probe failed → probe uccide app che sta caricando lentamente
  • MountVolume.SetUp failed → problema PV / configMap / secret

2.3 Describe completo del pod

Terminal window
oc describe pod -n <ns> <pod>

L’output è lungo, ma le sezioni chiave sono:

  • Conditions — stato attuale
  • Containers → Last State — dettaglio dell’ultimo exit (reason, exit_code, started/finished)
  • Events (in fondo) — cronologia recente

Esempio di Last State: Terminated diagnostico:

Last State: Terminated
Reason: OOMKilled
Exit Code: 137 # 128 + SIGKILL(9)
Started: Mon, 06 May 2026 14:12:03 +0200
Finished: Mon, 06 May 2026 14:15:47 +0200

Exit code 137 = SIGKILL = kernel OOM (o kill esplicito). Exit code 139 = SIGSEGV = crash applicativo (segfault, tipico C/C++). Exit code 143 = SIGTERM = kill “gentile” (di solito rolling update, non un bug). Exit code 1 = errore generico applicativo.


Fase 3 — Se è OOMKilled

Se Fase 2 dice Reason: OOMKilled (o exit code 137 con contesto memory), sei sul path OOM.

3.1 Quanto memoria stava usando

# Ultimi 30 minuti di working-set memory
max_over_time(
container_memory_working_set_bytes{
namespace="<ns>", pod=~"<pod>.*", container="<container>"
}[30m]
) / 1024 / 1024

Restituisce il picco in MiB.

3.2 Quanto limit era configurato

Terminal window
oc get pod -n <ns> <pod> -o jsonpath='{.spec.containers[?(@.name=="<container>")].resources}{"\n"}'

Confronta memoria usata con limits.memory. Se sono vicini, è memory pressure interna al container.

3.3 Conferma kernel-side

Terminal window
# Ottieni il node
NODE=$(oc get pod -n <ns> <pod> -o jsonpath='{.spec.nodeName}')
# Cerca OOM nel kernel log
oc debug node/$NODE -- chroot /host journalctl --since "1 hour ago" --grep "oom.*kill|Killed process"

Nell’output cerca Memory cgroup out of memory (= OOM cgroup del container, causa il tuo limit) vs Out of memory: Killed process senza cgroup (= OOM del node intero, causa memory pressure sul node).

3.4 Se è OOM cgroup (tuo limit)

Fix: alzi il limits.memory. Calcola il nuovo valore su 7 giorni:

max_over_time(
container_memory_working_set_bytes{
namespace="<ns>", pod=~"<pod>.*", container="<container>"
}[7d:5m]
) / 1024 / 1024

Prendi il picco, aggiungi 20-30% margin. Se l’app è in Java, considera anche -Xmx = 70-75% del limit del container.

3.5 Se è OOM node (pressione globale)

Fix: liberare il node o aumentare i request degli altri pod (per fargli evacuare via scheduler). Vedi anche OOM Killed § 3.

Vedi chi sta consumando sul node:

topk(20,
sum by (namespace, pod) (
container_memory_working_set_bytes{
container!="", container!="POD"
}
* on(pod, namespace) group_left(node) kube_pod_info{node="<NODE>"}
)
) / 1024 / 1024

Fase 4 — Se non è OOM (probabilmente config / dipendenza)

Se il container muore con exit code non-137 e i log non parlano di memoria, è quasi sicuramente un problema applicativo. Le cause più frequenti:

4.1 ConfigMap / Secret mancante o cambiato

Terminal window
oc get pod -n <ns> <pod> -o json | jq '.spec.volumes[] | select(.configMap or .secret)'
oc get configmap,secret -n <ns>

Verifica che le referenze siano coerenti.

4.2 Env var mancanti (crash all’avvio)

Terminal window
oc get pod -n <ns> <pod> -o json | jq '.spec.containers[].env'

Cerca env che referenziano secretKeyRef o configMapKeyRef che potrebbero non esistere.

4.3 Dipendenza esterna non raggiungibile

Se l’app chiama un DB / API esterno all’avvio e non ha timeout robusti:

Terminal window
# Log del container: se vedi "connection refused" o "timeout" nei primi secondi
oc logs -n <ns> <pod> -c <container> --previous | head -50
# Test dalla stessa network del pod
oc debug -n <ns> <pod> -- curl -v http://<db-service>:5432

4.4 Readiness/Liveness probe troppo aggressive

Se il pod muore ripetutamente ma i log non mostrano errori applicativi, sospetta probe:

Terminal window
oc get pod -n <ns> <pod> -o yaml | grep -A5 "livenessProbe\|readinessProbe\|startupProbe"

Sintomi tipici:

  • initialDelaySeconds troppo basso → probe partono prima che l’app sia pronta
  • timeoutSeconds troppo basso → una response lenta di 3 secondi killa il pod
  • Manca startupProbe per app slow-start (Java Spring, IIS, ecc.)

4.5 Init container fallisce

Terminal window
oc get pod -n <ns> <pod> -o json | jq '.status.initContainerStatuses'

Se un init container non passa, il container principale non parte mai. Log:

Terminal window
oc logs -n <ns> <pod> -c <init-container-name>

4.6 SCC / SELinux / permessi

Se il container gira come root ma la SCC del namespace non lo permette:

Terminal window
oc logs -n <ns> <pod> -c <container> --previous | grep -i "permission denied\|operation not permitted"
oc describe pod -n <ns> <pod> | grep -i "scc\|security"

Fase 5 — Se il node stesso è il problema

A volte il pod restarta perché il node ha problemi. Verifiche:

5.1 Condizioni del node

Terminal window
oc get node $NODE
oc describe node $NODE | grep -A20 "Conditions:"

Cerca MemoryPressure=True, DiskPressure=True, PIDPressure=True, NetworkUnavailable=True.

5.2 Kubelet in errore

Terminal window
oc debug node/$NODE -- chroot /host journalctl -u kubelet --since "1 hour ago" | grep -iE "error|failed" | tail -30

5.3 CRI-O in errore (container runtime)

Terminal window
oc debug node/$NODE -- chroot /host journalctl -u crio --since "1 hour ago" | grep -iE "error|failed" | tail -30

5.4 Se solo pod su UN node hanno il problema

Sposta il pod di test su un altro node per confermare l’ipotesi:

Terminal window
# Cordon del node sospetto (blocca nuova schedulazione)
oc adm cordon $NODE
# Delete del pod: il ReplicaSet lo riprogramma su un altro node
oc delete pod -n <ns> <pod>
# Se sul nuovo node funziona, il node originale ha un problema
# Non dimenticare di uncordon dopo!
oc adm uncordon $NODE

Fase 6 — Report finale

Una volta capita la causa, documenta l’RCA. Template minimale:

# RCA — <pod> restart in loop
**Ambiente**: <cluster>, namespace `<ns>`
**Pod**: `<pod>`
**Container**: `<container>`
**Node**: `<node>`
**Durata sintomo**: <ora inizio><ora fix>
## Sintomo
- <pod> in `CrashLoopBackOff` con <N> restart
- Impact: <servizio giù / degradato / nessuno>
## Root cause
<Config errata / OOM cgroup / dipendenza X non raggiungibile / probe troppo aggressive>
## Evidenza
- Log applicativo: `<estratto rilevante>`
- Events K8s: `<estratto>`
- Metriche: <link a Grafana / query PromQL>
- Kernel (se OOM): `<estratto journalctl>`
## Fix applicato
- <es: aumentato limits.memory da 512Mi a 1Gi>
- <es: fixato ConfigMap `app-config` chiave `db.url`>
- <es: aumentato startupProbe initialDelaySeconds a 60s>
## Prevenzione
- <es: alert PrometheusRule su restart > 5/1h>
- <es: aggiunta review su change config>

Salvarlo (Confluence, wiki, cartella RCA/ del repo openshift-cheatsheet) ti costruisce nel tempo un catalogo di casi che è oro puro per la squadra e per l’audit.


Vedi anche