Skip to content

Pod / Container

Tutto ciò che si può misurare su un pod. È la pagina di riferimento per qualunque segnalazione circoscritta a un singolo workload.

Triagele prime cinque query
CPU · Throttlingconsumo e saturazione
Memoria · OOMconsumo, leak, kill
Restart e lifecycleCrashLoop, waiting, terminated
Probeliveness, readiness, startup
Configurazione e QoSlimit, request, classi
Rete · Filesystemtraffico e I/O
RCA guidatail flusso completo
Comandi oc · Alert

Triage

Cinque query per capire subito se il problema è di questo pod o dell’ambiente attorno.

T1. Stato di tutti i container non sani

# elenco: ogni serie restituita soddisfa la condizione
sum by (namespace, pod, container, reason) (
kube_pod_container_status_waiting_reason{
namespace!~"kube-.*|openshift-.*|default"
} == 1
)
* on(namespace, pod) group_left(node) max by (namespace,pod,node) (kube_pod_info)

T2. Restart nell’ultima ora

# conteggio nell'ultima ora
topk(20,
sum by (namespace, pod, container) (
increase(kube_pod_container_status_restarts_total{
namespace!~"kube-.*|openshift-.*|default"
}[1h])
)
) > 0

T3. OOM kill nell’ultima ora

# conteggio nell'ultima ora
sum by (namespace, pod, container) (
increase(container_oom_events_total{
namespace!~"kube-.*|openshift-.*|default"
}[1h])
) > 0

T4. Container vicini al memory limit

# percentuale 0-100
topk(20,
100 * (
sum by (namespace, pod, container) (
container_memory_working_set_bytes{
namespace!~"kube-.*|openshift-.*|default",
container!="", container!="POD"
}
)
/
sum by (namespace, pod, container) (
kube_pod_container_resource_limits{resource="memory"}
)
) > 85
)

T5. Container throttled

# percentuale 0-100 dei periodi CFS in cui il container è stato bloccato
topk(20,
100 * (
sum by (namespace, pod, container) (
rate(container_cpu_cfs_throttled_periods_total{
namespace!~"kube-.*|openshift-.*|default",
container!="", container!="POD"
}[15m])
)
/
sum by (namespace, pod, container) (
rate(container_cpu_cfs_periods_total{
namespace!~"kube-.*|openshift-.*|default",
container!="", container!="POD"
}[15m])
)
) > 5
)

Se i risultati sono concentrati su uno o due nodi, il problema non è dei pod: vai a Nodo. Query di verifica:

# conteggio nell'ultima ora
sum by (node) (
increase(kube_pod_container_status_restarts_total[1h])
* on(namespace, pod) group_left(node) max by (namespace,pod,node) (kube_pod_info)
)

CPU

Consumo per pod

# core CPU
topk(20,
sum by (namespace, pod) (
rate(container_cpu_usage_seconds_total{
namespace!~"kube-.*|openshift-.*|default",
container!="", container!="POD"
}[5m])
)
)
* on(namespace, pod) group_left(node) max by (namespace,pod,node) (kube_pod_info)

In core. Per millicore, * 1000.

Consumo per singolo container

# core CPU · sostituisci $ns, $pod
sum by (namespace, pod, container) (
rate(container_cpu_usage_seconds_total{
namespace="$ns", pod=~"$pod.*",
container!="", container!="POD"
}[5m])
)

Separa l’applicazione dai sidecar. Un istio-proxy che consuma più del container applicativo indica quasi sempre troppi endpoint ricevuti da istiod, non traffico alto: si limita con una risorsa Sidecar che restringe la visibilità.

Solo il container applicativo

# core CPU · sostituisci $ns, $pod
sum by (namespace, pod) (
rate(container_cpu_usage_seconds_total{
namespace="$ns", pod=~"$pod.*",
container!="", container!="POD", container!="istio-proxy"
}[5m])
)

Saturazione — usage vs limit

# percentuale 0-100
topk(20,
100 * (
sum by (namespace, pod, container) (
rate(container_cpu_usage_seconds_total{
namespace!~"kube-.*|openshift-.*|default",
container!="", container!="POD"
}[5m])
)
/
sum by (namespace, pod, container) (
kube_pod_container_resource_limits{resource="cpu"}
)
) > 80
)

Usa kube_pod_container_resource_limits, non container_spec_cpu_quota / container_spec_cpu_period. Il quota vale -1 quando il limit non è impostato, e la divisione produce numeri privi di senso invece di una serie vuota.

Usage vs request

# percentuale 0-100 · sostituisci $ns, $pod
100 * (
sum by (namespace, pod, container) (
rate(container_cpu_usage_seconds_total{namespace="$ns", pod=~"$pod.*", container!="", container!="POD"}[5m])
)
/
sum by (namespace, pod, container) (
kube_pod_container_resource_requests{resource="cpu", namespace="$ns", pod=~"$pod.*"}
)
)

Sopra 1 il pod sta usando più di quanto ha prenotato: legittimo in QoS Burstable, ma se succede sempre le request sono sbagliate e lo scheduler sta prendendo decisioni su numeri finti.

Picco su 7 giorni

# core CPU · sostituisci $ns, $pod
max_over_time(
sum by (namespace, pod, container) (
rate(container_cpu_usage_seconds_total{
namespace="$ns", pod=~"$pod.*", container!="", container!="POD"
}[5m])
)[7d:5m]
)

Il numero per dimensionare limits.cpu. Regola pratica: picco 7d × 1.3.

Media su 7 giorni

# core CPU · sostituisci $ns, $pod
avg_over_time(
sum by (namespace, pod, container) (
rate(container_cpu_usage_seconds_total{
namespace="$ns", pod=~"$pod.*", container!="", container!="POD"
}[5m])
)[7d:5m]
)

Il numero per dimensionare requests.cpu.

Fattore di burst

# core CPU · sostituisci $ns, $pod
max_over_time(sum by (pod) (rate(container_cpu_usage_seconds_total{namespace="$ns", pod=~"$pod.*", container!="", container!="POD"}[5m]))[7d:5m])
/
avg_over_time(sum by (pod) (rate(container_cpu_usage_seconds_total{namespace="$ns", pod=~"$pod.*", container!="", container!="POD"}[5m]))[7d:5m])

