Das Symptom
Die Spalte RESTARTS zählt hoch, aber die Anwendung ist nicht kaputt. Wer sich in
den Container hängt oder die Logs mitliest, sieht ein Programm, das sauber
hochfährt, seine Verbindungen aufbaut, anfängt zu arbeiten — und dann mitten im
Satz aufhört. Kein Absturz, keine Ausnahme, keine letzte Fehlerzeile. Nach ein
paar Runden landet der Pod womöglich sogar in CrashLoopBackOff, und ab da sucht
die halbe Abteilung im falschen Programm. Typisch tritt das auf, wenn eine
Anwendung beim ersten Start einen Cache aufbaut, eine Datenbankmigration fährt
oder mehrere hundert Megabyte an Modelldaten lädt.
Die „Layer 8“-Wahrheit
Hier ist die Abgrenzung, um die es geht: Bei CrashLoopBackOff liegt die Ursache
im Container — er startet, bricht ab, und das kubelet wartet zwischen den
Versuchen immer länger. Hier ist der Container gesund. Er wird von außen
erschossen, weil eine Prüfung ihn für tot erklärt hat, obwohl er nur beschäftigt
war. Das Ergebnis sieht identisch aus, die Ursache ist die entgegengesetzte, und
genau deshalb kostet dieser Fehler so viel Zeit.
Der Grund liegt in den Vorgabewerten, die niemand liest, weil sie so harmlos
klingen. Eine Liveness-Probe ohne weitere Angaben beginnt sofort nach dem Start
des Containers, prüft alle zehn Sekunden, gibt der Anwendung eine einzige Sekunde
zum Antworten und gibt nach drei Fehlschlägen auf. Macht rund dreißig Sekunden
Gnadenfrist. Eine Anwendung, die beim ersten Start eine Minute braucht, hat gegen
diese Uhr keine Chance — und sie bekommt auch nie eine, denn nach jedem Neustart
beginnt dasselbe Rennen von vorn. Dass zusätzlich timeoutSeconds bei einer
Sekunde steht, macht die Sache unter Last zum Glücksspiel: Der Endpunkt antwortet
korrekt, nur eben in 1,2 Sekunden, und das zählt als Fehlschlag.
Der übliche Reflex, initialDelaySeconds immer weiter hochzudrehen, ist die
falsche Reparatur. Er verlängert nämlich genau das Fenster, in dem eine echte
Verklemmung unentdeckt bliebe — man tauscht das eine Problem gegen das andere.
Die Dokumentation benennt diesen Zielkonflikt ausdrücklich: Bei Anwendungen mit
zusätzlichem Zeitbedarf bei der ersten Initialisierung kann es knifflig sein,
die Werte der Liveness-Probe einzustellen, ohne die schnelle Reaktion auf
Verklemmungen aufzugeben, die der Grund für diese Probe war. Die Lösung ist eine
eigene Prüfung für die Startphase. Die Startup-Probe hält Liveness- und
Readiness-Probe zurück, bis sie einmal erfolgreich war, und gibt der Anwendung
so viel Zeit, wie failureThreshold mal periodSeconds ergibt. Danach übernimmt
die Liveness-Probe wieder — scharf gestellt, wie sie sein soll.
Bleibt die Frage, die man sich vor jeder Liveness-Probe stellen sollte und fast
nie stellt: Braucht diese Anwendung überhaupt eine? Wenn ein Prozess bei einem
Problem ohnehin von selbst abstürzt, erledigt die Neustart-Regel des Pods die
Arbeit bereits, und die Probe fügt nur eine weitere Möglichkeit hinzu, aus
Versehen etwas Gesundes zu töten. Eine Liveness-Probe ist ein Werkzeug für
Zustände, aus denen sich ein Programm nicht selbst befreien kann. Für „gerade
beschäftigt“ und „noch nicht so weit“ gibt es Readiness und Startup — und die
nehmen einem laufenden Dienst höchstens den Verkehr weg, nicht das Leben.
Die echte, verifizierte Lösung
1000 % faktenbasiert. Jeder Befehl ist real, jeder Pfad existiert. Schritt für Schritt:
1.Die Ereignisse lesen, bevor irgendetwas geändert wird
Der Befehl zeigt den Zustand jedes Containers und die letzten Ereignisse. Gesucht sind zwei Zeilen: eine Warnung mit dem Grund Unhealthy und dem Hinweis, dass die Liveness-Probe fehlgeschlagen ist, und unmittelbar danach die Meldung über das Beenden des Containers. Fehlen diese Zeilen, ist die Probe nicht die Ursache und die Suche gehört woanders hin.
kubectl describe pod POD-NAME
kubectl events --for pod/POD-NAME
2.Die Logs des vorherigen Durchlaufs auf ihre letzte Zeile hin ansehen
Entscheidend ist hier nicht der Inhalt, sondern das Ende. Bricht die Ausgabe mitten im normalen Betrieb ab, ohne Ausnahme, ohne Abschiedsmeldung, dann hat die Anwendung sich nicht selbst beendet — sie wurde beendet. Endet sie dagegen mit einem Fehler, einer fehlenden Umgebungsvariablen oder einer nicht erreichbaren Datenbank, liegt die Ursache im Container und nicht in der Probe.
kubectl logs POD-NAME --previous
kubectl logs POD-NAME -c CONTAINER-NAME --previous
3.Die eingestellten Werte der Proben auslesen
Erst jetzt in das Manifest sehen. Interessant sind die vier Zahlen, die zusammen die Frist ergeben: initialDelaySeconds, periodSeconds, timeoutSeconds und failureThreshold. Fehlt eine davon, gilt der Vorgabewert — und die Vorgabewerte sind streng. Besonders häufig übersehen wird timeoutSeconds: Ohne Angabe hat die Anwendung genau eine Sekunde Zeit zu antworten.
kubectl get pod POD-NAME -o yaml
kubectl explain pod.spec.containers.livenessProbe
4.Die echte Startzeit messen, statt sie zu schätzen
Den Prüf-Endpunkt lokal weiterleiten und die Antwortzeit messen. Die erste Zeile öffnet die Weiterleitung, die zweite läuft in einem zweiten Terminal und gibt Statuscode und Gesamtdauer aus. Wichtig ist der allererste Aufruf nach dem Start, nicht der zehnte: Die Dokumentation zählt eine HTTP-Probe erst dann als erfolgreich, wenn der Statuscode größer oder gleich 200 und kleiner als 400 ist. Port und Pfad an die eigene Probe anpassen.
kubectl port-forward pod/POD-NAME 8080:8080
curl -sS -o /dev/null -w "%{http_code} %{time_total}\n" http://127.0.0.1:8080/healthz
5.Eine Startup-Probe einrichten, statt die Liveness-Probe zu verwässern
Das ist der Punkt, an dem die meisten den falschen Weg wählen und einfach initialDelaySeconds hochdrehen. Die Dokumentation empfiehlt ausdrücklich das andere: Setze eine Startup-Probe mit derselben Prüfung ein, mit einem Produkt aus failureThreshold und periodSeconds, das lang genug für den schlimmsten Startfall ist. Ihr eigenes Beispiel rechnet es vor — mit failureThreshold 30 und periodSeconds 10 hat die Anwendung höchstens 5 Minuten (30 mal 10 gleich 300 Sekunden), um ihren Start abzuschließen. Sobald die Startup-Probe einmal erfolgreich war, übernimmt die Liveness-Probe und liefert wieder eine schnelle Reaktion auf Verklemmungen. Und was passiert, wenn der Start wirklich nie gelingt, steht auch dort: Ist die Startup-Probe nie erfolgreich, wird der Container nach 300 Sekunden getötet und unterliegt der restartPolicy des Pods.
startupProbe:
httpGet:
path: /healthz
port: liveness-port
failureThreshold: 30
periodSeconds: 10
6.Zeitgrenze und Toleranz der Liveness-Probe an die Wirklichkeit anpassen
Nach der Startup-Probe bleibt die Frage, ob die laufende Anwendung die Antwortfrist überhaupt einhalten kann. Antwortet der Endpunkt unter Last in 800 Millisekunden, ist ein timeoutSeconds von 1 ein Zufallsgenerator. Die Dokumentation warnt an dieser Stelle deutlich: Eine fehlerhaft umgesetzte Liveness-Probe kann zu kaskadierenden Fehlern führen — der Container wird unter hoher Last neu gestartet, Anfragen von Clients scheitern, weil die Anwendung schlechter skaliert, und die verbleibenden Pods bekommen zusätzliche Last ab, weil einige ausgefallen sind. Ein verbreitetes Muster ist laut Dokumentation, denselben günstigen HTTP-Endpunkt wie für die Readiness-Probe zu benutzen, aber mit einem höheren failureThreshold.
livenessProbe:
httpGet:
path: /healthz
port: liveness-port
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
7.Prüfen, ob überhaupt eine Liveness-Probe gebraucht wird
Die unbequemste Frage zum Schluss, und die Dokumentation stellt sie selbst: Wenn der Prozess in deinem Container von sich aus abstürzt, sobald er auf ein Problem stößt oder ungesund wird, brauchst du nicht zwingend eine Liveness-Probe — das kubelet führt dann automatisch die richtige Maßnahme gemäß der restartPolicy des Pods aus. Eine Liveness-Probe lohnt sich für Zustände, aus denen sich eine Anwendung nicht selbst befreien kann, etwa eine Verklemmung. Für alles andere ist die Readiness-Probe das mildere und meist richtige Werkzeug: Sie nimmt den Verkehr weg, statt den Prozess zu töten.
kubectl get pod POD-NAME -o yaml
kubectl get endpointslices