Skip to content

OOM Killed — Troubleshooting

OOM Killed — Troubleshooting

Query PromQL per identificare i pod che il kernel Linux ha terminato per Out Of Memory (reason=OOMKilled), correlare con memory usage vs limit, e capire su quale node è successo. In coda: comandi oc per verifica kernel-side (dmesg, journalctl).

Stack di riferimento: OpenShift Monitoring standard (openshift-monitoring). Metriche da kube-state-metrics + cadvisor.


1. Top 20 OOMKilled — chi, dove, quando

1.1 Container OOMKilled nell’ultima ora

sum by (namespace, pod, container, node) (
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}
* on(pod, namespace) group_left(node) kube_pod_info
) > 0

Cosa mostra: snapshot puntuale dei container il cui ultimo exit è stato OOMKilled. Il valore è 1 se è OOM, altrimenti la serie non esiste.

Nota: last_terminated_reason mostra solo l’ultima terminazione. Se un container è stato OOM e poi è restartato con successo, resta comunque nella lista con reason=OOMKilled fino al prossimo restart.

1.2 Top 20 OOMKilled negli ultimi 24h (per numero di eventi)

topk(20,
sum by (namespace, pod, container, node) (
increase(kube_pod_container_status_restarts_total[24h])
* on(namespace, pod, container) group_left(node)
(kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1)
* on(pod, namespace) group_left(node) kube_pod_info
)
)

Questa è la query più “operativa”: non solo chi è OOM ora, ma chi è OOM e ha restartato molte volte. I “top consumer del kernel OOM”.

1.3 Escludi i namespace di sistema

sum by (namespace, pod, container, node) (
kube_pod_container_status_last_terminated_reason{
reason="OOMKilled",
namespace!~"kube-.*|openshift-.*|default"
}
* on(pod, namespace) group_left(node) kube_pod_info
) > 0

2. Correlazione con memory usage

Un pod OOMKilled ha superato il proprio resources.limits.memory. Vale la pena vedere quanto stava usando poco prima del kill.

2.1 Pod OOMKilled + memoria usata al momento del kill

Combinazione di last_terminated_reason con working-set memory:

label_replace(
(kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1),
"pod_name", "$1", "pod", "(.+)"
)
* on(namespace, pod_name) group_right()
(sum by (namespace, pod_name, container) (
container_memory_working_set_bytes{container!="", container!="POD"}
) / 1024 / 1024)

Restituisce MiB usati nel container OOMKilled. Non sempre affidabile (dopo il kill le metriche possono degradare velocemente), ma nei primi minuti dopo l’evento è utile.

2.2 Pod con memory > 90% del limit (predittivo, PRIMA che OOM avvenga)

topk(20,
(
sum by (namespace, pod, container) (
container_memory_working_set_bytes{container!="", container!="POD"}
)
/
sum by (namespace, pod, container) (
kube_pod_container_resource_limits{resource="memory"}
)
) > 0.90
)

Cosa mostra: container che stanno usando > 90% del limits.memory. Sono i prossimi OOM se non intervieni.

Aggiungi il node:

topk(20,
(
sum by (namespace, pod, container) (
container_memory_working_set_bytes{container!="", container!="POD"}
)
/
sum by (namespace, pod, container) (
kube_pod_container_resource_limits{resource="memory"}
)
) > 0.90
) * on(pod, namespace) group_left(node) kube_pod_info

2.3 Pod SENZA memory limit configurato (a rischio)

Un pod senza limits.memory non verrà mai OOMKilled dal cgroup del container, ma può causare OOM a livello node (kill di altri pod). Query:

count by (namespace, pod, container) (
kube_pod_container_info{container!="POD"}
)
unless
count by (namespace, pod, container) (
kube_pod_container_resource_limits{resource="memory"}
)

Mostra container che non hanno un limits.memory definito.

2.4 Pod SENZA memory request

Non ha effetto sull’OOM diretto, ma il pod può finire su un node con poca memoria libera (perché lo scheduler non sa quanto serve). Query:

count by (namespace, pod, container) (
kube_pod_container_info{container!="POD"}
)
unless
count by (namespace, pod, container) (
kube_pod_container_resource_requests{resource="memory"}
)

3. OOM a livello di node

Se un node ha memory pressure, il kernel Linux invoca oom_reaper e killa il processo (container) con oom_score più alto — che non è sempre quello che ci si aspetta.

3.1 Node in memory pressure attualmente

kube_node_status_condition{condition="MemoryPressure", status="true"}

Restituisce 1 per i node con MemoryPressure=True. Se qualcuno è > 0, il node sta swappando o è al limite.

3.2 Memory allocatable vs usage per node

1 - (
node_memory_MemAvailable_bytes
/
node_memory_MemTotal_bytes
)

Restituisce % di memoria usata per ogni node (0-1). Sopra 0.85 è già zona di rischio.

Formattato in %:

100 * (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes))

3.3 Top 20 node per memory pressure

topk(20,
100 * (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes))
)

3.4 Overcommit di memoria per node

Somma dei requests.memory di tutti i pod su un node vs. memoria allocatable:

sum by (node) (
kube_pod_container_resource_requests{resource="memory"}
* on(pod, namespace) group_left(node) kube_pod_info
)
/
sum by (node) (
kube_node_status_allocatable{resource="memory"}
)

Valori > 0.95 = node in overcommit critico. > 1.0 = impossibile (scheduler dovrebbe averlo prevenuto, ma pod già running possono spingere oltre).