< 2 = carico piatto, si può mettere requests = limits (QoS Guaranteed). > 5 = carico a burst, servono limit generosi e request basse.

Sbilanciamento tra repliche

# core CPU · sostituisci $ns, $workload
sum by (pod) (
rate(container_cpu_usage_seconds_total{
namespace="$ns", pod=~"$workload-.*",
container!="", container!="POD"
}[5m])
)

Repliche identiche devono avere consumo simile. Una a 3× le altre indica routing sbilanciato, sessioni sticky, o uno stato che le altre non hanno.

Load average dentro il container

# numero medio di task runnable nel container · sostituisci $ns, $pod
container_cpu_load_average_10s{namespace="$ns", pod=~"$pod.*", container!="", container!="POD"} / 1000

Numero medio di task runnable. Confrontato con limits.cpu dice se ci sono più thread pronti di quanti core il container possa usare — la condizione che genera throttling anche con usage medio basso.


Throttling CFS

Il segnale più affidabile che il limits.cpu è troppo basso. Non è una previsione: è successo.

Frazione di periodi throttled

# percentuale 0-100 dei periodi CFS in cui il container è stato bloccato
topk(20,
100 * (
sum by (namespace, pod, container) (
rate(container_cpu_cfs_throttled_periods_total{
namespace!~"kube-.*|openshift-.*|default",
container!="", container!="POD"
}[1h])
)
/
sum by (namespace, pod, container) (
rate(container_cpu_cfs_periods_total{
namespace!~"kube-.*|openshift-.*|default",
container!="", container!="POD"
}[1h])
)
) > 5
)
* on(namespace, pod) group_left(node) max by (namespace,pod,node) (kube_pod_info)

Throttling di un container nel tempo

# percentuale 0-100 dei periodi CFS in cui il container è stato bloccato · sostituisci $container, $ns, $pod
100 * (
rate(container_cpu_cfs_throttled_periods_total{
namespace="$ns", pod=~"$pod.*", container="$container"
}[5m])
/
rate(container_cpu_cfs_periods_total{
namespace="$ns", pod=~"$pod.*", container="$container"
}[5m])
)

Da graficare insieme alla latenza applicativa. Se le due curve si sovrappongono, hai la prova da allegare alla richiesta di alzare il limit.

Secondi di throttling accumulati

# secondi per secondo (frazione di tempo) · sostituisci $container, $ns, $pod
rate(container_cpu_cfs_throttled_seconds_total{
namespace="$ns", pod=~"$pod.*", container="$container"
}[5m])

Utile solo come conferma: somma il tempo di tutti i thread, quindi può superare 1 e non ha un massimo interpretabile. La frazione di periodi è la metrica giusta.

Throttling con usage basso — il caso contro-intuitivo

# percentuale 0-100
100 * (
sum by (namespace, pod, container) (
rate(container_cpu_cfs_throttled_periods_total{container!="", container!="POD"}[15m])
)
/
sum by (namespace, pod, container) (
rate(container_cpu_cfs_periods_total{container!="", container!="POD"}[15m])
)
) > 10
and
100 * (
sum by (namespace, pod, container) (
rate(container_cpu_usage_seconds_total{container!="", container!="POD"}[15m])
)
/
sum by (namespace, pod, container) (
kube_pod_container_resource_limits{resource="cpu"}
)
) < 50

Container throttled oltre il 10% pur usando meno del 50% del limit. Sembra impossibile, è comune: l’applicazione fa burst di pochi millisecondi che saturano la quota nel singolo periodo CFS da 100 ms, mentre la media sui 15 minuti resta bassa.

Firma tipica di: JVM in fase di JIT, thread paralleli del garbage collector, thread pool che si sveglia in blocco, applicazione con event loop.

Alzare il limit non basta se il problema è la granularità. Su applicazioni Java, allineare i thread GC al limit del container risolve più che raddoppiare la CPU — vedi JVM.


Memoria

Quale metrica

MetricaIncludeQuando
container_memory_working_set_bytesRSS + page cache attivadefault: è quella che il kernel confronta col limit
container_memory_rsssolo RSS anonimaconfronto con la heap JVM
container_memory_cachesolo page cachecapire se il working set alto è cache di file
container_memory_usage_bytestutto, cache vecchia inclusaquasi mai: sovrastima
container_memory_swapswapsu OCP normalmente zero, swap disabilitato

Consumo per pod

# MiB
topk(20,
sum by (namespace, pod) (
container_memory_working_set_bytes{
namespace!~"kube-.*|openshift-.*|default",
container!="", container!="POD"
}
)
* on(namespace, pod) group_left(node) max by (namespace,pod,node) (kube_pod_info)
) / 1024^2

In MiB. / 1024^3 per GiB — base 2, coerente con i Gi dei manifest. / 1e9 darebbe GB decimali e non combacerebbe.

Consumo per singolo container

# MiB · sostituisci $ns, $pod
sum by (namespace, pod, container) (
container_memory_working_set_bytes{
namespace="$ns", pod=~"$pod.*",
container!="", container!="POD"
}
) / 1024^2

Un sidecar istio-proxy sta tipicamente tra 40 e 120 MiB. Se ne consuma 500, è il numero di endpoint, non il traffico.

Headroom rispetto al limit — la query predittiva

# percentuale 0-100
topk(20,
100 * (
sum by (namespace, pod, container) (
container_memory_working_set_bytes{
namespace!~"kube-.*|openshift-.*|default",
container!="", container!="POD"
}
)
/
sum by (namespace, pod, container) (
kube_pod_container_resource_limits{resource="memory"}
)
) > 85
)
* on(namespace, pod) group_left(node) max by (namespace,pod,node) (kube_pod_info)
ValoreLettura
< 70%sano, c’è margine per i picchi
70 – 85%monitorare
85 – 95%intervenire: un picco di traffico produce OOM
> 95%OOM imminente

È la query più utile della pagina. Girata in schedulato una volta al giorno evita la maggior parte degli OOM in produzione.

Scomposizione: working set, RSS, cache

