Symfony Health-Checks: Liveness- und Readiness-Endpoints für Kubernetes
AI generated
SF
{ }
Symfony · Kubernetes · Health-Checks · Betrieb
Symfony Health-Checks
Liveness- und Readiness-Endpoints für Kubernetes

Kubernetes entscheidet anhand von Health-Checks, ob ein Pod neu gestartet oder aus dem Load-Balancing herausgenommen werden muss, doch beide Entscheidungen brauchen unterschiedliche Informationen. Ein Liveness-Check beantwortet nur die Frage, ob der PHP-Prozess überhaupt noch läuft, während ein Readiness-Check prüfen muss, ob die Anwendung tatsächlich Traffic sinnvoll verarbeiten kann, etwa weil die Datenbankverbindung steht. Wer beide Checks über denselben Endpoint beantwortet, riskiert entweder unnötige Neustarts oder Traffic auf Pods, die eigentlich nicht bereit sind.

15 Min. Lesezeit Kubernetes Probes Symfony · Health-Checks

1. Der Unterschied zwischen Liveness und Readiness

Eine Liveness-Probe beantwortet eine sehr einfache Frage: Läuft der Prozess noch und reagiert er überhaupt auf Anfragen? Schlägt die Liveness-Probe mehrfach hintereinander fehl, geht Kubernetes davon aus, dass der Container in einem nicht mehr reparierbaren Zustand hängt, etwa durch einen Deadlock oder eine Endlosschleife, und startet den Container neu. Eine Liveness-Probe sollte deshalb bewusst minimal gehalten werden und keine externen Abhängigkeiten prüfen, denn ein Ausfall der Datenbank ist kein Grund, einen ansonsten gesunden PHP-Prozess neu zu starten.

Eine Readiness-Probe beantwortet dagegen die Frage, ob der Pod aktuell in der Lage ist, eingehenden Traffic sinnvoll zu verarbeiten. Schlägt sie fehl, entfernt Kubernetes den Pod aus dem Service-Endpoint, ohne den Container neu zu starten, und leitet keinen Traffic mehr dorthin, bis die Probe wieder erfolgreich ist. Das ist etwa während des Hochfahrens sinnvoll, wenn die Anwendung noch Verbindungen aufbaut, oder während eines vorübergehenden Ausfalls einer Abhängigkeit wie der Datenbank, ohne dass der Pod selbst als defekt gilt.

2. Ein dedizierter, ungesicherter Health-Endpoint

Health-Endpoints sollten außerhalb der regulären Firewall- und Authentifizierungs-Konfiguration liegen, da Kubernetes-Probes ohne Session-Cookies oder API-Tokens anfragen. In der security.yaml wird dafür typischerweise ein eigener Firewall-Eintrag mit pattern auf /health(/.*)? und security: false angelegt, sodass diese Routen komplett außerhalb jeder Authentifizierungs-Prüfung liegen, aber weiterhin durch das normale Symfony-Routing und Controller-System bedient werden.

Wichtig ist, den Health-Endpoint bewusst schlank zu halten und keine sensiblen Informationen preiszugeben. Eine Readiness-Antwort sollte im Erfolgsfall nur einen einfachen Status wie {"status": "ok"} liefern und im Fehlerfall zwar mitteilen, welche Abhängigkeit fehlgeschlagen ist, aber keine internen Details wie Connection-Strings oder Stacktraces preisgeben, da der Endpoint öffentlich ohne Authentifizierung erreichbar ist.


<?php
declare(strict_types=1);

namespace App\Controller;

use App\Health\HealthCheckRegistry;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

/**
 * Liveness- und Readiness-Endpoints für Kubernetes-Probes.
 */
final class HealthController
{
    public function __construct(private readonly HealthCheckRegistry $registry)
    {
    }

    #[Route('/health/live', name: 'health_live', methods: ['GET'])]
    public function live(): JsonResponse
    {
        // Liveness: nur prüfen, ob der Prozess überhaupt antwortet.
        return new JsonResponse(['status' => 'ok']);
    }