3.5 Pod recentemente evicted (spesso è OOM a livello node)

Se il kubelet fa preemption per memory pressure invece di aspettare il kernel OOM, il pod risulta Evicted (non OOMKilled).

sum by (namespace, pod, node) (
kube_pod_status_reason{reason="Evicted"}
* on(pod, namespace) group_left(node) kube_pod_info
) > 0

O via oc:

Terminal window
oc get pods -A --field-selector=status.phase=Failed \
-o custom-columns=NS:.metadata.namespace,POD:.metadata.name,\
NODE:.spec.nodeName,\
REASON:.status.reason,\
MESSAGE:.status.message

4. Cronologia OOM (chi è OOMKilled più spesso)

4.1 Container OOMKilled nelle ultime 24h + numero di eventi

Combinare restart che sono anche OOM è tecnicamente scomodo in PromQL puro (i counter di restart non hanno un label reason). Il proxy migliore è:

topk(20,
sum_over_time(
(kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1)[24h:1m]
)
) * on(pod, namespace) group_left(node) kube_pod_info

Cosa fa: campiona ogni minuto la condizione “sto ora in OOMKilled” nelle 24h, somma. Container che restano molto tempo in stato “ultimo exit = OOM” hanno valori alti. È un’approssimazione ma funziona.

4.2 Namespace con più OOMKilled

topk(10,
count by (namespace) (
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1
)
)

5. Verifica kernel-side (comandi oc complementari)

Le query PromQL dicono “K8s pensa che sia OOMKilled”. Per conferma vera dal kernel, serve andare sul node.

5.1 Ultimi OOM nel kernel log del node

Terminal window
# Trova il node su cui gira il pod OOMKilled
NODE=$(oc get pod -n <ns> <pod> -o jsonpath='{.spec.nodeName}')
echo "Node: $NODE"
# Debug node e cerca log OOM del kernel
oc debug node/$NODE -- chroot /host dmesg -T | grep -iE "out of memory|oom.*kill|killed process"
# Con journalctl (più affidabile se dmesg è stato ruotato)
oc debug node/$NODE -- chroot /host journalctl --since "1 hour ago" --grep "oom.*kill|Killed process"

5.2 Esempio di output kernel OOM

Un OOM kernel-side ha righe tipo:

kernel: [PID] Out of memory: Killed process 12345 (java) total-vm:5242880kB, anon-rss:4194304kB
kernel: oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=...
kernel: Memory cgroup out of memory: Killed process 12345 (java) total-vm:5242880kB

CONSTRAINT_MEMCG = OOM a livello cgroup del container (limit superato). Se vedi CONSTRAINT_NONE è OOM a livello node.

5.3 Chi era in esecuzione al momento

Terminal window
# Sul node, ricostruisce quali cgroup avevano più residuo memory al momento dell'OOM
oc debug node/$NODE -- chroot /host journalctl --since "1 hour ago" --grep "Task in /kubepods"

Il kernel logga chi era il “colpevole” con path cgroup completo. Utile quando il pod OOMKilled è morto ma vuoi capire se un vicino di casa gli ha causato pressione.


6. Debugging post-OOM

Una volta identificato il pod OOMKilled, questi sono i passi tipici:

6.1 Vedere quanto memory chiedeva

Terminal window
oc get pod -n <ns> <pod> -o json | jq '.spec.containers[] | {
name,
requests: .resources.requests,
limits: .resources.limits
}'

6.2 Vedere log del container prima del crash

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

6.3 Vedere events K8s recenti sul pod

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

6.4 Grafico memoria del pod (via port-forward Prometheus)

Se hai bisogno di vedere la curva di memoria pre-OOM:

Terminal window
# Port-forward su Prometheus
oc port-forward -n openshift-monitoring pod/prometheus-k8s-0 9090
# Nel browser: http://localhost:9090
# Query:
# container_memory_working_set_bytes{namespace="<ns>", pod=~"<pod>.*", container="<container>"}

Vedi se la curva è cresciuta gradualmente (memory leak) o saltata (spike, es. import di file grande).


7. Come dimensionare correttamente il limit

Una volta capito che il pod è sotto-dimensionato:

  • Guarda container_memory_working_set_bytes di picco negli ultimi 7 giorni per capire il vero fabbisogno
  • Aggiungi 20-30% di margine come limits.memory
  • Metti requests.memory uguale a limits.memory per garantire QoS Guaranteed (i pod Guaranteed sono gli ultimi a essere killati dal kubelet in memory pressure)

Query per picco 7 giorni:

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

8. Note operative

  • container!="" e container!="POD" — sempre nei filtri: escludono la metrica-fantasma della pod-sandbox che gonfia i totali.
  • Working set vs RSS vs Usage:
    • container_memory_working_set_bytes — memoria “viva” (RSS + cache attivamente usata). È questa che il kernel usa per l’OOM.
    • container_memory_rss — solo RSS puro. Meno preciso, sottostima.
    • container_memory_usage_bytes — include page cache “vecchia” che il kernel può liberare. Sovrastima.
    • Regola pratica: usa sempre working_set_bytes per allineamento con l’OOM killer.
  • kube_pod_info — è la metrica “ponte” che porta il label node. Il join * on(pod, namespace) group_left(node) kube_pod_info è il pattern standard.
  • Metriche last_terminated_reason — restano visibili per ~15 minuti dopo la terminazione. Poi Prometheus le fa scadere.

Vedi anche