# MiB · sostituisci $ns, $pod
sum by (container) (container_memory_working_set_bytes{namespace="$ns", pod=~"$pod.*", container!="", container!="POD"}) / 1024^2
# MiB · sostituisci $ns, $pod
sum by (container) (container_memory_rss{namespace="$ns", pod=~"$pod.*", container!="", container!="POD"}) / 1024^2
# MiB · sostituisci $ns, $pod
sum by (container) (container_memory_cache{namespace="$ns", pod=~"$pod.*", container!="", container!="POD"}) / 1024^2

Lette insieme:

  • working set ≈ RSS, cache bassa → memoria vera del processo. Un limit stretto causa OOM reali.
  • working set >> RSS, cache alta → il container legge o scrive molti file. La cache sarebbe liberabile, ma cAdvisor la conta nel working set: rischi un OOM per colpa della cache. Tipico di pod che scrivono log voluminosi su emptyDir.
  • RSS >> heap JVM → memoria non-heap. Vedi JVM.

Picco su 7 giorni

# MiB · sostituisci $ns, $pod
max_over_time(
sum by (namespace, pod, container) (
container_memory_working_set_bytes{
namespace="$ns", pod=~"$pod.*",
container!="", container!="POD"
}
)[7d:5m]
) / 1024^2

limits.memory = picco 7d × 1.25, arrotondato a un valore leggibile.

La divisione va fuori dal max_over_time: dentro, l’aggregazione lavorerebbe su valori già divisi.

Tasso di crescita — leak detection

# MiB all'ora · sostituisci $ns, $pod
deriv(
sum by (namespace, pod, container) (
container_memory_working_set_bytes{
namespace="$ns", pod=~"$pod.*",
container!="", container!="POD"
}
)[6h:5m]
) * 3600 / 1024^2

MiB di crescita per ora, come regressione lineare sulle ultime 6 ore.

AndamentoDiagnosi
~0, oscilla intorno allo zeroconsumo stabile: se sei vicino al limit è sotto-dimensionamento
costantemente positivo su più finestrecrescita monotona: candidato leak
positivo solo in certe fasce orariecarico, non leak

La verifica decisiva: se resta positiva anche quando il traffico cala di notte, è un leak.

Top candidati leak del cluster

# MiB all'ora
topk(20,
deriv(
sum by (namespace, pod, container) (
container_memory_working_set_bytes{
namespace!~"kube-.*|openshift-.*|default",
container!="", container!="POD"
}
)[6h:5m]
) * 3600 / 1024^2
)

Query costosa: subquery su tutti i container del cluster. Fuori dagli orari di picco, mai in dashboard.

Il pattern a dente di sega

Si chiama così perché il grafico della memoria disegna una serie di denti: salita lineare, caduta verticale, risalita con la stessa pendenza.

memoria
│ ╱| ╱| ╱|
│ ╱ | ╱ | ╱ |
│ ╱ |╱ |╱ |
└──────────────────────► tempo
restart restart

La salita è l’accumulo, la caduta verticale è il restart che azzera il processo. È la firma visiva di un memory leak: se fosse sotto-dimensionamento la curva oscillerebbe attorno a un plateau invece di salire sempre allo stesso ritmo.

# MiB · sostituisci $container, $ns, $pod
sum by (pod) (
container_memory_working_set_bytes{namespace="$ns", pod=~"$pod.*", container="$container"}
) / 1024^2
# contatore cumulativo di restart, azzerato alla ricreazione del pod · sostituisci $container, $ns, $pod
kube_pod_container_status_restarts_total{namespace="$ns", pod=~"$pod.*", container="$container"}

Sovrapposte su 48 ore. Memoria che sale linearmente fino al limit, restart, ripartenza da zero con la stessa pendenza: leak confermato. L’intervallo tra i restart è proporzionale al limit — raddoppiarlo raddoppia solo l’attesa.

Correlare crescita e traffico

# byte al secondo · sostituisci $ns, $pod
sum by (pod) (rate(container_network_receive_bytes_total{namespace="$ns", pod=~"$pod.*"}[5m]))

Memoria e traffico che salgono e scendono insieme = carico, dimensiona di più. Memoria che sale e non scende quando il traffico cala = leak.


OOM kill

Conteggio esatto

# conteggio nelle ultime 24 ore
topk(20,
sum by (namespace, pod, container) (
increase(container_oom_events_total{
namespace!~"kube-.*|openshift-.*|default"
}[24h])
)
) > 0

container_oom_events_total è un counter: increase() dà il numero esatto. È l’unica metrica con cui si può fare un report mensile affidabile.

Con il nodo:

# conteggio nelle ultime 24 ore
topk(20,
sum by (namespace, pod, container) (
increase(container_oom_events_total{namespace!~"kube-.*|openshift-.*|default"}[24h])
) > 0
)
* on(namespace, pod) group_left(node) max by (namespace,pod,node) (kube_pod_info)

Se la metrica non esiste

# numero di serie
count(container_oom_events_total)

Serie vuota su alcune versioni di cAdvisor. Il ripiego è uno snapshot, non un counter: registra soltanto che l’ultima terminazione del container è stata un OOM, decade dopo circa 15 minuti e non conta gli eventi.

# elenco: ogni serie restituita soddisfa la condizione
sum by (namespace, pod, container) (
kube_pod_container_status_last_terminated_reason{
reason="OOMKilled",
namespace!~"kube-.*|openshift-.*|default"
} == 1
)
* on(namespace, pod) group_left(node) max by (namespace,pod,node) (kube_pod_info)

OOM cgroup o OOM nodo

# conteggio nelle ultime 24 ore
sum by (node) (
increase(container_oom_events_total[24h])
* on(namespace, pod) group_left(node) max by (namespace,pod,node) (kube_pod_info)
)

Query decisiva. OOM sparsi su tutti i nodi = dimensionamento applicativo (OOM cgroup, il fix è sul pod). OOM concentrati su uno o due nodi = quei nodi sono in pressione e il kernel sta mietendo (OOM nodo, il fix è ribilanciare) → Nodo.

Conferma kernel-side

Terminal window
NS=<namespace>; POD=<pod>
NODE=$(oc get pod -n $NS $POD -o jsonpath='{.spec.nodeName}')
oc debug node/$NODE -- chroot /host journalctl --since "2 hours ago" --grep "oom.?kill|Killed process"

Output tipico:

