Pod / Container
Tutto ciò che si può misurare su un pod. È la pagina di riferimento per qualunque segnalazione circoscritta a un singolo workload.
| Triage | le prime cinque query |
| CPU · Throttling | consumo e saturazione |
| Memoria · OOM | consumo, leak, kill |
| Restart e lifecycle | CrashLoop, waiting, terminated |
| Probe | liveness, readiness, startup |
| Configurazione e QoS | limit, request, classi |
| Rete · Filesystem | traffico e I/O |
| RCA guidata | il 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 condizionesum 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 oratopk(20, sum by (namespace, pod, container) ( increase(kube_pod_container_status_restarts_total{ namespace!~"kube-.*|openshift-.*|default" }[1h]) )) > 0T3. OOM kill nell’ultima ora
# conteggio nell'ultima orasum by (namespace, pod, container) ( increase(container_oom_events_total{ namespace!~"kube-.*|openshift-.*|default" }[1h])) > 0T4. Container vicini al memory limit
# percentuale 0-100topk(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 bloccatotopk(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 orasum 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 CPUtopk(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, $podsum 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, $podsum 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-100topk(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, $pod100 * ( 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, $podmax_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, $podavg_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, $podmax_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, $workloadsum 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, $podcontainer_cpu_load_average_10s{namespace="$ns", pod=~"$pod.*", container!="", container!="POD"} / 1000Numero 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 bloccatotopk(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, $pod100 * ( 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, $podrate(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-100100 * ( 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]) )) > 10and100 * ( 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"} )) < 50Container 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
| Metrica | Include | Quando |
|---|---|---|
container_memory_working_set_bytes | RSS + page cache attiva | default: è quella che il kernel confronta col limit |
container_memory_rss | solo RSS anonima | confronto con la heap JVM |
container_memory_cache | solo page cache | capire se il working set alto è cache di file |
container_memory_usage_bytes | tutto, cache vecchia inclusa | quasi mai: sovrastima |
container_memory_swap | swap | su OCP normalmente zero, swap disabilitato |
Consumo per pod
# MiBtopk(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^2In 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, $podsum by (namespace, pod, container) ( container_memory_working_set_bytes{ namespace="$ns", pod=~"$pod.*", container!="", container!="POD" }) / 1024^2Un 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-100topk(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)| Valore | Lettura |
|---|---|
< 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, $podsum by (container) (container_memory_working_set_bytes{namespace="$ns", pod=~"$pod.*", container!="", container!="POD"}) / 1024^2# MiB · sostituisci $ns, $podsum by (container) (container_memory_rss{namespace="$ns", pod=~"$pod.*", container!="", container!="POD"}) / 1024^2# MiB · sostituisci $ns, $podsum by (container) (container_memory_cache{namespace="$ns", pod=~"$pod.*", container!="", container!="POD"}) / 1024^2Lette 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, $podmax_over_time( sum by (namespace, pod, container) ( container_memory_working_set_bytes{ namespace="$ns", pod=~"$pod.*", container!="", container!="POD" } )[7d:5m]) / 1024^2limits.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, $podderiv( sum by (namespace, pod, container) ( container_memory_working_set_bytes{ namespace="$ns", pod=~"$pod.*", container!="", container!="POD" } )[6h:5m]) * 3600 / 1024^2MiB di crescita per ora, come regressione lineare sulle ultime 6 ore.
| Andamento | Diagnosi |
|---|---|
| ~0, oscilla intorno allo zero | consumo stabile: se sei vicino al limit è sotto-dimensionamento |
| costantemente positivo su più finestre | crescita monotona: candidato leak |
| positivo solo in certe fasce orarie | carico, non leak |
La verifica decisiva: se resta positiva anche quando il traffico cala di notte, è un leak.
Top candidati leak del cluster
# MiB all'oratopk(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 restartLa 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, $podsum 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, $podkube_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, $podsum 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 oretopk(20, sum by (namespace, pod, container) ( increase(container_oom_events_total{ namespace!~"kube-.*|openshift-.*|default" }[24h]) )) > 0container_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 oretopk(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 seriecount(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 condizionesum 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 oresum 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
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:4194304kBoom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=crio-abc...| Elemento | Significato |
|---|---|
Memory cgroup out of memory | OOM cgroup — il container ha superato il proprio limit |
constraint=CONSTRAINT_MEMCG | conferma |
constraint=CONSTRAINT_NONE | OOM nodo — memoria fisica esaurita |
anon-rss | memoria 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:
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 condizionesum 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:
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"; doneLa forma della curva dice il fix
| Forma | Diagnosi | Azione |
|---|---|---|
| Rampa lineare fino al limit | leak | non alzare il limit: sposta il problema. Escalation all’AM col grafico |
| Salto improvviso | allocazione singola grande | paginare la query, streammare il file, limitare il payload |
| Plateau al limit con oscillazioni | sotto-dimensionamento | limits.memory = picco 7d × 1.25 |
| Segue il traffico | dimensionato per il carico sbagliato | alzare il limit, o HPA |
| Heap JVM bassa ma container killato | memoria non-heap o -Xmx sbagliato | JVM — 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, $podincrease(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 secondotopk(20, sum by (namespace, pod, container) ( rate(kube_pod_container_status_restarts_total{ namespace!~"kube-.*|openshift-.*|default" }[15m]) )) > 0rate 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 oretopk(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
# secondisum by (namespace, pod, container) ( increase(kube_pod_container_status_restarts_total{ namespace!~"kube-.*|openshift-.*|default" }[1h])) > 0and 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.*"}) / 3600In ore. Dieci restart in due ore e dieci in due settimane sono due problemi diversi.
Waiting reason
# elenco: ogni serie restituita soddisfa la condizionesum 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 condizionesum by (reason) (kube_pod_container_status_waiting_reason == 1)ImagePullBackOff diffuso su molti namespace = registry o pull-secret, non applicazioni.
reason | Significato | Prima verifica |
|---|---|---|
CrashLoopBackOff | parte e muore ripetutamente | oc logs --previous |
ImagePullBackOff | pull fallito dopo retry | pull-secret, tag, registry |
ErrImagePull | primo pull fallito | come sopra |
CreateContainerConfigError | ConfigMap o Secret referenziato non esiste | oc describe pod |
CreateContainerError | CRI-O non riesce a creare il container | SCC, SELinux, mount |
ContainerCreating | in corso — normale se breve | se persiste: PVC non bound o CNI |
PodInitializing | init container in esecuzione | log dell’init container |
RunContainerError | entrypoint non eseguibile | permessi, shebang, architettura immagine |
Terminated reason
# elenco: ogni serie restituita soddisfa la condizionesum 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)reason | Exit code | Significato | Dove andare |
|---|---|---|---|
OOMKilled | 137 | superato il memory limit del cgroup | OOM |
Error | 1, 2, altri | uscita anomala dell’applicazione | oc logs --previous |
Error | 137 | SIGKILL non da OOM: liveness probe | Probe, o pausa GC → JVM |
Error | 143 | SIGTERM non gestito in tempo | terminationGracePeriodSeconds, shutdown hook |
Error | 139 | SIGSEGV, crash nativo | libreria JNI, driver, architettura immagine |
Completed | 0 | uscita normale | atteso per Job; su un Deployment significa che il processo non resta in foreground |
ContainerCannotRun | — | CRI-O ha rifiutato l’avvio | SCC, SELinux, runAsUser |
DeadlineExceeded | — | superato activeDeadlineSeconds | timeout 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 condizionekube_pod_status_unschedulable == 1kube_pod_status_unschedulable non ha un label reason: aggregare by (reason) restituisce una serie vuota. Il motivo sta negli events:
oc get pods -A --field-selector=status.phase=Pending -o wideoc describe pod -n <ns> <pod> | sed -n '/Events:/,$p'0/6 nodes are available: 3 Insufficient cpu, 3 Insufficient memorynode(s) had untolerated taint {node-role.kubernetes.io/master: }node(s) didn't match Pod's node affinity/selectorpod has unbound immediate PersistentVolumeClaimsL’ultimo → Volumi. Gli altri → Nodo.
Decadenza delle metriche
| Metrica | Persistenza |
|---|---|
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_total | finché il pod esiste; azzerata alla ricreazione |
| Kubernetes events | 3 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:
TS=$(date +%Y%m%d-%H%M%S); mkdir -p /tmp/triage-$TS; cd /tmp/triage-$TSoc get pods -A -o wide > pods.txtoc get pods -A --field-selector=status.phase!=Running,status.phase!=Succeeded -o wide > pods-unhealthy.txtoc get events -A --sort-by=.lastTimestamp | tail -200 > events.txtoc adm top pods -A --sort-by=memory | head -40 > top-mem.txtoc adm top pods -A --sort-by=cpu | head -40 > top-cpu.txtProbe
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, $podincrease(kube_pod_container_status_restarts_total{namespace="$ns", pod=~"$pod.*"}[6h])# elenco: ogni serie restituita soddisfa la condizione · sostituisci $ns, $podkube_pod_container_status_last_terminated_reason{namespace="$ns", pod=~"$pod.*"} == 1Se 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.
oc get events -n $ns --field-selector=involvedObject.name=$pod --sort-by=.lastTimestamp | grep -i probeLiveness probe failed: context deadline exceeded ripetuto senza corrispondenza nei log = l’applicazione era ferma, non rotta.
Configurazione
oc get pod -n $ns $pod -o json | jq '.spec.containers[] | {name, livenessProbe, readinessProbe, startupProbe}'| Sintomo | Causa | Fix |
|---|---|---|
| Restart durante l’avvio, sempre allo stesso momento | initialDelaySeconds basso, nessuna startupProbe | aggiungere startupProbe con failureThreshold alto |
| Restart sotto carico, non a riposo | timeoutSeconds basso: sotto carico la risposta supera il timeout | alzare timeoutSeconds, o dedicare un endpoint leggero alla probe |
| Restart senza errori nei log | l’app era ferma: GC, deadlock, I/O bloccato | JVM |
| Pod tolto dal Service ma non riavviato | readiness fallita, liveness ok | dipendenza 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 condizionesum 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.*"}) / 60Minuti dall’ultimo passaggio a Ready. Non disponibile su tutte le versioni di kube-state-metrics.
Configurazione e QoS
Container senza memory limit
# numero di seriecount by (namespace, pod, container) ( kube_pod_container_info{container!="POD", namespace!~"kube-.*|openshift-.*|default"})unlesscount 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 seriecount by (namespace, pod, container) ( kube_pod_container_info{container!="POD", namespace!~"kube-.*|openshift-.*|default"})unlesscount by (namespace, pod, container) ( kube_pod_container_resource_limits{resource="cpu"})Container senza request
# numero di seriecount by (namespace, pod, container) ( kube_pod_container_info{container!="POD", namespace!~"kube-.*|openshift-.*|default"})unlesscount 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 condizionesum by (qos_class) (kube_pod_status_qos_class == 1)# numero di seriecount 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.
| Profilo | requests | limits | QoS |
|---|---|---|---|
| Latency-critical, carico piatto | = picco 7d | = requests | Guaranteed — nessun throttling, ultimo a essere evictato |
| Applicazione normale | = media 7d | = picco 7d × 1.3 | Burstable |
| Batch / Job | = media | 2–3× requests | Burstable |
| Mai | assente | assente | BestEffort |
Limit configurati per container
# MiB · sostituisci $ns, $podkube_pod_container_resource_limits{namespace="$ns", pod=~"$pod.*", resource="memory"} / 1024^2# core CPU · sostituisci $ns, $podkube_pod_container_resource_requests{namespace="$ns", pod=~"$pod.*", resource="cpu"}Rapporto limit/request
# moltiplicatore: 1 = QoS Guaranteed · valori alti = rischio spostato a runtime · sostituisci $nskube_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, $podkube_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, $workloadcount 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, $podkube_pod_info{namespace="$ns", pod=~"$pod.*"}# numero di serie · sostituisci $ns, $workloadcount 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
# MiBtopk(20, sum by (namespace, pod) ( rate(container_network_receive_bytes_total{ namespace!~"kube-.*|openshift-.*|default" }[5m]) )) / 1024^2# MiBtopk(20, sum by (namespace, pod) ( rate(container_network_transmit_bytes_total{ namespace!~"kube-.*|openshift-.*|default" }[5m]) )) / 1024^2Spike brevi
# MiBtopk(20, max_over_time( sum by (namespace, pod) ( rate(container_network_transmit_bytes_total{ namespace!~"kube-.*|openshift-.*|default" }[1m]) )[6h:1m] )) / 1024^2Finestra [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 secondotopk(20, sum by (namespace, pod) ( rate(container_network_receive_packets_dropped_total{ namespace!~"kube-.*|openshift-.*|default" }[5m]) )) > 0Deve 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, $podrate(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
# MiBtopk(20, sum by (namespace, pod) ( container_fs_usage_bytes{ namespace!~"kube-.*|openshift-.*|default", container!="", container!="POD" } )) / 1024^2I/O per pod
# MiBtopk(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^2MiB/s combinati. Identifica i pod che stressano lo storage del nodo: export, dump, indicizzazione, compattazione.
IOPS per pod
# operazioni al secondotopk(20, sum by (namespace, pod) ( rate(container_fs_writes_total{namespace!~"kube-.*|openshift-.*|default"}[5m]) ))Container senza limite di ephemeral-storage
# numero di seriecount by (namespace, pod, container) ( kube_pod_container_info{container!="POD", namespace!~"kube-.*|openshift-.*|default"})unlesscount 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
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
oc get pod -n $NS $POD# percentuale 0-100 · sostituisci $ns100 * ( 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, $workloadcount(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
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
oc logs -n $NS $POD -c $C --previous --tail=200Il container in CrashLoopBackOff non gira e non ha log: serve --previous.
| Pattern | Significato |
|---|---|
Exception in thread, Traceback, panic: | crash applicativo: hai già la causa |
Connection refused, timeout, UnknownHost | dipendenza esterna → Fase 5 |
OutOfMemoryError: Java heap space | OOM della JVM, non del container → JVM |
OutOfMemoryError: Metaspace | non-heap → JVM |
Permission denied, Operation not permitted | SCC / SELinux → Fase 6 |
| log vuoto o troncato a metà riga | ucciso senza poter scrivere: OOM o SIGKILL |
| nessun errore, log che si interrompe | l’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
oc get events -n $NS --field-selector=involvedObject.name=$POD --sort-by=.lastTimestamp| Evento | Causa |
|---|---|
Back-off restarting failed container | conferma del CLBO, nessuna informazione in più |
Liveness probe failed | ucciso da Kubernetes, non morto da solo → Probe |
Readiness probe failed | non causa restart, ma toglie il pod dal Service |
Failed to pull image | registry, pull-secret, tag |
MountVolume.SetUp failed | PVC, ConfigMap o Secret |
Insufficient cpu/memory | non è mai partito → Nodo |
Fase 5 — Config e dipendenze
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 $NSoc 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:
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
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
oc adm cordon $NODEoc delete pod -n $NS $POD# il ReplicaSet riprogramma altrove: il problema segue il pod o resta sul nodo?oc adm uncordon $NODEFase 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
# Pod non sanioc get pods -A --field-selector=status.phase!=Running,status.phase!=Succeeded -o wide
# Classifica per restart, con l'ultimo motivooc 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 podoc get pod -n $ns $pod -o json | jq '.status.containerStatuses[] | {name, ready, restartCount, state, lastState}'
# Risorse configurateoc 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 precedenteoc logs -n $ns $pod -c $c --previous --tail=200
# Log di tutti i containeroc logs -n $ns $pod --all-containers --tail=100
# Events del podoc get events -n $ns --field-selector=involvedObject.name=$pod --sort-by=.lastTimestamp
# Solo Warning, tutto il clusteroc get events -A --field-selector type=Warning --sort-by=.lastTimestamp | tail -40
# Consumo realeoc adm top pod -n $ns $pod --containers
# Nodo che ospita il podoc get pod -n $ns $pod -o jsonpath='{.spec.nodeName}{"\n"}'
# Dentro il containeroc rsh -n $ns $pod df -hoc rsh -n $ns $pod ps -efoc 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_bytesL’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:
oc -n openshift-monitoring get cm cluster-monitoring-config -o jsonpath='{.data.config\.yaml}'# deve contenere: enableUserWorkload: trueoc -n openshift-user-workload-monitoring get podsapiVersion: monitoring.coreos.com/v1kind: PrometheusRulemetadata: name: pod-health namespace: <ns> labels: role: alert-rulesspec: 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 segnale | for | Ragione |
|---|---|---|
| Evento discreto (OOM, restart) | assente | è già successo, aspettare non aggiunge nulla |
| Saturazione (memoria, throttling) | 15m | filtra i picchi transitori |
| Trend (leak) | 2h – 6h | serve una tendenza, non un campione |
| Disponibilità (Ready, repliche) | 20m – 30m | copre 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:
oc get prometheusrule -A | grep -v user-workloadoc -n openshift-monitoring get prometheusrule kubernetes-monitoring-rules -o yaml | grep "alert:"Soglie di riferimento
| Metrica | Sano | Attenzione | Critico |
|---|---|---|---|
| memoria / limit | < 70% | 70 – 90% | > 90% |
| CPU / limit | < 70% | 70 – 85% | > 85% |
| throttling CFS | < 1% | 1 – 5% | > 20% |
| restart in 15 min | 0 | 1 – 3 | > 3 |
| crescita memoria | ~0 MiB/h | > 20 MiB/h | > 50 MiB/h su 6h |
| drop pacchetti | 0 | > 0 | > 0.1% dei pacchetti |