Skip to content

RCA — Ingress Gateway in OOMKilled

Percorso di RCA per il caso classico: tutti i replica di istio-ingressgateway con restart count alto e crescente, pod vecchi di mesi, nessun rollout in corso.

Sintomo

istio-ingressgateway-759b886f64-255zg 1/1 Running 49 (10m ago) 228d
istio-ingressgateway-759b886f64-2pnzt 1/1 Running 47 (30m ago) 228d
istio-ingressgateway-759b886f64-5c4nf 1/1 Running 51 (19m ago) 228d

Restart scaglionati nel tempo su tutti i replica → non è un rollout né un problema di un singolo nodo: è il container istio-proxy che muore da solo, indipendentemente su ogni pod.

Trappola: readiness ≠ causa

Negli eventi del namespace si vedono decine di righe come:

Warning Unhealthy pod/istio-ingressgateway-... Readiness probe failed:
Get "http://10.131.0.9:15021/healthz/ready": dial tcp ...: connect: connection refused

Sono conseguenza, non causa: subito dopo un restart Envoy impiega qualche secondo ad aprire la porta 15021 e la readiness fallisce.

La discriminante è l’assenza di Liveness probe failed e di Killing:

Eventi presentiInterpretazione
Solo Readiness probe failedIl kubelet non sta uccidendo il container → morte interna (OOM o crash)
Liveness probe failed + KillingRestart pilotato dalla liveness → indagare probe e latenza

Step 1 — Causa del restart

Terminal window
oc get pod -n istio-system -l app=istio-ingressgateway -o custom-columns='NAME:.metadata.name,RC:.status.containerStatuses[*].restartCount,REASON:.status.containerStatuses[*].lastState.terminated.reason,EXIT:.status.containerStatuses[*].lastState.terminated.exitCode,SIG:.status.containerStatuses[*].lastState.terminated.signal,AT:.status.containerStatuses[*].lastState.terminated.finishedAt'

Lettura degli exit code:

ExitReasonSignificato
137OOMKilledSuperato il limits.memory del container
137ErrorStesso OOM, ma il kubelet non ha attribuito il flag (capita sotto pressione del nodo) — trattalo come OOM
139ErrorSIGSEGV — crash di Envoy, non memoria
143ErrorSIGTERM — terminazione pilotata

Step 2 — Log del container morto

Terminal window
oc logs -n istio-system <pod> -c istio-proxy --previous --tail=200

In caso di OOM il log è tipicamente troncato senza errori: Envoy viene ucciso con SIGKILL e non ha modo di loggare nulla. L’assenza di messaggi conferma l’ipotesi invece di smentirla.

Step 3 — Limiti vs consumo reale

Terminal window
oc get deploy istio-ingressgateway -n istio-system -o jsonpath='{.spec.template.spec.containers[?(@.name=="istio-proxy")].resources}{"\n"}'
oc adm top pod -n istio-system -l app=istio-ingressgateway

Esempio reale:

{"limits":{"cpu":"2","memory":"1Gi"},"requests":{"cpu":"10m","memory":"128Mi"}}

Due problemi in una riga:

  • 1Gi di limit è insufficiente per un ingress gateway di un mesh non banale.
  • requests: 128Mi contro limits: 1Gi (ratio 8x) → QoS Burstable con scheduling completamente scollegato dal consumo reale: lo scheduler prenota 128Mi, il pod ne usa 700+.

Incrociando oc adm top con l’età dei pod si ricava il rate di crescita:

hvzdg 737Mi (restart 32m fa) → ~10 Mi/min
qwfhf 424Mi (restart 7m fa)

Step 4 — Dove finisce la memoria

Terminal window
oc exec -n istio-system <pod> -c istio-proxy -- pilot-agent request GET stats | grep -E 'server\.(memory_allocated|memory_heap_size|memory_physical_size|total_connections|concurrency)|listener_manager\.total_listeners_active|cluster_manager\.active_clusters'

Output tipico del caso:

cluster_manager.active_clusters: 1564
listener_manager.total_listeners_active: 4
server.concurrency: 16
server.memory_allocated: 673689312
server.memory_heap_size: 720371712
server.total_connections: 512