Memory cgroup out of memory: Killed process 12345 (java) total-vm:5242880kB, anon-rss:4194304kB
oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=crio-abc...
ElementoSignificato
Memory cgroup out of memoryOOM cgroup — il container ha superato il proprio limit
constraint=CONSTRAINT_MEMCGconferma
constraint=CONSTRAINT_NONEOOM nodo — memoria fisica esaurita
anon-rssmemoria anonima al momento del kill, da confrontare col limit
cpuset=crio-<id>ID del container, per risalire al pod

Risalire dal container ID quando la vittima non è il pod che stavi guardando:

Terminal window
CID=<id-da-cpuset>
oc debug node/$NODE -- chroot /host crictl inspect $CID 2>/dev/null \
| jq -r '.status.labels | {ns: ."io.kubernetes.pod.namespace", pod: ."io.kubernetes.pod.name", c: ."io.kubernetes.container.name"}'

Pod evictati

# elenco: ogni serie restituita soddisfa la condizione
sum by (namespace, pod) (kube_pod_status_reason{reason="Evicted"} == 1)
* on(namespace, pod) group_left(node) max by (namespace,pod,node) (kube_pod_info)

L’eviction è il kubelet che agisce prima del kernel: preventiva e ordinata. Non è un OOM, ma la causa a monte è la stessa.

I pod Evicted restano nell’API come oggetti Failed e si accumulano a migliaia:

Terminal window
oc get pods -A --field-selector=status.phase=Failed -o json \
| jq -r '.items[] | select(.status.reason=="Evicted") | "\(.metadata.namespace) \(.metadata.name)"' \
| while read ns pod; do oc delete pod -n "$ns" "$pod"; done

La forma della curva dice il fix

FormaDiagnosiAzione
Rampa lineare fino al limitleaknon alzare il limit: sposta il problema. Escalation all’AM col grafico
Salto improvvisoallocazione singola grandepaginare la query, streammare il file, limitare il payload
Plateau al limit con oscillazionisotto-dimensionamentolimits.memory = picco 7d × 1.25
Segue il trafficodimensionato per il carico sbagliatoalzare il limit, o HPA
Heap JVM bassa ma container killatomemoria non-heap o -Xmx sbagliatoJVM — alzare -Xmx peggiora

Restart e lifecycle

Tre segnali distinti:

  • Restart count — quante volte è morto. Storico, cumulativo.
  • Waiting reason — perché non parte adesso. Snapshot.
  • Terminated reason — perché è morto l’ultima volta. Snapshot con decadenza.

Restart nel tempo

# conteggio negli ultimi 5 minuti · sostituisci $container, $ns, $pod
increase(kube_pod_container_status_restarts_total{
namespace="$ns", pod=~"$pod.*", container="$container"
}[5m])

Graficato su 24 ore. La regolarità è diagnostica: intervalli costanti = causa interna deterministica (leak, probe che scade sempre allo stesso punto); raffiche irregolari = causa esterna intermittente (dipendenza, rete, nodo).

Restart in corso adesso

# eventi al secondo
topk(20,
sum by (namespace, pod, container) (
rate(kube_pod_container_status_restarts_total{
namespace!~"kube-.*|openshift-.*|default"
}[15m])
)
) > 0

rate invece di increase: cattura il restart in corso, non quelli storici. Sopra 0.01 è un CrashLoop conclamato (un restart ogni 100 secondi o meno).

Top restart su finestre diverse

# conteggio nelle ultime 6 ore
topk(20,
sum by (namespace, pod, container) (
increase(kube_pod_container_status_restarts_total{
namespace!~"kube-.*|openshift-.*|default"
}[6h])
)
)
* on(namespace, pod) group_left(node) max by (namespace,pod,node) (kube_pod_info)

[1h] per l’incident in corso, [6h] per il turno, [24h] per il report.

Pod nuovo che nasce già rotto

# secondi
sum by (namespace, pod, container) (
increase(kube_pod_container_status_restarts_total{
namespace!~"kube-.*|openshift-.*|default"
}[1h])
) > 0
and on(namespace, pod) (
(time() - kube_pod_created) < 3600
)

Pod creati da meno di un’ora e già con restart. Restringe drasticamente le cause: errore in command/args, ConfigMap o Secret inesistenti, init container che fallisce, startupProbe assente su applicazione slow-start.

Diverso da un pod che rimbalza da tre giorni, che è quasi sempre memoria o dipendenza intermittente.

Età del pod

# ore · sostituisci $ns, $pod
(time() - kube_pod_created{namespace="$ns", pod=~"$pod.*"}) / 3600

In ore. Dieci restart in due ore e dieci in due settimane sono due problemi diversi.

Waiting reason

# elenco: ogni serie restituita soddisfa la condizione
sum by (namespace, pod, container, reason) (
kube_pod_container_status_waiting_reason{
namespace!~"kube-.*|openshift-.*|default"
} == 1
)
* on(namespace, pod) group_left(node) max by (namespace,pod,node) (kube_pod_info)

Distribuzione, per capire se è sistemico:

# elenco: ogni serie restituita soddisfa la condizione
sum by (reason) (kube_pod_container_status_waiting_reason == 1)

ImagePullBackOff diffuso su molti namespace = registry o pull-secret, non applicazioni.

reasonSignificatoPrima verifica
CrashLoopBackOffparte e muore ripetutamenteoc logs --previous
ImagePullBackOffpull fallito dopo retrypull-secret, tag, registry
ErrImagePullprimo pull fallitocome sopra
CreateContainerConfigErrorConfigMap o Secret referenziato non esisteoc describe pod
CreateContainerErrorCRI-O non riesce a creare il containerSCC, SELinux, mount
ContainerCreatingin corso — normale se brevese persiste: PVC non bound o CNI
PodInitializinginit container in esecuzionelog dell’init container
RunContainerErrorentrypoint non eseguibilepermessi, shebang, architettura immagine

Terminated reason

# elenco: ogni serie restituita soddisfa la condizione
sum by (namespace, pod, container, reason) (
kube_pod_container_status_last_terminated_reason{
namespace!~"kube-.*|openshift-.*|default"
} == 1
)
* on(namespace, pod) group_left(node) max by (namespace,pod,node) (kube_pod_info)
reasonExit codeSignificatoDove andare
OOMKilled137superato il memory limit del cgroupOOM
Error1, 2, altriuscita anomala dell’applicazioneoc logs --previous
Error137SIGKILL non da OOM: liveness probeProbe, o pausa GC → JVM
Error143SIGTERM non gestito in tempoterminationGracePeriodSeconds, shutdown hook
Error139SIGSEGV, crash nativolibreria JNI, driver, architettura immagine
Completed0uscita normaleatteso per Job; su un Deployment significa che il processo non resta in foreground
ContainerCannotRun—CRI-O ha rifiutato l’avvioSCC, SELinux, runAsUser
DeadlineExceeded—superato activeDeadlineSecondstimeout del Job

