Kubernetes OOM Killer: Prečo Kontajner Zomiera pri 50% Pamäte
OOMKilled je jedna z tych sprav, ktora po polnoci posobi osobne. “Náš kontajner má 4GB memory limit ale dostane OOMKill pri 2GB RSS.” Počul som túto sťažnosť od nespočetných tímov a zmätok je úplne pochopiteľný. Pozriete sa na top alebo váš monitoring dashboard, vidíte 2GB využitie pamäte a potom—bum—OOMKilled. Kam zmizli tie ďalšie 2GB? Klamú metriky?
Metriky neklamú. Jednoducho vám nehovoria celý príbeh. Container memory accounting v Linuxe zahŕňa oveľa viac ako to, čo vaša aplikácia alokuje. Kernel sleduje page cache, socket buffers, kernel dátové štruktúry a rôzny iný overhead—všetko sa počíta do memory limitu vášho kontajnera. Pochopenie tejto skrytej pamäte je kľúč k nastaveniu limitov, ktoré nespôsobia neočakávané OOMKills.
Prvýkrát som na tento problém narazil so službou na spracovanie dát, ktorá čítala veľké súbory z S3. Samotná aplikácia používala asi 1.5GB heap, ale stále bola zabíjaná s 4GB limitom. Vinník bol užitočný zvyk Linuxu cachovať file data v pamäti. Každý súbor, ktorý sme prečítali, zostal v page cache, ticho konzumujúc pamäť kým sme nenarazili na cgroup limit.
Testované na: Kubernetes 1.28, cgroup v2, Java a Go aplikácie
Pochopenie Container Memory Accounting
Rozoberme si presne čo sa počíta do memory limitu vášho kontajnera. Tieto znalosti sú esenciálne pre nastavenie správnych limitov a debugovanie OOMKill problémov.
Čo Sa Počíta Do Memory Limitu
Keď nastavíte memory limit na kontajner, Linux cgroup controller sleduje viaceré kategórie využitia pamäte:
Container memory cgroup obsahuje:
1. RSS (Resident Set Size)
- Heap vašej aplikácie
- Stack memory
- Namapované súbory (skutočne v RAM)
2. Page Cache
- Čítania súborov cachované kernelom
- Dá sa uvoľniť pod tlakom
3. Kernel Memory (kmem)
- Socket buffers
- Dentry cache
- inode cache
4. Swap (ak povolený)
Total cgroup memory = RSS + Cache + Kmem
┌─────────────────────────────────────────────────────────────┐
│ Container limit: 4GB │
│ │
│ Application RSS: 2.0GB ← Čo vidíte v top │
│ Page Cache: 1.5GB ← Cachované čítania súborov │
│ Kernel Buffers: 0.6GB ← Socket buffers, atď │
│ ───────────────────────────── │
│ Total cgroup usage: 4.1GB ← PREKRAČUJE LIMIT = OOMKill! │
└─────────────────────────────────────────────────────────────┘
Kritický vhľad je, že heap vašej aplikácie (čo väčšina monitoringu ukazuje ako “memory usage”) je často len zlomok celkovej cgroup spotreby pamäte. Zvyšok je overhead, ktorý je neviditeľný pokiaľ neviete kde hľadať.
Problém Page Cache
Linux agresívne cachuje file data v pamäti. Keď čítate súbor, kernel drží tieto dáta v RAM pre prípad, že ich budete znova potrebovať. Toto je zvyčajne skvelé pre performance—opakovaný prístup k súborom je oveľa rýchlejší.
Problém v kontajneroch je, že tieto cachované dáta sa počítajú do vášho memory limitu. Ak vaša aplikácia prečíta 2GB súborov počas spracovania, tie 2GB sedia v page cache aj potom čo s nimi aplikácia skončila. Page cache je “reclaimable”—kernel ju môže vyhodiť pod memory pressure—ale kým sa pressure vybuduje, možno ste už na limite.
Bežní Vinníci
Niekoľko vzorcov bežne spúšťa neočakávané OOMKills:
# 1. Vysoké file I/O (explózia page cache)
# Čítanie množstva súborov plní page cache
cat /sys/fs/cgroup/memory.stat | grep -E "^(file|anon)"
# 2. Veľa network spojení (socket buffers)
ss -s # Počet socketov
cat /proc/net/sockstat # Socket memory
# 3. JVM off-heap memory
# DirectByteBuffers, memory-mapped files
# Nepočítajú sa do -Xmx
# 4. Native memory v language runtimes
# Go GC overhead, Python object overhead
Rozveďme každý bod:
File I/O vzorce: ETL joby, log procesory a data pipelines, ktoré čítajú množstvo súborov, sú primárni kandidáti. Každé čítanie súboru pridáva do page cache. Aj keď zatvoríte file handle, dáta zostávajú cachované.
Network spojenia: Každý TCP socket má send a receive buffers. S defaultnými kernel nastaveniami je to asi 200KB na socket. Servis s 1000 spojeniami môže mať 200MB len v socket bufferoch—pamäť, ktorá sa neobjaví v heape vašej aplikácie.
JVM off-heap: Java -Xmx kontroluje len veľkosť heap. DirectByteBuffer alokácie (používané NIO, Netty, atď.), Metaspace, code cache a thread stacks všetky konzumujú dodatočnú pamäť mimo heap.
Native memory: Každý language runtime má overhead. Go garbage collector potrebuje priestor na efektívnu prácu. Python object system má signifikantný per-object overhead. C extensions v Pythone alebo Ruby môžu alokovať pamäť, ktorú interpreter nesleduje.
Diagnostika OOMKills
Keď kontajner dostane OOMKill, potrebujete pochopiť čo skutočne skonzumovalo pamäť. Tu je ako to vyšetrovať:
Zisti Čo To Skutočne Zabilo
# Nájdi OOMKill eventy
kubectl describe pod <pod-name> | grep -A5 "Last State"
# Získaj detailné cgroup stats pred smrťou
kubectl exec <pod> -- cat /sys/fs/cgroup/memory.stat
# Kľúčové metriky:
# anon: Anonymous memory (heap, stack)
# file: Page cache
# kernel: Kernel data structures
# sock: Socket buffers
Súbor memory.stat je váš najlepší priateľ pre pochopenie rozloženia pamäte. Na rozdiel od high-level metrík, ktoré často ukazujú len RSS, tento súbor rozkladá presne kam pamäť ide.
Rozklad Memory Stat
Ukážem vám ako čítať tento súbor:
# Vnútri kontajnera
cat /sys/fs/cgroup/memory.stat
# Interpretácia výstupu:
anon 1073741824 # 1GB - skutočná app memory
file 1610612736 # 1.5GB - page cache (reclaimable)
kernel 104857600 # 100MB - kernel structures
sock 52428800 # 50MB - socket buffers
shmem 0 # 0 - shared memory
# ... viac polí
# memory.current ukazuje total:
cat /sys/fs/cgroup/memory.current
# 2841640960 (2.6GB celkom)
V tomto príklade aplikácia používa 1GB skutočnej pamäte (anon), ale celkové cgroup využitie je 2.6GB kvôli page cache a kernel overheadu. Ak by tento kontajner mal 2.5GB limit, dostal by OOMKill napriek tomu že aplikácia “používa” len 1GB.
Prečo Sa Page Cache Neuvoľnil?
Možno sa pýtate: ak je page cache reclaimable, prečo triggeruje OOMKill? Odpoveď zahŕňa timing a reclaim správanie kernela.
Kernel začína uvoľňovať page cache keď sa buduje memory pressure. Ale tento reclaim proces trvá čas. Ak vaša aplikácia náhle alokuje veľký kus pamäte (ako spracovanie veľkého requestu), môže to pretlačiť cgroup usage cez limit predtým ako reclaim stihne uvoľniť dosť cache. OOM killer triggeruje okamžite keď je limit prekročený.
Navyše, niektoré cachované stránky môžu byť “dirty” (modifikované ale ešte nezapísané na disk). Dirty stránky nemôžu byť okamžite uvoľnené—najprv musia byť zapísané. Write-heavy workload môže mať signifikantný dirty page cache, ktorý nie je rýchlo reclaimable.
Riešenia
Teraz keď rozumieme problému, opravme ho.
1. Nastav Realistické Limity (Započítaj Overhead)
Najbežnejšia oprava je jednoducho nastaviť vyššie limity, ktoré započítavajú realitu container memory accountingu:
# deployment.yaml
resources:
requests:
memory: "2Gi" # Čo app typicky používa
limits:
memory: "4Gi" # App + cache + buffers overhead
# Pravidlo:
# limit = expected_rss × 1.5 až 2.0
# Uprav podľa I/O patterns
Pre I/O-heavy workloads (ETL, data processing, file serving) použite 2x multiplikátor alebo vyšší. Pre memory-bound workloads s málo I/O môže stačiť 1.3-1.5x.
Request by mal reflektovať typické využitie pamäte. Limit by mal započítavať worst-case scenáre vrátane cache a kernel overheadu.
2. Správne Nastav JVM Memory
JVM aplikácie sú obzvlášť zložité pretože Java má viacero pamäťových regiónov a -Xmx kontroluje len jeden z nich:
# Pre JVM aplikácie
env:
- name: JAVA_OPTS
value: >-
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-XX:+ExitOnOutOfMemoryError
-XX:NativeMemoryTracking=summary
# MaxRAMPercentage=75% necháva miesto pre:
# - Metaspace
# - Code cache
# - Direct buffers
# - Thread stacks
# - Kernel overhead
Pravidlo 75% je dôležité. S 4GB container limitom nastavenie -Xmx4g je recept na OOMKills. Heap sa možno vojde, ale Metaspace, code cache, direct buffers, thread stacks a kernel overhead vás pretlačia cez. Použite 75% (3GB heap pre 4GB limit) aby ste nechali miesto pre všetko ostatné.
UseContainerSupport (default od JDK 10) hovorí JVM aby čítalo cgroup limity namiesto host memory. Vždy overte že to funguje—niektoré container konfigurácie (ako používanie host PID namespace) môžu zmiasť JVM.
NativeMemoryTracking vám umožňuje vidieť kam ide JVM memory mimo heap. Použite jcmd <pid> VM.native_memory summary pre rozklad.
3. Limituj Rast Page Cache
Pre workloads, ktoré čítajú množstvo súborov, môžete podniknúť kroky na redukciu akumulácie page cache:
# Pre I/O heavy workloads, použi memory.high
# Toto triggeruje reclaim pred dosiahnutím limitu
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
resources:
limits:
memory: "4Gi"
# Alebo v aplikácii - použi O_DIRECT pre veľké súbory
# Obchádza page cache
dd if=/dev/zero of=test bs=1M count=1000 oflag=direct
O_DIRECT kompletne obchádza page cache pre I/O operácie. Toto je užitočné pre aplikácie, ktoré čítajú veľké súbory sekvenčne a nebudú benefitovať z cachovania. Database engines často používajú O_DIRECT pre dátové súbory pretože spravujú vlastné cachovanie.
Ďalší prístup je periodické sync; echo 3 > /proc/sys/vm/drop_caches na vynútenie dropov cache. Toto je hrubé ale efektívne pre batch joby, ktoré spracovávajú súbory vo fázach. Poznámka: toto ovplyvňuje všetky kontajnery na node, takže to používajte opatrne.
4. Monitoruj Komponenty Memory
Proaktívny monitoring predchádza prekvapeniam. Sledujte nie len celkovú pamäť ale jej rozklad:
# Python - sleduj rozloženie memory
import os
def get_cgroup_memory():
with open('/sys/fs/cgroup/memory.stat') as f:
stats = {}
for line in f:
key, value = line.strip().split(' ')
stats[key] = int(value)
return stats
def get_memory_breakdown():
stats = get_cgroup_memory()
return {
'anon_gb': stats.get('anon', 0) / (1024**3),
'file_gb': stats.get('file', 0) / (1024**3),
'kernel_gb': stats.get('kernel', 0) / (1024**3),
'sock_gb': stats.get('sock', 0) / (1024**3),
}
Exportujte tieto ako Prometheus metriky aby ste videli trendy v čase. Postupne rastúci page cache môže indikovať leak alebo akumulujúci sa stav, ktorý nakoniec spôsobí problémy.
5. Použi memory.high pre Soft Limity
cgroup v2 zaviedol memory.high, soft limit ktorý triggeruje agresívny reclaim bez zabíjania kontajnera:
# V cgroup v2, memory.high triggeruje reclaim
# Nastav cez Kubernetes memory request/limit gap
# Alebo priamo:
echo $((3 * 1024 * 1024 * 1024)) > /sys/fs/cgroup/memory.high
# memory.high = soft limit (triggeruje reclaim)
# memory.max = hard limit (triggeruje OOMKill)
Keď využitie prekročí memory.high, kernel agresívne uvoľňuje pamäť, spomaľujúc aplikáciu ale nezabíjajúc ju. Toto dáva page cache a inej reclaimable memory čas na uvoľnenie pred dosiahnutím hard limitu.
V Kubernetes medzera medzi requests.memory a limits.memory ovplyvňuje ako agresívne kernel uvoľňuje pamäť. Väčšia medzera znamená viac priestoru pred tým ako sa spustí agresívny reclaim.
Monitoring
Nastavte alerty aby ste zachytili memory problémy predtým ako spôsobia OOMKills:
# Prometheus alert - blížiaci sa k memory limitu
- alert: ContainerMemoryNearLimit
expr: |
container_memory_working_set_bytes /
container_spec_memory_limit_bytes > 0.85
for: 5m
annotations:
summary: "Container {{ $labels.container }} na 85%+ memory"
# Alert na rast page cache
- alert: HighPageCacheUsage
expr: |
(container_memory_cache / container_memory_working_set_bytes) > 0.5
for: 15m
annotations:
summary: "Page cache > 50% working set"
# Alert na OOMKills
- alert: ContainerOOMKilled
expr: |
increase(kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}[5m]) > 0
annotations:
summary: "Container {{ $labels.container }} bol OOMKilled"
Threshold 85% vám dáva čas vyšetrovať pred dosiahnutím 100%. Alert na page cache zachytáva kontajnery kde cachované dáta dominujú využitiu pamäte—často znak že limity treba upraviť alebo I/O patterns aplikácie treba preskúmať.
Checklist
## Container Memory Konfigurácia
### Sizing
- [ ] Započítaj page cache (1.5-2x RSS)
- [ ] Pridaj buffer pre kernel memory (10-20%)
- [ ] Testuj pod realistickým I/O loadom
### JVM Špecifické
- [ ] Použi -XX:+UseContainerSupport
- [ ] Nastav MaxRAMPercentage na max 75%
- [ ] Povoľ NativeMemoryTracking pre debugging
### Monitoring
- [ ] Alert na memory > 85% limitu
- [ ] Sleduj anon vs file memory ratio
- [ ] Monitoruj OOMKill eventy
### Debugging
- [ ] Skontroluj /sys/fs/cgroup/memory.stat pri OOM
- [ ] Over čo je v page cache
- [ ] Profiluj native memory usage
Záver
Container memory je oveľa viac ako to, čo vaša aplikácia alokuje. Memory accounting kernela zahŕňa cache, buffers a overhead, ktorý väčšina monitoring nástrojov prominentne nezobrazuje. Pochopenie tejto medzery medzi “application memory” a “cgroup memory” je esenciálne pre spoľahlivú prevádzku kontajnerov.
Kľúčové poznatky:
- Page cache a kernel buffers sa počítajú do vášho limitu—nie sú to “free” memory
- Nastavte limity 1.5-2x očakávaný RSS aby ste započítali overhead, hlavne pre I/O workloads
- JVM aplikácie potrebujú 75% MaxRAMPercentage maximum—ďalších 25% ide na non-heap memory
- Monitorujte všetky komponenty memory—RSS samotný nehovorí celý príbeh
- Kontrolujte memory.stat pri debugovaní OOMKills—odhaľuje kam pamäť skutočne išla
Pred nastavením memory limitov spustite vašu aplikáciu pod realistickým loadom a skontrolujte /sys/fs/cgroup/memory.stat. Čísla tam vám povedia aké limity skutočne potrebujete.
Súvisiace články
- Container Page Cache Thrashing - Detaily memory pressure
- Go GOMAXPROCS v Kontajneroch - Detekcia container resources
- Java Native Memory OOMKilled - JVM memory mimo heap
Súvisiace články
'No space left on device' s 40% voľného disku: Inode a OverlayFS Death Spiral
df -h ukazuje 40% voľného miesta. Ale váš kontajner stále padá s ENOSPC. Vinník: vyčerpanie inodov na overlayfs vrstvách, neviditeľné pre štandardný monitoring.
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.
Java OOMKilled So Stabilným Heapom: Native Memory, Direct Buffers a glibc Arenas
Heap metriky vyzerajú dobre, GC je spokojný, ale kontajner stále umiera. Vinník: native memory z direct buffers, JNI a glibc memory allocator fragmentácia.
Pakety prichadzaju ale aplikacia timeoutuje: rp_filter pasca v Kubernetes
tcpdump ukazuje pakety ktore prichadzaju, ale aplikacia nic nevidi. Vinik: Linux reverse path filtering ticho zahadzuje pakety predtym nez dosiahnu iptables, sposobene asymetrickym routovanim.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.