    #[Route('/health/ready', name: 'health_ready', methods: ['GET'])]
    public function ready(): JsonResponse
    {
        $results = $this->registry->runAll();
        $healthy = array_reduce(
            $results,
            static fn (bool $carry, bool $ok): bool => $carry && $ok,
            true,
        );

        return new JsonResponse(
            ['status' => $healthy ? 'ok' : 'unavailable', 'checks' => $results],
            $healthy ? 200 : 503,
        );
    }
}

3. Readiness-Check für die Datenbankverbindung

Ein Datenbank-Check sollte nicht einfach nur die Doctrine-Connection aus dem Container holen, sondern aktiv prüfen, ob eine Verbindung tatsächlich aufgebaut werden kann. Üblich ist eine minimale Query wie SELECT 1, die keine Tabellenzugriffe braucht und daher auch dann funktioniert, wenn das Schema noch nicht vollständig migriert ist. Wichtig ist ein kurzes Timeout für diese Prüfung, damit eine hängende Datenbankverbindung nicht die gesamte Readiness-Probe blockiert und Kubernetes länger als nötig auf eine Antwort warten lässt.

Für den Verbindungsaufbau selbst empfiehlt es sich, eventuell auftretende Exceptions gezielt abzufangen und in ein einfaches boolesches Ergebnis zu übersetzen, statt sie ungefangen bis zum Controller durchzureichen. So bleibt die Readiness-Antwort auch bei einem Datenbankausfall ein sauberes JSON mit HTTP-Status 503, statt eine unformatierte Fehlerseite mit potenziell sensiblen Stacktrace-Informationen zu zeigen.

4. Checks für Redis und externe Abhängigkeiten

Für Redis, etwa als Cache- oder Session-Backend, eignet sich ein einfacher PING-Befehl, der bei erfolgreicher Antwort PONG zurückliefert und damit sowohl Netzwerk-Erreichbarkeit als auch grundsätzliche Funktionsfähigkeit bestätigt. Auch hier gilt: Ein kurzes Timeout verhindert, dass ein hängender Redis-Server die gesamte Readiness-Probe unnötig verzögert.

Bei externen Abhängigkeiten wie Drittanbieter-APIs ist zu überlegen, ob ein Ausfall dieser Abhängigkeit tatsächlich bedeuten soll, dass der Pod als nicht bereit gilt. Ist die externe API nur für eine Nebenfunktion nötig, etwa einen optionalen Versand-Tracking-Abruf, sollte ihr Ausfall nicht die komplette Readiness-Probe scheitern lassen, da sonst der gesamte Pod aus dem Load-Balancing genommen wird, obwohl der Kernfunktionsumfang weiterhin einwandfrei funktioniert.

5. Eine zentrale HealthCheckRegistry für erweiterbare Checks

Statt einzelne Checks fest im Controller zu verdrahten, bietet sich eine HealthCheckRegistry an, die einzelne Check-Klassen über ein gemeinsames HealthCheckInterface einsammelt, etwa per Symfony-Service-Tagging. Jeder Check implementiert eine einzige Methode, die true oder false zurückgibt, und bekommt seine Abhängigkeiten, etwa die Doctrine-Connection oder den Redis-Client, ganz normal per Constructor Injection.

Dieser Ansatz macht es einfach, neue Checks hinzuzufügen, ohne den Controller selbst anzufassen: Eine neue Check-Klasse wird geschrieben, als Service mit dem passenden Tag registriert, und die Registry nimmt sie beim nächsten Request automatisch mit auf. Das ist besonders in wachsenden Systemlandschaften mit mehreren externen Abhängigkeiten praktisch, weil sich der Health-Endpoint so organisch mit den tatsächlichen Abhängigkeiten der Anwendung weiterentwickelt.

6. Die passende Kubernetes-Probe-Konfiguration

In der Pod-Spezifikation werden livenessProbe und readinessProbe als eigene httpGet-Blöcke mit jeweils eigenem Pfad konfiguriert, also /health/live für die Liveness- und /health/ready für die Readiness-Probe. Wichtig sind auch initialDelaySeconds und periodSeconds: Eine zu kurze initiale Verzögerung kann dazu führen, dass Kubernetes einen Pod neu startet, der schlicht noch beim Hochfahren ist, während ein zu langes periodSeconds bedeutet, dass ein tatsächlicher Ausfall erst spät erkannt wird.

