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 dakube-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) > 0Cosa 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) > 02. 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_info2.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"})unlesscount 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"})unlesscount 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) > 0O via oc:
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.message4. 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_infoCosa 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
# Trova il node su cui gira il pod OOMKilledNODE=$(oc get pod -n <ns> <pod> -o jsonpath='{.spec.nodeName}')echo "Node: $NODE"
# Debug node e cerca log OOM del kerneloc 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:4194304kBkernel: oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=...kernel: Memory cgroup out of memory: Killed process 12345 (java) total-vm:5242880kBCONSTRAINT_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
# Sul node, ricostruisce quali cgroup avevano più residuo memory al momento dell'OOMoc 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
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
oc logs -n <ns> <pod> -c <container> --previousoc logs -n <ns> <pod> -c <container> --previous --tail=2006.3 Vedere events K8s recenti sul pod
oc get events -n <ns> --field-selector=involvedObject.name=<pod> --sort-by=.lastTimestamp6.4 Grafico memoria del pod (via port-forward Prometheus)
Se hai bisogno di vedere la curva di memoria pre-OOM:
# Port-forward su Prometheusoc 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_bytesdi picco negli ultimi 7 giorni per capire il vero fabbisogno - Aggiungi 20-30% di margine come
limits.memory - Metti
requests.memoryuguale alimits.memoryper garantire QoSGuaranteed(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 / 10248. Note operative
container!=""econtainer!="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_bytesper allineamento con l’OOM killer.
kube_pod_info— è la metrica “ponte” che porta il labelnode. 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
- Restart & Pod Lifecycle — per una view completa non solo di OOM
- RCA guidata — Pod in CrashLoop — flusso di analisi step-by-step
- Consumi risorse per namespace — CPU/memoria a livello namespace