Come si legge:

  • memory_allocated / memory_heap_size ≈ 93% → heap realmente occupata, non frammentazione tcmalloc. Se invece il rapporto fosse basso (< 60%), il problema sarebbe il rilascio pagine, non il volume di dati.
  • concurrency: 16 con cpu limit: 2 → Envoy dimensiona i worker thread sui core del nodo, non sul limit del container. Envoy duplica per-worker gran parte dello stato dei cluster.
  • 1564 cluster × 16 thread con soli 4 listener → la memoria è quasi tutta stato xDS replicato, non buffer di traffico.

Rimedio 1 — Concurrency allineata al CPU limit

È l’intervento a rischio più basso e con l’effetto maggiore. 16 → 4 taglia tipicamente il 40-60% della heap.

Terminal window
oc patch smcp basic -n istio-system --type=merge -p '{"spec":{"gateways":{"ingress":{"runtime":{"pod":{"metadata":{"annotations":{"proxy.istio.io/config":"{\"concurrency\":4}"}}}}}}}}'

Rimedio 2 — Resources coerenti

Terminal window
oc patch smcp basic -n istio-system --type=merge -p '{"spec":{"gateways":{"ingress":{"runtime":{"container":{"resources":{"requests":{"cpu":"500m","memory":"1Gi"},"limits":{"cpu":"2","memory":"4Gi"}}}}}}}}'

Prima di applicare, verificare che i nodi reggano il nuovo totale (es. 8 repliche × 4Gi = 32Gi di limit):

Terminal window
oc describe node -l node-role.kubernetes.io/infra= | grep -A6 'Allocated resources'

Applicare i due rimedi in un unico rollout: se la riduzione di concurrency non bastasse, il limit più alto fa comunque da cuscinetto.

Terminal window
oc rollout status deploy/istio-ingressgateway -n istio-system --timeout=5m

Verifica

Attendere almeno il tempo che prima portava all’OOM (nel caso di esempio ~30-60 min), poi:

Terminal window
oc exec -n istio-system $(oc get pod -n istio-system -l app=istio-ingressgateway -o name | head -1 | cut -d/ -f2) -c istio-proxy -- pilot-agent request GET stats | grep -E 'server\.(concurrency|memory_heap_size)|cluster_manager\.active_clusters'
oc adm top pod -n istio-system -l app=istio-ingressgateway
oc get pod -n istio-system -l app=istio-ingressgateway

Il restart count deve restare stabile e la heap assestarsi su un plateau invece di crescere linearmente.

Causa di fondo — ridurre i cluster

1564 cluster attivi su 4 listener indicano che il gateway riceve la configurazione di tutti i servizi del mesh, non solo di quelli che espone.

Terminal window
oc get gateway,virtualservice,destinationrule,serviceentry -A --no-headers | wc -l
oc get smmr default -n istio-system -o jsonpath='{.status.configuredMembers}{"\n"}' | tr ',' '\n' | wc -l

Se i namespace membri della SMMR sono molti più di quelli effettivamente serviti dal gateway, il problema è lo scoping del mesh, non Envoy.

La leva tecnica è su istiod:

spec:
runtime:
components:
pilot:
container:
env:
PILOT_FILTER_GATEWAY_CLUSTER_CONFIG: "true"

Limita i cluster pushati ai gateway a quelli effettivamente referenziati dai Gateway. Riduzione tipica del 60-80% su mesh grandi.

Checklist riassuntiva

  1. Restart scaglionati su tutti i replica → morte interna, non rollout.
  2. Solo eventi di readiness → escludere la liveness.
  3. lastState.terminated → confermare 137/OOMKilled.
  4. --previous vuoto → coerente con SIGKILL.
  5. resources + oc adm top → limit basso e ratio requests/limits sbagliato.
  6. server.concurrency vs cpu limit → moltiplicatore nascosto.
  7. cluster_manager.active_clusters → volume di stato xDS.
  8. Patch SMCP (concurrency + resources) in un solo rollout.
  9. Scoping del mesh come rimedio strutturale, fuori dall’incidente.