Ein häufiger Fehler ist, failureThreshold für die Liveness-Probe zu niedrig zu setzen, wodurch kurzzeitige Lastspitzen, die die Antwortzeit vorübergehend erhöhen, fälschlicherweise als Ausfall gewertet werden und einen unnötigen Neustart auslösen. Ein höherer Schwellenwert bei der Liveness-Probe im Vergleich zur Readiness-Probe ist meist sinnvoll, da ein Neustart eine deutlich einschneidendere Maßnahme ist als das vorübergehende Herausnehmen aus dem Load-Balancing.

7. Graceful Shutdown: Readiness während des Pod-Terminierens berücksichtigen

Ein oft übersehener Aspekt bei Readiness-Probes ist das Verhalten während des Pod-Terminierens selbst. Wenn Kubernetes einen Pod beendet, sendet es zunächst ein SIGTERM-Signal und wartet bis zu terminationGracePeriodSeconds, bevor es den Prozess hart mit SIGKILL beendet. Problematisch wird es, wenn Kubernetes den Pod parallel noch als bereit führt und weiterhin Traffic dorthin leitet, während die Anwendung bereits mit dem Herunterfahren begonnen hat, was zu abgebrochenen Requests führen kann, weil eingehende Verbindungen mitten in der Verarbeitung abreißen.

Die übliche Lösung ist ein preStop-Hook, der vor dem eigentlichen SIGTERM eine kurze Wartezeit einlegt, typischerweise wenige Sekunden, damit sich die Änderung am Service-Endpoint im gesamten Cluster verteilen kann, bevor der Prozess tatsächlich beendet wird. Zusätzlich kann die Readiness-Probe so implementiert werden, dass sie bei einem eingehenden SIGTERM sofort false zurückgibt, auch wenn der Prozess selbst noch läuft, damit Kubernetes den Pod schnellstmöglich aus dem Load-Balancing entfernt, während bereits laufende Requests noch geordnet zu Ende verarbeitet werden können.

8. Startup-Probes für langsam startende Anwendungen

Bei Symfony-Anwendungen mit aufwendigem Boot-Prozess, etwa durch große Container-Kompilierung oder Cache-Warmup, kann eine zusätzliche startupProbe sinnvoll sein. Sie unterdrückt Liveness- und Readiness-Probes so lange, bis die Anwendung einmal erfolgreich geantwortet hat, und verhindert damit, dass Kubernetes einen Pod während eines längeren, aber normalen Startvorgangs fälschlicherweise als hängend interpretiert.

Die startupProbe kann denselben Endpoint wie die Liveness-Probe verwenden, sollte aber mit großzügigeren failureThreshold- und periodSeconds-Werten konfiguriert werden, da hier bewusst mehr Zeit für den initialen Start eingeräumt wird, ohne dass die reguläre Liveness-Probe während des laufenden Betriebs dieselbe Nachsicht zeigen muss.

9. Häufige Fehler bei der Umsetzung

Ein verbreiteter Fehler ist, denselben Endpoint für Liveness und Readiness zu verwenden und darin auch die Datenbankverbindung zu prüfen. Fällt die Datenbank kurzzeitig aus, wertet Kubernetes das dann als Liveness-Fehler und startet alle Pods neu, obwohl ein Neustart das eigentliche Problem, den Datenbankausfall, gar nicht löst und stattdessen zusätzliche Instabilität durch gleichzeitige Neustarts erzeugt.

Ein weiterer häufiger Fehler ist, den Health-Endpoint versehentlich hinter der regulären Authentifizierung zu belassen. Kubernetes-Probes senden keine Credentials mit, sodass ein solcher Endpoint immer mit HTTP 401 antwortet und Kubernetes den Pod dauerhaft als nicht bereit oder nicht lebendig einstuft, selbst wenn die Anwendung intern vollkommen funktionsfähig ist.

Probe-Typ Prüft Bei Fehlschlag Typische Prüfungen
Liveness Prozess reagiert Container-Neustart einfacher HTTP-200-Response
Readiness Traffic-Bereitschaft Entfernung aus Service DB, Redis, kritische Abhängigkeiten
Startup Boot abgeschlossen unterdrückt andere Probes wie Liveness, mit mehr Geduld
Health-Endpoint beide Zustände je nach Route getrennte Pfade /health/live, /health/ready

