Layer 8Ehrlicher Tech-Support
SoftwareSchweregrad: LästigOOMKilled

Kubernetes: Container OOMKilled mit Exit-Code 137

Grund OOMKilled, Exit-Code 137: Der Container hat sein eigenes Speicherlimit gerissen – nicht der Knoten hatte zu wenig Arbeitsspeicher.

Aufwand:
ca. 25 Min.
Voraussetzung:
kubectl, metrics-server für die Verbrauchsmessung
Gilt für:
Kubernetes (Container mit gesetztem Speicherlimit)

Das Symptom

Der Pod lief, und plötzlich steht in seinem Zustand OOMKilled, dazu der Exit-Code 137. Kein Absturzbericht, keine letzte Log-Zeile mit einer Fehlermeldung der Anwendung, oft nicht einmal ein halber Satz – die Ausgabe endet mitten im Betrieb. Das kubelet startet den Container neu, alles wirkt kurz normal, und ein paar Stunden später steht dasselbe wieder da. Auf dem Knoten sind derweil Gigabytes an Arbeitsspeicher frei.

Die „Layer 8“-Wahrheit

Genau dieses letzte Detail ist der Schlüssel, und es ist der Punkt, an dem die meisten in die falsche Richtung suchen. Der Knoten hatte reichlich Speicher. Er wurde nie gefragt. Beendet wurde der Container, weil er die Grenze überschritten hat, die in seiner eigenen Beschreibung steht – ein Wert, den irgendwann jemand aus dem Handgelenk hingeschrieben hat, weil das Feld nun einmal ausgefüllt werden wollte. Die Dokumentation ist da unmissverständlich: Ein Container darf seine Anforderung überschreiten, solange der Knoten Speicher übrig hat, sein Limit aber niemals.

Das unterscheidet den Fall grundlegend von dem, was auf einem gewöhnlichen Linux-Server passiert. Dort schlägt der OOM-Killer zu, weil dem gesamten System der Speicher ausgeht, und er sucht sich sein Opfer nach Verbrauch aus – getroffen wird oft nicht der Verursacher, sondern der Dickste im Raum. Hier ist beides anders: Es herrscht kein Mangel, und das Opfer stand von vornherein fest. Der Kernel setzt lediglich eine Buchhaltungsregel durch, die man selbst aufgestellt hat.

Dazu kommt eine Eigenart, die das Ganze so unberechenbar wirken lässt: Die Grenze wird reaktiv durchgesetzt. Die Dokumentation schreibt ausdrücklich, dass Beendigungen erst erfolgen, wenn der Kernel Speicherdruck feststellt – ein Container über seiner Grenze wird also nicht zwingend sofort beendet. Deshalb überlebt dieselbe Anwendung eine Woche lang knapp über dem Limit und stirbt dann ausgerechnet nachts um drei, wenn ein Nachbar-Pod aufwacht. Es sieht nach Zufall aus, ist aber nur verzögerte Konsequenz.

Die Lösung ist entsprechend unromantisch: messen, was die Anwendung wirklich braucht, und die Grenze danach setzen – statt sie zu raten oder, schlimmer, ganz zu streichen. Wer das Limit entfernt, hat den Container gerettet und den Knoten geopfert. Das merkt man dann daran, dass beim nächsten Mal nicht ein Pod stirbt, sondern alle auf dieser Maschine.

Die echte, verifizierte Lösung

1000 % faktenbasiert. Jeder Befehl ist real, jeder Pfad existiert. Schritt für Schritt:

  1. 1.

    Den Zustand und die Zahl der Neustarts ansehen

    In der Übersicht erscheint der Pod als beendet oder erneut startend, die Zahl der Neustarts wächst. Ein Container, der beim Start sofort stirbt, sieht hier genauso aus wie einer, der erst unter Last umkippt – der Unterschied ergibt sich aus den nächsten Schritten.

    kubectl get pods
    
  2. 2.

    Grund und Exit-Code des letzten Laufs auslesen

    Der vollständige Zustand nennt unter lastState.terminated den Grund und den Exit-Code. Die Kubernetes-Dokumentation zeigt für diesen Fall genau diese Ausgabe: reason OOMKilled und exitCode 137. Damit ist der Verdacht nicht mehr Vermutung, sondern belegt.

    kubectl get pod POD-NAME -o yaml
    
  3. 3.

    Denselben Punkt kurz abfragen

    Für den schnellen Blick oder ein Skript reicht ein gezielter Auszug. Steht dort nichts, wurde der Container bisher nicht wegen Speichers beendet – dann ist die Ursache eine andere und die Suche geht woanders weiter.

    kubectl get pod POD-NAME -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason}'
    
  4. 4.

    Das gesetzte Limit nachlesen

    Jetzt die Vorgabe ansehen, an der der Container gescheitert ist. Wichtig ist der Unterschied zwischen Anforderung und Grenze: Nach der Anforderung wird der Pod auf einen Knoten verteilt, an der Grenze wird er beendet. Wer nur die Anforderung erhöht, verschiebt den Pod – er verhindert den Tod nicht.

    kubectl get pod POD-NAME -o jsonpath='{.spec.containers[*].resources}'
    
  5. 5.

    Den tatsächlichen Verbrauch messen

    Wie nah die Anwendung im Normalbetrieb an der Grenze läuft, zeigt die Verbrauchsmessung. Sie setzt einen laufenden metrics-server im Cluster voraus. Liegt der Wert dauerhaft knapp unter dem Limit, ist der nächste Lastspitzen-Abend nur eine Frage der Zeit.

    kubectl top pod POD-NAME
    
  6. 6.

    Den Knoten ausschließen

    Hier wird die Abgrenzung entschieden. Zeigt der Knoten Speicherdruck an oder finden sich mehrere verdrängte Pods, war nicht das Limit das Problem, sondern der Knoten war zu klein für alles, was auf ihm liegt. Dann hilft kein höheres Limit, sondern Verteilung oder mehr Kapazität.

    kubectl get pods -o wide
    kubectl describe node KNOTEN-NAME
    
  7. 7.

    Die Grenze anpassen oder die Anwendung bändigen

    Zwei ehrliche Möglichkeiten: das Limit auf den gemessenen Bedarf anheben, oder der Anwendung beibringen, weniger zu brauchen. Laufzeitumgebungen mit eigener Speicherverwaltung – etwa die JVM oder Node.js – müssen dabei zusätzlich intern begrenzt werden, sonst kennen sie die Grenze des Containers gar nicht und laufen sehenden Auges hinein.

  8. 8.

    Nicht das Limit ganz weglassen

    Der bequeme Ausweg, die Grenze zu streichen, verlagert das Problem nur auf den Knoten: Statt eines beendeten Containers gefährdet die Anwendung dann alle Pods, die sich denselben Knoten teilen. Ein Limit ist keine Schikane, sondern die Zusage, dass ein einzelner Ausrutscher nicht die Nachbarn mitreißt.