La distinzione tra 137 con OOMKilled e 137 con Error è la più utile della tabella: il primo è memoria, il secondo è quasi sempre una liveness probe che ha ucciso un’applicazione viva ma bloccata.

Pod Pending

# elenco: ogni serie restituita soddisfa la condizione
kube_pod_status_unschedulable == 1

kube_pod_status_unschedulable non ha un label reason: aggregare by (reason) restituisce una serie vuota. Il motivo sta negli events:

Terminal window
oc get pods -A --field-selector=status.phase=Pending -o wide
oc describe pod -n <ns> <pod> | sed -n '/Events:/,$p'
0/6 nodes are available: 3 Insufficient cpu, 3 Insufficient memory
node(s) had untolerated taint {node-role.kubernetes.io/master: }
node(s) didn't match Pod's node affinity/selector
pod has unbound immediate PersistentVolumeClaims

L’ultimo → Volumi. Gli altri → Nodo.

Decadenza delle metriche

MetricaPersistenza
kube_pod_container_status_last_terminated_reason~15 min dopo la terminazione
kube_pod_* di un pod eliminato~5 min (staleness Prometheus)
container_* di un container morto~2–3 min
kube_pod_container_status_restarts_totalfinché il pod esiste; azzerata alla ricreazione
Kubernetes events3 ore (default OCP)

Se un pod è stato sostituito da un rollout, il suo storico di restart non esiste più. Per questo lo snapshot va fatto in apertura di incident, non in fase di RCA:

Terminal window
TS=$(date +%Y%m%d-%H%M%S); mkdir -p /tmp/triage-$TS; cd /tmp/triage-$TS
oc get pods -A -o wide > pods.txt
oc get pods -A --field-selector=status.phase!=Running,status.phase!=Succeeded -o wide > pods-unhealthy.txt
oc get events -A --sort-by=.lastTimestamp | tail -200 > events.txt
oc adm top pods -A --sort-by=memory | head -40 > top-mem.txt
oc adm top pods -A --sort-by=cpu | head -40 > top-cpu.txt

Probe

Non esistono metriche PromQL native sui fallimenti delle probe (il kubelet non le espone come counter per pod). Il segnale arriva dagli events, correlato ai restart.

Restart senza corrispondenza nei log

# conteggio nelle ultime 6 ore · sostituisci $ns, $pod
increase(kube_pod_container_status_restarts_total{namespace="$ns", pod=~"$pod.*"}[6h])
# elenco: ogni serie restituita soddisfa la condizione · sostituisci $ns, $pod
kube_pod_container_status_last_terminated_reason{namespace="$ns", pod=~"$pod.*"} == 1

Se reason="Error" con exit code 137 e i log applicativi non mostrano nulla nei secondi precedenti, è una liveness probe che ha ucciso un processo vivo.

Terminal window
oc get events -n $ns --field-selector=involvedObject.name=$pod --sort-by=.lastTimestamp | grep -i probe

Liveness probe failed: context deadline exceeded ripetuto senza corrispondenza nei log = l’applicazione era ferma, non rotta.

Configurazione

Terminal window
oc get pod -n $ns $pod -o json | jq '.spec.containers[] | {name, livenessProbe, readinessProbe, startupProbe}'
SintomoCausaFix
Restart durante l’avvio, sempre allo stesso momentoinitialDelaySeconds basso, nessuna startupProbeaggiungere startupProbe con failureThreshold alto
Restart sotto carico, non a riposotimeoutSeconds basso: sotto carico la risposta supera il timeoutalzare timeoutSeconds, o dedicare un endpoint leggero alla probe
Restart senza errori nei logl’app era ferma: GC, deadlock, I/O bloccatoJVM
Pod tolto dal Service ma non riavviatoreadiness fallita, liveness okdipendenza esterna non raggiungibile

Errore ricorrente: usare come liveness un endpoint che interroga il database. Se il DB rallenta, Kubernetes riavvia tutte le repliche contemporaneamente e trasforma un degrado in un’interruzione totale. La liveness deve verificare solo che il processo sia vivo; la dipendenza esterna va nella readiness, o in nessuna delle due.

Pod non Ready

# elenco: ogni serie restituita soddisfa la condizione
sum by (namespace, pod) (
kube_pod_status_ready{condition="false", namespace!~"kube-.*|openshift-.*|default"} == 1
)
* on(namespace, pod) group_left(node) max by (namespace,pod,node) (kube_pod_info)

Tempo da non-Ready

# minuti · sostituisci $ns, $pod
(time() - kube_pod_status_ready_time{namespace="$ns", pod=~"$pod.*"}) / 60

Minuti dall’ultimo passaggio a Ready. Non disponibile su tutte le versioni di kube-state-metrics.


Configurazione e QoS

Container senza memory limit

# numero di serie
count by (namespace, pod, container) (
kube_pod_container_info{container!="POD", namespace!~"kube-.*|openshift-.*|default"}
)
unless
count by (namespace, pod, container) (
kube_pod_container_resource_limits{resource="memory"}
)

Non moriranno mai di OOM cgroup: faranno morire il nodo, e in quel caso il kernel killa per oom_score — spesso un pod innocente accanto.

Container senza CPU limit

# numero di serie
count by (namespace, pod, container) (
kube_pod_container_info{container!="POD", namespace!~"kube-.*|openshift-.*|default"}
)
unless
count by (namespace, pod, container) (
kube_pod_container_resource_limits{resource="cpu"}
)

Container senza request

# numero di serie
count by (namespace, pod, container) (
kube_pod_container_info{container!="POD", namespace!~"kube-.*|openshift-.*|default"}
)
unless
count by (namespace, pod, container) (
kube_pod_container_resource_requests{resource="memory"}
)

Peggio del precedente: lo scheduler li tratta come a costo zero e li impila tutti sullo stesso nodo.