Mironsoft

Symfony-Architektur, saubere Domänenlogik und Legacy-Modernisierung

Symfony-Anwendungen, die nach zwei Jahren noch wartbar sind?

Wir prüfen bestehende Symfony-Projekte auf aufgeblähte Controller, fehlende Serviceabstraktionen und ungetestete Kernlogik und bauen daraus eine Architektur, die neue Features aufnimmt, ohne bei jedem Release brüchiger zu werden.

Architektur-Review

Bundle-Struktur, Dependency Injection und Serviceabstraktionen auf Wartbarkeit prüfen.

Legacy-Modernisierung

Schrittweise Migration veralteter Symfony-Versionen ohne kompletten Neuschrieb.

Test- und Qualitätssicherung

PHPUnit, PHPStan und CI-Pipelines für nachhaltige Codequalität aufbauen.

10. Zusammenfassung

Health-Checks für Kubernetes: Das Wichtigste auf einen Blick

Liveness

minimal, prüft nur, ob der Prozess überhaupt antwortet

Readiness

prüft echte Traffic-Bereitschaft inklusive Datenbank und Redis

Endpoint

dediziert, außerhalb der Firewall-Authentifizierung

Registry

erweiterbare HealthCheckRegistry statt fest verdrahteter Checks

11. FAQ: Health-Checks für Kubernetes: Das Wichtigste auf einen Blick

1Was ist der Unterschied zwischen Liveness- und Readiness-Probe?
Liveness prüft nur, ob der Prozess überhaupt noch reagiert und rechtfertigt bei Fehlschlag einen Container-Neustart. Readiness prüft, ob die Anwendung tatsächlich Traffic verarbeiten kann, und führt bei Fehlschlag nur zur Entfernung aus dem Load-Balancing.
2Sollte die Liveness-Probe die Datenbankverbindung prüfen?
Nein. Ein Datenbankausfall ist kein Grund, einen ansonsten funktionierenden PHP-Prozess neu zu starten. Solche Checks gehören in die Readiness-Probe.
3Wie wird der Health-Endpoint von der Authentifizierung ausgenommen?
Über einen eigenen Firewall-Eintrag in der security.yaml mit passendem pattern und security: false, damit Kubernetes-Probes ohne Credentials erfolgreich antworten können.
4Wie prüfe ich die Datenbankverbindung für Readiness?
Mit einer minimalen Query wie SELECT 1 und einem kurzen Timeout, damit eine hängende Verbindung nicht die gesamte Probe blockiert.
5Sollte jede externe Abhängigkeit die Readiness-Probe scheitern lassen?
Nur wenn sie für die Kernfunktion der Anwendung wirklich notwendig ist. Nebenfunktionen wie optionale Drittanbieter-APIs sollten die Readiness-Probe nicht blockieren.
6Wozu dient eine Startup-Probe zusätzlich zu Liveness und Readiness?
Sie unterdrückt beide anderen Probes während eines längeren, aber normalen Boot-Vorgangs und verhindert so fälschliche Neustarts bei langsam startenden Anwendungen.
7Was liefert der Readiness-Endpoint bei einem Ausfall zurück?
Einen HTTP-Status 503 mit einem JSON-Body, der angibt, welcher Check fehlgeschlagen ist, aber ohne sensible interne Details wie Connection-Strings preiszugeben.
8Wie füge ich einen neuen Health-Check hinzu, ohne den Controller zu ändern?
Über eine zentrale HealthCheckRegistry, die Check-Klassen per Service-Tagging automatisch einsammelt. Eine neue Check-Klasse implementiert nur das gemeinsame Interface.
9Warum ist ein zu niedriger failureThreshold bei der Liveness-Probe problematisch?
Kurzzeitige Lastspitzen können dann fälschlicherweise als Ausfall gewertet werden und einen unnötigen Neustart auslösen, obwohl der Prozess grundsätzlich funktionsfähig ist.
10Was passiert, wenn Liveness und Readiness denselben Endpoint teilen?
Ein kurzzeitiger Datenbankausfall würde dann als Liveness-Fehler gewertet und führt zu unnötigen Container-Neustarts, statt nur den Pod vorübergehend aus dem Load-Balancing zu nehmen.