Kubernetes p99 špičky bez OOM: Diagnostika cgroup v2 memory.high cez PSI
p99 skočí z 80ms na 3s. CPU je v pohode. RSS je pod limitom. Nikto nedostal OOMKilled. A predsa je služba v krátkych vlnách “zaseknutá”.
Toto je typický cgroup v2 problém: ste throttlení pamäťovým tlakom, nie zabití pamäťovým limitom. Najčastejšie ide o kombináciu:
memory.high(soft limit / backpressure)- reclaim stalls (direct reclaim / refault stormy)
- PSI (Pressure Stall Information), ktoré takmer nikto negrafuje
Ak pozeráte iba RSS a OOMKilly, miniete root cause.
Testované na: Kubernetes 1.29–1.31, containerd 1.7, Linux 6.1–6.6 (cgroup v2), systemd 252+.
Prečo to v roku 2026 bolí viac
cgroup v2 je default na moderných distro. Platform tímy navyše často zapnú “skorý backpressure” (systemd slices alebo kubelet/runtime QoS), aby chránili nody pred tvrdým OOM. Výsledok: viac incidentov, kde latencia padá bez jedného OOMKillu.
Ak máte SLO na latenciu, memory.high je rovnako “reálny limit” ako CPU throttling.
Incident (anonymizovaný)
Máme latency‑senzitívne API (Go) v multi‑tenant Kubernetes clustri. Po refreshi node poolu začali padať alerty:
- p99 > 2s v burstoch 30–90s
- 5xx nízke, ale klientské timeouty stúpli
- node CPU < 50%, pod RSS ~70% limitu
- žiadne restarty, žiadne OOMKilly, žiadny očividný GC spike
Blast radius: ~20% trafficu (requesty na pody schedulované na podmnožinu nodov).
Constraint: nemohli sme len “pridať pamäť nodom”; kapacita bola napnutá a chceli sme presnú opravu.
Timeline
- T-0: alert na p99 latenciu; dashboardy ukazujú CPU ok, pamäť “ok”.
- T+5m: app metriky ukazujú handler time stabilný; rastie queue time (thready stoja).
- T+15m: v pode
memory.eventsrýchlo zvyšujehigh. - T+25m:
memory.pressureukazuje výrazný stall time počas špičky. - T+35m: na postihnutých nodoch
/proc/pressure/memoryskáče presne s latenciou. - T+50m: mitigácia: prehodiť workload do Guaranteed QoS (request==limit) + mierne zvýšiť limit.
- T+90m: p99 sa stabilizuje; PSI padá;
highprestane rásť pri steady trafficu.
Mechanizmus: čo sa naozaj stalo
memory.max vás zabije. memory.high vás spomalí.
V cgroup v2 máte dve “čiary”:
memory.max: hard strop; po prekročení príde OOM (v cgroup alebo globálne).memory.high: soft strop; po prekročení kernel spustí reclaim a môže throttliť alokácie.
Keď cgroup prekročí memory.high, kernel začne reclaimovať a vie spomaliť alokácie. Proces neumrie; len trávi čas v reclaim pathoch.
Prečo latencia exploduje a CPU vyzerá v pohode
Zaseknutý thread často nevyzerá ako “CPU problém”, lebo je blokovaný na:
- reclaim práci
- IO vyvolanom refaultami (podľa storage a cache patternu)
- allocator stalloch
Ten čas je reálna latencia, ale nie nutne “busy CPU”.
PSI je truth serum
PSI meria “koľko času boli tasky zaseknuté kvôli tlaku” (memory/CPU/IO). V tomto incidente PSI ukázalo koreláciu okamžite: keď p99 vyskočilo, memory PSI vyskočilo.
Runbook: potvrdenie memory.high reclaim stallov
Čo skontrolovať ako prvé
- Používate cgroup v2?
V pode:
stat -fc %T /sys/fs/cgroup
# cgroup2fs == v2
- Je
memory.highnastavené (nie “max”)?
cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/memory.high
cat /sys/fs/cgroup/memory.current
Ak je memory.high nižšie ako memory.max, pod môže byť throttlený skôr, než dostane OOMKilled.
- Kernel zaznamenal throttling eventy?
cat /sys/fs/cgroup/memory.events
Hľadajte, či high rastie počas incidentu.
Ako potvrdiť hypotézu
A. PSI na úrovni podu:
cat /sys/fs/cgroup/memory.pressure
Uvidíte napr.:
some avg10=0.25 avg60=0.12 avg300=0.05 total=123456789
full avg10=0.03 avg60=0.01 avg300=0.00 total=987654
Interpretácia:
some= % času, keď aspoň jeden task stál kvôli memory tlakufull= % času, keď stáli všetky non-idle tasky (prakticky “všetko je stuck”)
Pre latency‑senzitívne služby je už pár % some problém. Sustained full je typicky katastrofa.
B. Korelácia s latenciou (rýchly loop):
while true; do
date
cat /sys/fs/cgroup/memory.current
cat /sys/fs/cgroup/memory.events
cat /sys/fs/cgroup/memory.pressure
echo
sleep 5
done
Hľadáte:
memory.currentblízko/nadmemory.highhighcounter rastie- PSI
some/fullrastie - p99 rastie v tom istom čase
C. Node-level PSI (globálny tlak):
Na node (SSH alebo kubectl debug node/...):
cat /proc/pressure/memory
cat /proc/pressure/io
Ak skáče aj node-level PSI, máte problém s headroomom nody (kube-reserved/system-reserved príliš malé) plus podový memory.high.
Bezpečné mitigácie
- Presuňte kritický workload do Guaranteed QoS (request == limit).
- Mierne zvýšte memory limit, ak steady-state + normálne špičky idú príliš blízko k backpressure.
- Dočasne znížte alokačný rate / fan-out:
- znížiť concurrency
- cap na in-flight requesty
- znížiť per-request buffering
- Drainnite pár “hot” nodov, ak problém vidíte iba na časti fleet.
Rizikové mitigácie (môžu ublížiť clustru)
- Globálne vypnutie
memory.high(napr. nakubepods.slice) bez pochopenia dôvodu. - Drop caches (
echo 3 > /proc/sys/vm/drop_caches) – často zhorší refault/IO. - Restarty podov ako “fix” – maskujú mechanizmus a vedia vyvolať connection storm.
Čo sme zmenili (konkrétne)
1) Guaranteed QoS + reálny headroom
Predtým (Burstable, tesné):
resources:
requests:
memory: "512Mi"
limits:
memory: "768Mi"
Potom (Guaranteed + headroom):
resources:
requests:
memory: "1Gi"
limits:
memory: "1Gi"
2) Odstránenie náhodného MemoryHigh clampu na node
Našli sme systemd drop-in, ktorý aplikoval MemoryHigh na kubepods.slice príliš agresívne.
Diff (ilustratívny):
# /etc/systemd/system/kubepods.slice.d/10-memoryhigh.conf
[Slice]
-MemoryHigh=85%
+MemoryHigh=infinity
Node ochranu sme nechali primárne na Kubernetes eviction thresholdoch.
3) PSI a high eventy ako explicitný alert/runbook signál
Pridali sme guardrails pre:
- rate
higheventov (ak to scrapujete) - node-level PSI (minimálne ako on-call diagnostiku)
Ako verifikovať (merateľné signály)
memory.eventsprestane rýchlo zvyšovaťhigh:
watch -n 2 'cat /sys/fs/cgroup/memory.events'
- PSI ostane nízke počas steady trafficu:
watch -n 2 'cat /sys/fs/cgroup/memory.pressure'
- Latencia sa zotaví bez restartov:
- p99 späť na baseline
- PSI bez špičiek
- Node-level headroom drží:
/proc/pressure/memorynešpičkuje vo veľkom- menej eviction eventov v kubelet logoch
Prevencia / guardrails
Kontrakty (čo vynucujeme)
- Latency‑kritické služby musia byť Guaranteed (alebo explicitne schválené ako Burstable).
- Memory headroom budget:
- držať
memory.current< ~70–80% zmemory.maxpri peak loade (service-specific)
- držať
- PSI budget:
- sustained
full~0 - sustained
somev nízkych jednotkách %
- sustained
Alerty, ktoré stoja za to
- node memory PSI
some avg10nad thresholdom X minút highevent rate > 0 pre kritické pody- zvýšený refault/major fault rate (ak máte dostupné metriky)
Súvisiace čítanie
- Kubernetes OOM Killer: Prečo Kontajner Zomiera pri 50% Pamäte
- Linux Page Cache Thrashing v Kontajneroch: Keď Voľná Pamäť Nie Je Voľná
- Kubernetes CPU Throttling Pitva: Prečo p99 Latencia Exploduje pri 40% CPU Usage
- RSS Contracts: Ako prestat zabijat Java pody v Kubernetes (OOMKilled) testovanim RSS ako API
- eBPF Off-CPU Analýza: Nájdenie Latencie Ktorú Metriky Nevidia
- Java OOMKilled So Stabilným Heapom: Native Memory, Direct Buffers a glibc Arenas
- Adaptive Concurrency Limits: Prestaňte Hádať Veľkosti Thread Poolov
Súvisiace články
tcpdump vidí SYN, ale služba timeoutuje: pasca listen backlogu
Klienti timeoutujú, tcpdump ukazuje SYN (niekedy aj SYN-ACK), ale aplikácia nič neloguje. Častý vinník: Linux listen/accept fronty, ktoré sa pri load-e alebo CPU starvation preplnia.
Redis AOF fsync latency špičky: keď sa durabilita stane tvojím p99
Redis AOF vie spraviť z durability p99 špičky: fsync tlak a BGREWRITEAOF fork CoW. Runbook na dôkaz, bezpečné mitigácie a guardrails.
Linux Page Cache Thrashing v Kontajneroch: Keď Voľná Pamäť Nie Je Voľná
Váš kontajner má 2GB voľné ale beží pomaly. Page cache sa počíta proti memory limitu. File I/O vytláča code pages. Vysvetlím s benchmarkmi a riešeniami.
Python GIL a Kubernetes CPU Limity: Pasca Threadingu
Vaša Python appka má 4 thready ale K8s dáva 1 CPU. GIL + CFS kvóta = brutálny throttling. Ukážem prečo a ako správne nastaviť workery.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.