Classe QoS

# elenco: ogni serie restituita soddisfa la condizione
sum by (qos_class) (kube_pod_status_qos_class == 1)
# numero di serie
count by (namespace) (
kube_pod_status_qos_class{qos_class="BestEffort", namespace!~"kube-.*|openshift-.*|default"} == 1
)

Ordine di sacrificio sotto pressione: BestEffort → Burstable (i più sopra le proprie request) → Guaranteed. In un namespace di produzione bancaria non dovrebbero esserci pod BestEffort.

ProfilorequestslimitsQoS
Latency-critical, carico piatto= picco 7d= requestsGuaranteed — nessun throttling, ultimo a essere evictato
Applicazione normale= media 7d= picco 7d × 1.3Burstable
Batch / Job= media2–3× requestsBurstable
MaiassenteassenteBestEffort

Limit configurati per container

# MiB · sostituisci $ns, $pod
kube_pod_container_resource_limits{namespace="$ns", pod=~"$pod.*", resource="memory"} / 1024^2
# core CPU · sostituisci $ns, $pod
kube_pod_container_resource_requests{namespace="$ns", pod=~"$pod.*", resource="cpu"}

Rapporto limit/request

# moltiplicatore: 1 = QoS Guaranteed · valori alti = rischio spostato a runtime · sostituisci $ns
kube_pod_container_resource_limits{namespace="$ns", resource="memory"}
/
kube_pod_container_resource_requests{namespace="$ns", resource="memory"}

1 = Guaranteed. Valori molto alti (> 4) significano che lo scheduler sta prenotando poco e il rischio è tutto a runtime.

Immagine in uso

# sempre 1: metrica portatrice di label (image, image_id) · sostituisci $ns, $pod
kube_pod_container_info{namespace="$ns", pod=~"$pod.*"}

Il label image e image_id (digest). Utile per verificare che tutte le repliche stiano davvero girando la stessa build — durante un rollout parziale non è così.

# numero di serie · sostituisci $ns, $workload
count by (namespace, image) (
kube_pod_container_info{namespace="$ns", pod=~"$workload-.*"}
)

Più di una riga = repliche su versioni diverse.

Pod su quale nodo

# sempre 1: metrica portatrice di label (node, host_ip, pod_ip) · sostituisci $ns, $pod
kube_pod_info{namespace="$ns", pod=~"$pod.*"}
# numero di serie · sostituisci $ns, $workload
count by (node) (kube_pod_info{namespace="$ns", pod=~"$workload-.*"})

Tutte le repliche sullo stesso nodo = manca l’anti-affinity, e un reboot di quel nodo porta giù il servizio.


Rete del pod

Le metriche container_network_* sono raccolte sulla network namespace del pod, quindi esistono solo sulla serie con container="". È l’unico caso in cui il filtro container!="" va omesso: usarlo restituisce una serie vuota.

Traffico

# MiB
topk(20,
sum by (namespace, pod) (
rate(container_network_receive_bytes_total{
namespace!~"kube-.*|openshift-.*|default"
}[5m])
)
) / 1024^2
# MiB
topk(20,
sum by (namespace, pod) (
rate(container_network_transmit_bytes_total{
namespace!~"kube-.*|openshift-.*|default"
}[5m])
)
) / 1024^2

Spike brevi

# MiB
topk(20,
max_over_time(
sum by (namespace, pod) (
rate(container_network_transmit_bytes_total{
namespace!~"kube-.*|openshift-.*|default"
}[1m])
)[6h:1m]
)
) / 1024^2

Finestra [1m] dentro una subquery a 6 ore: cattura i picchi che una rate([5m]) media via. La query per identificare job notturni e sincronizzazioni.

Pacchetti scartati

# pacchetti al secondo
topk(20,
sum by (namespace, pod) (
rate(container_network_receive_packets_dropped_total{
namespace!~"kube-.*|openshift-.*|default"
}[5m])
)
) > 0

Deve essere zero. Drop in ricezione = buffer del socket pieno, l’applicazione non consuma abbastanza in fretta.

Dimensione media del pacchetto

# byte al secondo · sostituisci $ns, $pod
rate(container_network_receive_bytes_total{namespace="$ns", pod=~"$pod.*"}[5m])
/
rate(container_network_receive_packets_total{namespace="$ns", pod=~"$pod.*"}[5m])

Pacchetti sotto i 200 byte ad alta frequenza indicano protocolli chatty o mancanza di batching: costano CPU al kernel e al sidecar molto più del volume dati.

Approfondimento su drop, conntrack, OVN e ingress: Rete.


Filesystem del container

Il layer scrivibile e gli emptyDir non hanno limite di default: un pod che riempie /tmp riempie il disco del nodo, e il kubelet inizia a evictare pod su quel nodo.

Uso del filesystem effimero

# MiB
topk(20,
sum by (namespace, pod) (
container_fs_usage_bytes{
namespace!~"kube-.*|openshift-.*|default",
container!="", container!="POD"
}
)
) / 1024^2

I/O per pod

# MiB
topk(20,
sum by (namespace, pod) (
rate(container_fs_writes_bytes_total{namespace!~"kube-.*|openshift-.*|default"}[5m])
+
rate(container_fs_reads_bytes_total{namespace!~"kube-.*|openshift-.*|default"}[5m])
)
) / 1024^2

MiB/s combinati. Identifica i pod che stressano lo storage del nodo: export, dump, indicizzazione, compattazione.

IOPS per pod

# operazioni al secondo
topk(20,
sum by (namespace, pod) (
rate(container_fs_writes_total{namespace!~"kube-.*|openshift-.*|default"}[5m])
)
)

Container senza limite di ephemeral-storage

# numero di serie
count by (namespace, pod, container) (
kube_pod_container_info{container!="POD", namespace!~"kube-.*|openshift-.*|default"}
)
unless
count by (namespace, pod, container) (
kube_pod_container_resource_limits{resource="ephemeral_storage"}
)

Impostare limits.ephemeral-storage fa sì che il kubelet evicti solo il pod colpevole invece di mettere in pressione l’intero nodo. Su cluster multi-tenant dovrebbe essere obbligatorio.

PVC e volumi persistenti: Volumi.


RCA guidata — pod che restarta