Häufige Fragen (FAQ)

?Ist das dasselbe wie der OOM-Killer, der auf einem Linux-Server Prozesse beendet?

Derselbe Mechanismus im Kernel, aber ein völlig anderer Anlass – und das ist der wichtigste Unterschied dieser Seite. Auf einem gewöhnlichen Server schlägt der OOM-Killer zu, weil dem ganzen System der Arbeitsspeicher ausgeht; er sucht sich dann anhand einer Punktzahl ein Opfer, und getroffen wird der größte Verbraucher, nicht der Verursacher. Bei `OOMKilled` in Kubernetes ist der Knoten dagegen oft kerngesund und hat reichlich freien Speicher. Beendet wird der Container, weil er seine eigene, ausdrücklich gesetzte Grenze überschritten hat. Die Kubernetes-Dokumentation sagt dazu klar: Ein Container darf sein memory request überschreiten, wenn der Knoten Speicher übrig hat, aber niemals sein memory limit. Das Opfer ist hier also kein Zufall, sondern eine Vorgabe – deine eigene.

?Woran erkenne ich, ob es das Limit war oder der Knoten?

Am Ort des Eingriffs. War es das Limit des Containers, steht im Pod-Zustand unter lastState.terminated der Grund OOMKilled mit dem Exit-Code 137, und der Pod läuft am selben Knoten einfach weiter, sobald das kubelet ihn neu startet. Ging dagegen dem Knoten der Speicher aus, sieht man Verdrängung: Das kubelet beendet laut Dokumentation bei Ressourcenmangel von sich aus Pods, um Speicher zurückzugewinnen, und setzt deren Phase auf Failed. Kommt der Knoten dem kubelet zuvor, verlässt sich der Knoten auf den oom_killer des Kernels, wobei das kubelet für jeden Container je nach Dienstgüteklasse einen oom_score_adj-Wert setzt. Ein einzelner OOMKilled-Container bei gesunden Nachbarn spricht für das Limit, viele beendete Pods auf demselben Knoten für den Knoten.

?Warum stirbt der Container nicht sofort, wenn er das Limit überschreitet?

Weil die Grenze reaktiv durchgesetzt wird. Die Dokumentation zu Ressourcen beschreibt es ungewöhnlich offen: Speichergrenzen werden vom Kernel per OOM-Kill durchgesetzt, Beendigungen geschehen aber erst, wenn der Kernel Speicherdruck feststellt – ein Container, der zu viel anfordert, wird also nicht zwingend sofort beendet. Genau daher kommt das sprunghafte Verhalten: Dieselbe Anwendung läuft tagelang knapp über der Grenze und stirbt dann scheinbar grundlos mitten in der Nacht, wenn auf dem Knoten wirklich Druck entsteht.

Belege

Diese Seite ist gegen die folgenden Primärquellen geprüft – Hersteller, Standard oder Gesetzestext. Wer es nicht glaubt, muss es nicht: Da steht, wo es nachzulesen ist.

Zuletzt geprüft am . Dokumentation altert – ein Datum daneben ist der einzige ehrliche Umgang damit.

Auch zu finden unter

Verwandte Fehler

SWLästigCrashLoopBackOff

Kubernetes: Pod in CrashLoopBackOff

CrashLoopBackOff ist kein Fehler von Kubernetes, sondern die Meldung, dass dein Container beim Start abbricht. So kommst du an die Ursache.

SWAnfängerfehlerImagePullBackOff

Kubernetes: ImagePullBackOff und ErrImagePull

Der Pod wartet, weil das Abbild nicht kommt: falscher Name, falsches Tag, fehlende Zugangsdaten. So liest du die Ursache statt zu raten.