Wat OOMKilled betekent
Exit code 137 is het kenmerk. De rekensom: 128 + 9. Signal 9 is SIGKILL, het onblokkeerbare, niet-afvangbare beëindigingssignaal. Zodra het geheugengebruik van een container zijn cgroup-limiet bereikt, stuurt de OOM-killer van de Linux-kernel SIGKILL naar het proces. Geen respijt, geen kans om netjes af te sluiten.
Je ziet het in kubectl describe pod onder de terminated-status van de container:
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Mon, 07 Apr 2026 14:20:01 +0000
Finished: Mon, 07 Apr 2026 14:20:01 +0000
De volgorde is simpel. Wanneer het geheugengebruik memory.max bereikt in de cgroup van de container, probeert de kernel eerst reclaimable geheugen vrij te maken (page cache, inactieve pagina's). Lukt dat niet, dan selecteert de OOM-killer het duurste proces in de cgroup en stuurt SIGKILL.
Op clusters met Kubernetes 1.28+ en cgroup v2 staat memory.oom.group op 1. Dat betekent dat alle processen in de container tegelijk gekilld worden, zodat je geen half-dode containers overhoudt in een ongedefinieerde toestand.
Container-limit OOM vs node OOM vs eviction
Drie verschillende scenario's kunnen ervoor zorgen dat een pod verdwijnt. Van buiten lijken ze op elkaar, maar de oorzaak en de oplossing verschillen.
Container-limiet overschreden (OOMKilled). Een enkele container overschrijdt zijn resources.limits.memory-grens. De cgroup OOM-killer vuurt alleen binnen de cgroup van die container. Andere containers in de pod draaien gewoon door. De kubelet herstart de gekilelde container volgens het restartPolicy van de pod. Dit is veruit de meest voorkomende situatie.
Node-level OOM-kill. De hele node zit zonder geheugen. De globale Linux OOM-killer vuurt (niet de cgroup OOM-killer) en selecteert processen node-breed op basis van oom_score_adj-waarden. Hoe je het verschil ziet: op de node wijst grep -i "Memory cgroup out of memory" /var/log/syslog naar een container-level kill. Een generiek "Out of memory: Killed process" zonder "memcg" of "memory cgroup" wijst op een node-level kill.
Node-pressure eviction. De kubelet beëindigt proactief pods wanneer de node een geheugen-evictiondrempel nadert. Dit is niet de kernel maar de kubelet. De podstatus toont Failed met reden Evicted, niet OOMKilled. De hele pod wordt verwijderd, niet slechts een container.
Het onderscheid is belangrijk omdat de oplossing anders is. Container-level OOMKill betekent dat je limiet te laag is of dat je applicatie te veel geheugen gebruikt. Node-level OOM betekent dat de node overcommitted is. Eviction betekent dat het cluster te heet draait. Voor een diepere uitleg over hoe requests en limits samenwerken met deze scenario's, zie het artikel over resource requests en limits.
QoS-klassen en kill-prioriteit
Bij node-level geheugendruk gebruikt de kernel oom_score_adj om te bepalen welke pod als eerste gekilld wordt. Kubernetes wijst QoS-klassen toe op basis van de configuratie van requests en limits:
| QoS-klasse | Criteria | oom_score_adj | Kill-prioriteit |
|---|---|---|---|
| Guaranteed | Alle containers: request == limit (niet nul) | -997 | Laatst (meest beschermd) |
| Burstable | Minimaal een container heeft een request of limit | 2 tot 999 | Midden |
| BestEffort | Geen requests of limits op welke container dan ook | 1000 | Eerst |
Een container-level OOM-kill (eigen limiet bereikt) vuurt ongeacht de QoS-klasse. QoS bepaalt alleen wie als eerste sneuvelt bij node-level druk.
OOMKilled diagnosticeren
Begin met kubectl describe pod. De output vertelt je of de vorige container-terminatie OOMKilled was en geeft je de exit code en timestamps.
kubectl describe pod my-app-7d4f9b8c6-xvk2p -n production
Zoek naar het Last State: Terminated-blok met Reason: OOMKilled en Exit Code: 137.
Controleer daarna events voor de namespace:
kubectl get events --sort-by='.lastTimestamp' -n production
Events met reden OOMKilling of container-terminatieberichten bevestigen de kill en bevatten vaak timestamps die je kunt correleren met verkeerspieken of deployments.
Controleer het huidige geheugengebruik als de pod herstart is en nog draait (vereist metrics-server):
kubectl top pod my-app-7d4f9b8c6-xvk2p -n production
# NAME CPU(cores) MEMORY(bytes)
# my-app-7d4f9b8c6-xvk2p 245m 412Mi
Voor de logs van de vorige container (pre-kill output):
kubectl logs my-app-7d4f9b8c6-xvk2p -n production --previous
Voor live-inspectie van de cgroup-geheugenteller in de container:
kubectl exec -it my-app-7d4f9b8c6-xvk2p -n production -- cat /sys/fs/cgroup/memory.current
Prometheus-metrics voor geheugenmonitoring
Twee cAdvisor-metrics zijn belangrijk voor OOM-diagnose:
container_memory_working_set_bytes is actief geheugen dat niet makkelijk teruggewonnen kan worden (usage minus inactieve file-backed pagina's). Dit is de metric die Kubernetes vergelijkt met de geheugenlimiet. Het is ook wat kubectl top toont. Als dit getal de limiet nadert, is een OOM-kill aanstaande.
container_memory_rss is de Resident Set Size: anoniem geheugen plus swap-cache, exclusief page cache. Hoge RSS met weinig page cache betekent dat het meeste geheugen anonieme allocaties zijn die de kernel niet kan terugwinnen. Dat is een hoog-risicoprofiel voor OOM.
Geheugengebruik als percentage van de limiet:
sum(container_memory_working_set_bytes{container!=""}) by (pod, namespace)
/
sum(kube_pod_container_resource_limits{resource="memory"}) by (pod, namespace)
Pods die meer dan 80% van hun limiet gebruiken (vroege waarschuwing):
sum(container_memory_working_set_bytes{container!=""}) by (pod)
/
sum(kube_pod_container_resource_limits{resource="memory"}) by (pod) > 0.8
Alertregel voor OOMKilled-events (vereist kube-state-metrics):
- alert: PodOOMKilled
expr: kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} > 0
for: 0m
labels:
severity: critical
annotations:
summary: "Container in pod was OOMKilled"
Memory limits goed instellen
Het doel is een limiet die hoog genoeg is zodat legitieme workloads overleven, maar laag genoeg om isolatie te bieden en runaway-processen te vangen. Verzamel eerst echte data.
- Draai de workload onder realistische belasting gedurende 7 tot 30 dagen. Registreer
container_memory_working_set_bytesop p50, p95 en p99 met Prometheus. - Zet
resources.requests.memorydicht bij de p95 van de working set. Dit geeft de scheduler nauwkeurige plaatsingsdata. - Zet
resources.limits.memoryop de p99 van de working set plus 20 tot 30% marge. Dit vangt legitieme pieken op en begrenst ongecontroleerd gebruik. - Overweeg voor kritieke productie-workloads om request gelijk te zetten aan limit (Guaranteed QoS). Dit voorkomt eviction bij node-druk, ten koste van OOMKill bij elke piek boven de limiet.
# Voorbeeld: p95 = 420 MiB, p99 = 512 MiB
resources:
requests:
memory: "512Mi" # dicht bij p95 voor scheduler-nauwkeurigheid
limits:
memory: "640Mi" # p99 + ~25% marge
Geheugenlek vs te krappe limiet
Voordat je de limiet verhoogt, moet je weten of de applicatie echt meer geheugen nodig heeft.
- Te krappe limiet: geheugengebruik is stabiel maar zit constant dicht bij de limiet. De oplossing: verhoog de limiet.
- Geheugenlek: geheugengebruik groeit monotoon zonder plateau. De limiet verhogen stelt het probleem alleen uit. Profileer de applicatie en fix het lek.
- Verkeersspiek: geheugengebruik piekt tijdelijk bij hoge load. Verhoog de limiet met voldoende marge, of schaal replica's met HPA voordat geheugendruk opbouwt.
Bekijk de Prometheus-historie. Als container_memory_working_set_bytes een alleen-maar-omhoog-trend laat zien zonder plateau, is het een lek. Een zaagtandpatroon (groei, GC-dip, groei) met toenemende amplitude is ook een lek-indicator.
Wat er gebeurt zonder limits
Containers zonder memory limits krijgen BestEffort QoS. Ze worden als eerste gekilld bij node-druk, ze kunnen al het node-geheugen opeten en zo andere pods meesleuren, en ze bieden geen OOM-isolatie. Stel altijd memory limits in voor productie.
Taalspecifiek geheugengedrag
JVM (Java, Kotlin, Scala)
De meest voorkomende oorzaak van JVM OOMKilled: de containerlimiet gelijkzetten aan -Xmx. De JVM gebruikt veel meer geheugen dan alleen de heap.
| Component | Flag | Typische grootte |
|---|---|---|
| Heap | -Xmx |
Expliciet ingesteld |
| Metaspace | -XX:MaxMetaspaceSize |
250 tot 500 MB (groeit met classloading) |
| Code cache | -XX:ReservedCodeCacheSize |
240 MB standaard |
| Thread stacks | -Xss (per thread) |
~1 MiB per thread |
| Direct buffers | -XX:MaxDirectMemorySize |
Varieert |
| JVM-overhead | (geen flag) | ~100 tot 300 MB |
Een grove formule voor de containerlimiet: heap + metaspace + code cache + (threads x stackgrootte) + 300 MB overhead. Voor een 600 MB heap, 250 MB metaspace, 50 MB code cache, 100 threads van 1 MB en 300 MB overhead is dat ruwweg 1.300 MB voor een heap van 600 MB.
In plaats van -Xmx hard te coderen, gebruik -XX:MaxRAMPercentage=75.0 (JDK 8u191+ en JDK 11+) om de heap als percentage van het zichtbare containergeheugen in te stellen. Op JDK 11+ leest de JVM automatisch de cgroup-limieten. 75% is een gangbaar startpunt; dat laat 25% over voor non-heap componenten.
Begrens de non-heap regio's expliciet om verrassingen te voorkomen:
-XX:MaxMetaspaceSize=256m
-XX:ReservedCodeCacheSize=128m
-XX:MaxDirectMemorySize=128m
Een belangrijk onderscheid: OOMKilled (exit code 137) is de Linux-kernel die de container killt omdat het totale geheugen de cgroup-limiet overschreed. Een Java OutOfMemoryError is de JVM die een exception gooit omdat de heap-GC niet genoeg ruimte kan vrijmaken. De container overleeft de Java OOM-exception mogelijk als de applicatie de error afvangt. Het zijn verschillende fouten met verschillende diagnostiek.
Go
Voor Go 1.19 werden Go-applicaties regelmatig OOMKilled in containers omdat de garbage collector geen weet had van het containergeheugenplafond. De GC gebruikt een ratio-gebaseerde trigger (GOGC=100): hij draait wanneer de heap verdubbeld is ten opzichte van de vorige collectie. Met 500 MB live heap na de laatste GC vuurt de volgende collectie pas bij 1 GB, en dat kan al voorbij de containerlimiet zijn.
GOMEMLIMIT (Go 1.19+) is een soft geheugenlimiet voor de Go-runtime. Wanneer het totale Go-geheugengebruik deze waarde nadert, wordt de GC agressiever. Het is "soft" omdat Go niet garandeert eronder te blijven, maar het voorkomt de situatie dat de GC te laat vuurt.
env:
- name: GOMEMLIMIT
value: "1843MiB" # ~90% van een 2Gi containerlimiet
- name: GOGC
value: "off" # schakel ratio-gebaseerde GC uit; laat GOMEMLIMIT de collectie sturen
Stel GOMEMLIMIT in op 90 tot 95% van de containerlimiet. De overige 5 tot 10% geeft de kernel ruimte voor page cache en cgroup-accounting. Benchmarks van Ardan Labs laten zien dat GOMEMLIMIT de doorvoer zelfs kan verbeteren door de GC op een strakkere, voorspelbaardere cyclus te houden.
Node.js
Node.js is container-aware sinds v12. Zonder flags gebruikt V8 ongeveer 50% van het zichtbare containergeheugen voor de JavaScript-heap, met een plafond rond 2 GiB. Bij kleine containers (512 Mi of minder) laat deze standaard erg weinig marge over.
Overschrijf het met --max-old-space-size (in MB):
env:
- name: NODE_OPTIONS
value: "--max-old-space-size=512" # voor een 1 Gi container: ~50% voor heap
Stel --max-old-space-size in op 50 tot 70% van de containerlimiet. Zet het nooit gelijk aan de limiet. Node.js heeft geheugen nodig buiten de V8-heap voor native C++-addons, Buffer-allocaties (die buiten V8 leven), en OS-overhead.
De Buffer-valkuil is het vermelden waard: applicaties die veel streamen of aan beeldverwerking doen, alloceren grote Buffer-objecten die buiten V8's heap-accounting vallen. Je kunt --max-old-space-size correct ingesteld hebben en toch OOMKillen omdat Buffer-geheugen het totale containergebruik over de limiet duwt.
Python
Python heeft geen container-awareness. CPython leest geen cgroup-limieten en heeft geen equivalent van MaxRAMPercentage of GOMEMLIMIT. Het laat zijn heap gewoon groeien tot de cgroup-limiet het proces killt.
Veelvoorkomende oorzaken van Python OOMKilled in containers:
- Langdraaiende async-services (aiohttp, FastAPI met asyncio) die geheugen ophopen door ongesloten verbindingen en event-loop-referenties.
- Grote datasets in geheugen laden (pandas DataFrames, numpy arrays) zonder chunking.
- C-extensiebibliotheken (numpy, scipy, Pillow) die geheugen alloceren via
mallocbuiten Python's heap, onzichtbaar voor Python's GC.
Voor profiling: tracemalloc (stdlib) volgt Python-heap-allocaties. Voor productieomgevingen is memray (van Bloomberg) completer omdat het zowel Python-heap als C-level allocaties bijhoudt.
VPA voor geautomatiseerd right-sizing
De Vertical Pod Autoscaler (VPA) past resource-requests en -limits automatisch aan op basis van historisch gebruik. Het is een aparte CRD-gebaseerde installatie die metrics-server vereist.
VPA bestaat uit drie componenten. De Recommender analyseert continu historisch resourcegebruik, inclusief OOM-events, en berekent target-, lower-bound- en upper-bound-geheugenrecommandaties. De Updater vergelijkt huidige podresources met recommendaties en verwijdert pods wanneer het verschil een drempel overschrijdt (of past in-place updates toe op Kubernetes 1.27+). De Admission Controller onderschept pod-creatie en injecteert de aanbevolen waarden zodat nieuwe pods meteen goed gedimensioneerd starten.
Begin met updateMode: Off om recommendaties te bekijken zonder automatische wijzigingen:
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: my-app-vpa
spec:
targetRef:
apiVersion: "apps/v1"
kind: Deployment
name: my-app
updatePolicy:
updateMode: "Off"
resourcePolicy:
containerPolicies:
- containerName: "*"
minAllowed:
memory: "128Mi"
maxAllowed:
memory: "4Gi"
controlledValues: RequestsAndLimits
Bekijk wat VPA aanbeveelt:
kubectl describe vpa my-app-vpa
# Recommendation:
# Container Recommendations:
# Container Name: my-app
# Lower Bound:
# Memory: 312Mi
# Target:
# Memory: 420Mi
# Upper Bound:
# Memory: 1Gi
De VPA Recommender verwerkt OOMKilled-events actief in zijn berekeningen en verhoogt doorgaans de upper bound na een OOM-event. Dat maakt VPA zowel een right-sizing tool als een automatisch responsmechanisme.
| Modus | Gedrag | Toepassing |
|---|---|---|
Off |
Berekent alleen recommendaties | Auditing, eerste deployment |
Initial |
Past recommendaties alleen toe bij pod-creatie | Veilig startpunt |
Recreate |
Verwijdert pods wanneer recommendaties significant afwijken | Actief right-sizing |
InPlaceOrRecreate |
In-place update eerst, evict als fallback (Kubernetes 1.27+ beta) | Minimale verstoring |
Beperkingen om rekening mee te houden: VPA kan niet dezelfde resource (memory) delen met HPA op dezelfde workload. Recreate-modus veroorzaakt podverstoringen, dus gebruik PodDisruptionBudgets om de blast radius te beheersen. Recommendaties hebben minstens enkele uren tot dagen aan historische data nodig voordat ze stabiliseren.
Wanneer escaleren
Als je de diagnosestappen hebt doorlopen en de OOMKilled-events aanhouden, verzamel dan deze informatie voordat je hulp inschakelt:
- Output van
kubectl describe pod <pod-name> -n <namespace>(volledige output, niet alleen de status) - Output van
kubectl top podenkubectl top nodeop het moment van de kill - Prometheus-grafiek van
container_memory_working_set_bytesvoor de pod over de afgelopen 24 uur - Applicatieframework en versie (JVM-versie, Go-versie, Node.js-versie, Python-versie)
- Eventuele geheugen-gerelateerde flags in de containerspec of het entrypoint (
-Xmx,GOMEMLIMIT,--max-old-space-size) - De resource-requests en -limits YAML van de pod
- Of de pod een enkel proces draait of subprocessen start
Herhaling voorkomen
- Stel memory limits in op elke productiecontainer. BestEffort QoS is een recept voor onvoorspelbare kills.
- Gebruik taalspecifieke geheugenflags (
MaxRAMPercentage,GOMEMLIMIT,--max-old-space-size) om applicatiegeheugen ruim onder de containerlimiet te houden. - Deploy VPA in
Off-modus en bekijk de recommendaties als onderdeel van reguliere capaciteitsreviews. - Stel een Prometheus-alert in op
container_memory_working_set_bytesdie 80% van de limiet overschrijdt. De trend zien voordat hij 100% bereikt is het verschil tussen een configwijziging en een incident. - Als een CrashLoopBackOff veroorzaakt wordt door herhaalde OOMKills, ligt de oplossing hier (memory limits), niet in het restart-beleid.