Flusso completo, dal sintomo alla causa scrivibile in un ticket.

Setup

Terminal window
NS=<namespace>; POD=<pod>; C=<container>
NODE=$(oc get pod -n $NS $POD -o jsonpath='{.spec.nodeName}')
echo "ns=$NS pod=$POD container=$C node=$NODE"

Fase 0 — Impatto

Terminal window
oc get pod -n $NS $POD
# percentuale 0-100 · sostituisci $ns
100 * (
kube_deployment_status_replicas_available{namespace="$ns"}
/
kube_deployment_spec_replicas{namespace="$ns"}
)

Se le altre repliche reggono, è un ticket. Se il rapporto è 0, è un incident: cattura lo snapshot prima di toccare qualsiasi cosa.

Fase 1 — Tutte le repliche o solo questa

# numero di serie · sostituisci $ns, $workload
count(kube_pod_container_status_restarts_total{namespace="$ns", pod=~"$workload-.*"} > 0)
/
count(kube_pod_info{namespace="$ns", pod=~"$workload-.*"})
  • Tutte → problema del workload: immagine, config, dipendenza comune. Fase 2.
  • Solo alcune, stesso nodo → è il nodo. Vai a Nodo.
  • Solo una, sparsa → quella replica ha ricevuto qualcosa che le altre no.

Fase 2 — Il motivo dichiarato

Terminal window
oc get pod -n $NS $POD -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\n"}{end}'

Instrada con la tabella dei terminated reason.

Fase 3 — Log del ciclo precedente

Terminal window
oc logs -n $NS $POD -c $C --previous --tail=200

Il container in CrashLoopBackOff non gira e non ha log: serve --previous.

PatternSignificato
Exception in thread, Traceback, panic:crash applicativo: hai già la causa
Connection refused, timeout, UnknownHostdipendenza esterna → Fase 5
OutOfMemoryError: Java heap spaceOOM della JVM, non del container → JVM
OutOfMemoryError: Metaspacenon-heap → JVM
Permission denied, Operation not permittedSCC / SELinux → Fase 6
log vuoto o troncato a metà rigaucciso senza poter scrivere: OOM o SIGKILL
nessun errore, log che si interrompel’applicazione era viva ma ferma: pausa GC, deadlock, I/O bloccato

L’ultima riga è quella che si sbaglia più spesso: assenza di errori nei log non significa assenza di problemi.

Fase 4 — Events

Terminal window
oc get events -n $NS --field-selector=involvedObject.name=$POD --sort-by=.lastTimestamp
EventoCausa
Back-off restarting failed containerconferma del CLBO, nessuna informazione in più
Liveness probe faileducciso da Kubernetes, non morto da solo → Probe
Readiness probe failednon causa restart, ma toglie il pod dal Service
Failed to pull imageregistry, pull-secret, tag
MountVolume.SetUp failedPVC, ConfigMap o Secret
Insufficient cpu/memorynon è mai partito → Nodo

Fase 5 — Config e dipendenze

Terminal window
oc get pod -n $NS $POD -o json | jq '.spec.volumes[] | select(.configMap or .secret)'
oc get pod -n $NS $POD -o json | jq '[.spec.containers[].env[]? | select(.valueFrom)]'
oc get cm,secret -n $NS
oc get pod -n $NS $POD -o json | jq '.spec.containers[] | {name, command, args, image}'
oc get pod -n $NS $POD -o json | jq '.status.initContainerStatuses'

Verifica dalla stessa rete del pod:

Terminal window
oc debug -n $NS pod/$POD -- sh -c 'getent hosts <host>; timeout 5 bash -c "</dev/tcp/<host>/<porta>" && echo OPEN || echo CLOSED'

Fase 6 — SCC, SELinux, permessi

Terminal window
oc logs -n $NS $POD -c $C --previous | grep -iE "permission denied|operation not permitted|cannot open"
oc get pod -n $NS $POD -o jsonpath='{.metadata.annotations.openshift\.io/scc}{"\n"}'
oc get pod -n $NS $POD -o json | jq '.spec.securityContext, .spec.containers[].securityContext'

Causa tipica: immagine costruita per girare come root, namespace con SCC restricted-v2 che assegna un UID arbitrario. Il processo non riesce a scrivere nelle directory dell’immagine.

Fase 7 — Conferma per esclusione

Terminal window
oc adm cordon $NODE
oc delete pod -n $NS $POD
# il ReplicaSet riprogramma altrove: il problema segue il pod o resta sul nodo?
oc adm uncordon $NODE

Fase 8 — Report

# RCA — <pod> restart in loop
**Ambiente**: <cluster> · namespace `<ns>` · nodo `<node>`
**Workload**: `<deployment>` · container `<container>`
**Finestra**: <primo restart> → <fix applicato>
**Impatto**: <N repliche su M, servizio interrotto/degradato/nessun impatto>
## Sintomo
<pod> in CrashLoopBackOff, N restart. Reason `<X>`, exit code `<Y>`.
## Root cause
<una frase causale, non descrittiva>
## Evidenza
- Log applicativo: <estratto>
- Events: <estratto>
- Metriche: <query PromQL + finestra>
- Kernel (se OOM): <estratto con CONSTRAINT_*>
## Perimetro
<Infrastruttura / Applicativo> — <chi deve agire>
## Fix
<azione, con valori prima → dopo>
## Prevenzione
- <alert da attivare>
- <verifica da aggiungere al rilascio>

Il campo Perimetro evita il rimpallo: dichiarare esplicitamente se la causa è infrastrutturale o applicativa, con l’evidenza a supporto, chiude la discussione prima che si apra.


Comandi oc

