Ein OOM Kill (OOMKilled) ist das erzwungene Beenden eines Prozesses durch den Out-of-Memory-Killer von Linux, wenn dem System oder einer cgroup der Speicher ausgeht. In Kubernetes passiert das, wenn ein Container sein Memory-Limit überschreitet: Er wird per SIGKILL gestoppt und meldet Exit-Code 137 mit dem Grund OOMKilled.
Was ist ein OOM Kill?
Ein OOM Kill tritt ein, wenn Linux eine Speicheranforderung nicht erfüllen kann und Speicher gewaltsam freigeben muss. Der Out-of-Memory-Killer des Kernels wählt einen Prozess aus und sendet ihm SIGKILL, ein Signal, das sich weder abfangen noch behandeln lässt. Der Prozess endet sofort, ohne Shutdown-Hooks, ohne Puffer zu leeren und ohne laufende Requests abzuschließen.
In Containern wird Speicher über cgroups begrenzt. Erreicht ein Container sein Limit (memory.max in cgroup v2) und kann der Kernel nicht genug zurückgewinnen, greift der OOM-Killer nur innerhalb dieser cgroup. Kubernetes meldet den Container-Status dann als OOMKilled mit Exit-Code 137, also 128 plus Signalnummer 9.
So wählt der OOM-Killer sein Opfer
Bei systemweitem Speichermangel berechnet der Kernel für jeden Prozess einen oom_score, vor allem anhand des Speicherverbrauchs, korrigiert um oom_score_adj (von -1000 bis 1000). Der Prozess mit dem höchsten Wert wird beendet. Kubernetes setzt oom_score_adj abhängig von der Quality-of-Service-Klasse des Pods:
| QoS-Klasse | Bedingung | OOM-Priorität |
|---|---|---|
| Guaranteed | Requests gleich Limits für CPU und Speicher in jedem Container | Wird zuletzt beendet |
| Burstable | Mindestens ein Request oder Limit gesetzt, aber nicht Guaranteed | Dazwischen, abhängig von den Requests |
| BestEffort | Weder Requests noch Limits | Wird zuerst beendet |
Ein OOM Kill wegen des eigenen Container-Limits ist etwas anderes als eine Eviction durch das Kubelet. Dabei steht der Node unter Speicherdruck und das Kubelet entfernt Pods vorsorglich. Beides endet in einem Neustart oder einer Neuplanung, erscheint aber mit unterschiedlichen Gründen und erfordert unterschiedliche Maßnahmen.
Typische Ursachen
- Zu niedriges Limit für das tatsächliche Working Set, oft nachdem Traffic oder Datenvolumen gewachsen sind.
- Ein Speicherleck, das den Verbrauch langsam an das Limit heranschiebt.
- Runtime kennt das Container-Limit nicht. Ein JVM-Heap größer als das Limit oder Heap, Metaspace, Thread-Stacks und Direct Buffers, die zusammen das Limit übersteigen.
- Lastspitzen mit vielen gleichzeitigen Requests, die jeweils große Puffer halten.
- Page Cache und tmpfs, die gegen die cgroup zählen, etwa Dateien auf einem In-Memory-Volume.
Was gegen das Memory-Limit zählt
Viele OOM Kills überraschen, weil Teams das Limit mit der falschen Zahl vergleichen. Der Kernel rechnet einer cgroup anonymen Speicher (Heap, Stacks, native Allokationen) zu, außerdem den Page Cache aus Lese- und Schreibzugriffen auf Dateien sowie Kernel-Speicher wie Socket-Puffer. Rückgewinnbarer Page Cache wird unter Druck meist freigegeben. Kubernetes und das Kubelet nutzen daher das Working Set als maßgebliche Größe, grob der Gesamtverbrauch abzüglich inaktiven Datei-Caches. Deshalb sagt container_memory_working_set_bytes einen OOM Kill besser voraus als container_memory_usage_bytes.
Bei verwalteten Runtimes ist der Heap nur ein Teil des Bildes. Eine JVM belegt zusätzlich Metaspace, Code Cache, Thread-Stacks, Strukturen des Garbage Collectors und Direct Buffers. Ein Node.js-Prozess hält Puffer außerhalb des V8-Heaps. Eine Heap-Einstellung, die sicher wirkt, kann den Container also trotzdem über sein Limit treiben. Wer Heap-Metriken und Container-Working-Set nebeneinander beobachtet, vermeidet diesen blinden Fleck und kann ein langsames Leck von einem Dimensionierungsproblem unterscheiden.
Beispiel: OOMKilled in Kubernetes analysieren
Eine illustrative Prüfung eines neu startenden Pods:
kubectl describe pod checkout-7d9f
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Limits: memory: 512Mi
Eine PromQL-Abfrage wie max_over_time(container_memory_working_set_bytes{container="checkout"}[1h]) / on(pod) group_left kube_pod_container_resource_limits{resource="memory", container="checkout"} zeigt, wie nah der Container seinem Limit kommt. Steigt das Working Set zwischen den Neustarts kontinuierlich, suchen Sie nach einem Leck. Springt es nur bei Lastspitzen, sind Limit oder Parallelität zu knapp bemessen. kube-state-metrics stellt den letzten Beendigungsgrund bereit, sodass Sie OOM Kills pro Workload zählen können, statt sie zufällig zu entdecken.
OOM Kills vorbeugen
- Bemessen Sie Memory-Limits aus dem beobachteten Spitzen-Working-Set plus Reserve und setzen Sie Requests nahe am realen Verbrauch.
- Machen Sie Runtimes container-bewusst, etwa mit
-XX:MaxRAMPercentagefür die JVM oderGOMEMLIMITfür Go, und lassen Sie Platz für Speicher außerhalb des Heaps. - Begrenzen Sie Parallelität über Pools und Queues, damit Spitzen den Speicherbedarf nicht vervielfachen.
- Alarmieren Sie, wenn der Speicher sich dem Limit nähert und bei OOMKilled-Ereignissen, nicht nur bei Neustarts. Ein Pod im CrashLoopBackOff verdeckt sonst schnell die eigentliche Ursache.
- Behandeln Sie wiederholte OOM Kills als Zuverlässigkeitsproblem: Für Nutzer wirken sie wie zufällige Fehler und können sich kaskadierend ausbreiten. Das gehört zum Monitoring der Sättigung.
Wie Obseria hilft
Das Kubernetes-Monitoring von Obseria zeigt Container-Speicher im Verhältnis zu Requests und Limits, Beendigungsgründe und Neustarts pro Workload, verknüpft mit Traces und Logs der betroffenen Pods. Continuous Profiling hilft zu erkennen, welcher Code vor dem Kill Speicher anlegt. Eine umfassendere Liste der Signale im Cluster finden Sie in der Kubernetes-Observability-Checkliste.