Terminal window
# Pod non sani
oc get pods -A --field-selector=status.phase!=Running,status.phase!=Succeeded -o wide
# Classifica per restart, con l'ultimo motivo
oc get pods -A --sort-by='.status.containerStatuses[0].restartCount' \
-o custom-columns=NS:.metadata.namespace,POD:.metadata.name,\
NODE:.spec.nodeName,\
RESTARTS:.status.containerStatuses[0].restartCount,\
REASON:.status.containerStatuses[0].lastState.terminated.reason \
| tail -25
# Stato completo dei container di un pod
oc get pod -n $ns $pod -o json | jq '.status.containerStatuses[] | {name, ready, restartCount, state, lastState}'
# Risorse configurate
oc get pod -n $ns $pod -o json | jq '.spec.containers[] | {name, resources}'
oc get pod -n $ns $pod -o jsonpath='{.status.qosClass}{"\n"}'
# Log del ciclo precedente
oc logs -n $ns $pod -c $c --previous --tail=200
# Log di tutti i container
oc logs -n $ns $pod --all-containers --tail=100
# Events del pod
oc get events -n $ns --field-selector=involvedObject.name=$pod --sort-by=.lastTimestamp
# Solo Warning, tutto il cluster
oc get events -A --field-selector type=Warning --sort-by=.lastTimestamp | tail -40
# Consumo reale
oc adm top pod -n $ns $pod --containers
# Nodo che ospita il pod
oc get pod -n $ns $pod -o jsonpath='{.spec.nodeName}{"\n"}'
# Dentro il container
oc rsh -n $ns $pod df -h
oc rsh -n $ns $pod ps -ef
oc rsh -n $ns $pod cat /sys/fs/cgroup/memory.max 2>/dev/null || oc rsh -n $ns $pod cat /sys/fs/cgroup/memory/memory.limit_in_bytes

L’ultimo comando legge il limit dal punto di vista del container: utile per verificare che sia davvero quello che credi, indipendentemente dal manifest.


Alert

Da applicare nel namespace dell’applicazione. Richiede il user workload monitoring attivo:

Terminal window
oc -n openshift-monitoring get cm cluster-monitoring-config -o jsonpath='{.data.config\.yaml}'
# deve contenere: enableUserWorkload: true
oc -n openshift-user-workload-monitoring get pods
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: pod-health
namespace: <ns>
labels:
role: alert-rules
spec:
groups:
- name: pod.memory
rules:
- alert: ContainerMemoryNearLimit
expr: |
(
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
for: 15m
labels:
severity: warning
annotations:
summary: "{{ $labels.pod }}/{{ $labels.container }} oltre il 90% del memory limit"
description: "Uso al {{ $value | humanizePercentage }} da 15 minuti. OOMKill probabile al prossimo picco: verificare il picco 7d e ridimensionare limits.memory."
- alert: ContainerOOMKilled
expr: increase(container_oom_events_total[10m]) > 0
labels:
severity: critical
annotations:
summary: "{{ $labels.pod }}/{{ $labels.container }} OOMKilled"
description: "{{ $value }} OOM kill in 10 minuti. Distinguere leak da sotto-dimensionamento prima di alzare il limit."
- alert: ContainerMemoryLeakSuspected
expr: |
deriv(
sum by (namespace, pod, container) (
container_memory_working_set_bytes{container!="", container!="POD"}
)[6h:5m]
) * 3600 / 1024^2 > 50
for: 6h
labels:
severity: warning
annotations:
summary: "{{ $labels.pod }}/{{ $labels.container }} cresce di {{ $value | printf \"%.0f\" }} MiB/h"
description: "Crescita monotona da 6 ore. Candidato leak: alzare il limit sposta il problema, non lo risolve."
- name: pod.cpu
rules:
- alert: ContainerCPUThrottled
expr: |
(
sum by (namespace, pod, container) (
rate(container_cpu_cfs_throttled_periods_total{container!="", container!="POD"}[15m])
)
/
sum by (namespace, pod, container) (
rate(container_cpu_cfs_periods_total{container!="", container!="POD"}[15m])
)
) > 0.20
for: 15m
labels:
severity: warning
annotations:
summary: "{{ $labels.pod }}/{{ $labels.container }} throttled al {{ $value | humanizePercentage }}"
description: "Bloccato dal CFS oltre il 20% dei periodi. Latenza già degradata: alzare limits.cpu."
- name: pod.lifecycle
rules:
- alert: PodCrashLooping
expr: increase(kube_pod_container_status_restarts_total[15m]) > 3
for: 5m
labels:
severity: critical
annotations:
summary: "{{ $labels.pod }}/{{ $labels.container }} in CrashLoop"
description: "{{ $value }} restart in 15 minuti. Partire da: oc logs --previous."
- alert: PodNotReady
expr: |
sum by (namespace, pod) (kube_pod_status_ready{condition="false"} == 1)
for: 30m
labels:
severity: warning
annotations:
summary: "{{ $labels.pod }} non Ready da 30 minuti"
- alert: PodImagePullFailing
expr: |
kube_pod_container_status_waiting_reason{reason=~"ImagePullBackOff|ErrImagePull"} == 1
for: 10m
labels:
severity: warning
annotations:
summary: "{{ $labels.pod }}/{{ $labels.container }}: {{ $labels.reason }}"
description: "Verificare pull-secret, tag e raggiungibilità del registry."

Dimensionare il for

Tipo di segnaleforRagione
Evento discreto (OOM, restart)assenteè già successo, aspettare non aggiunge nulla
Saturazione (memoria, throttling)15mfiltra i picchi transitori
Trend (leak)2h – 6hserve una tendenza, non un campione
Disponibilità (Ready, repliche)20m – 30mcopre rollout e restart normali

Con for: 15m e scrape a 30 secondi, l’espressione deve essere vera per 30 valutazioni consecutive. Una sola valutazione falsa azzera il contatore: comportamento voluto per la saturazione, sbagliato per un evento discreto.

Non duplicare gli alert nativi

KubePodCrashLooping, KubePodNotReady, KubeContainerWaiting esistono già. Verifica prima:

Terminal window
oc get prometheusrule -A | grep -v user-workload
oc -n openshift-monitoring get prometheusrule kubernetes-monitoring-rules -o yaml | grep "alert:"

Soglie di riferimento

MetricaSanoAttenzioneCritico
memoria / limit< 70%70 – 90%> 90%
CPU / limit< 70%70 – 85%> 85%
throttling CFS< 1%1 – 5%> 20%
restart in 15 min01 – 3> 3
crescita memoria~0 MiB/h> 20 MiB/h> 50 MiB/h su 6h
drop pacchetti0> 0> 0.1% dei pacchetti

Vedi anche

  • Workload — quando il problema è di tutte le repliche
  • Nodo — quando è concentrato su un nodo
  • JVM — applicazioni Java
  • Volumi — PVC montati nel pod
  • Rete — connettività e OVN