Health Checks mit Liveness und Readiness Probes
Ein falsch konfigurierter Health Check ist gefährlicher als gar keiner. Kubernetes startet gesunde Pods neu, weil eine Liveness Probe zu streng geschrieben wurde, oder schickt Traffic an einen Pod, dessen Datenbankverbindung längst abgebrochen ist. Dieser Artikel zeigt, wie ein Health Check für Symfony in Kubernetes wirklich zuverlässig wird, mit Liveness, Readiness und Startup Probe im Zusammenspiel.
Inhaltsverzeichnis
- 1. Warum ein Health Check in Kubernetes über den Betrieb entscheidet
- 2. Liveness Probes: wann Kubernetes einen Container neustartet
- 3. Readiness Probes: wann ein Pod tatsächlich Traffic erhält
- 4. Startup Probes für langsam startende Symfony Anwendungen
- 5. Einen eigenen Health Check Endpoint in Symfony bauen
- 6. Abhängigkeiten prüfen: Datenbank, Cache und Message Queue
- 7. Probes im Kubernetes-Manifest korrekt konfigurieren
- 8. Häufige Fallstricke: Timeouts, Kaskaden und Fehlalarme
- 9. Liveness, Readiness und Startup Probe im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum ein Health Check in Kubernetes über den Betrieb entscheidet
Kubernetes trifft anhand eines Health Check automatisiert Entscheidungen über Leben und Traffic eines Pods: neustarten, aus dem Load Balancer nehmen, weiter warten. Diese Entscheidungen laufen ohne menschliches Zutun ab, und genau deshalb ist die Qualität des zugrunde liegenden Health Check so wichtig. Ein zu einfacher Check, der nur prüft, ob PHP-FPM antwortet, sagt nichts darüber aus, ob die Anwendung tatsächlich Datenbankanfragen beantworten kann.
Umgekehrt ist ein zu strenger Health Check genauso gefährlich. Prüft die Liveness Probe die Datenbankverbindung mit, führt ein kurzer Netzwerk-Hänger bei der Datenbank dazu, dass Kubernetes reihenweise gesunde Anwendungscontainer neustartet, obwohl das eigentliche Problem woanders liegt. Ein durchdachter Health Check für Symfony trennt deshalb strikt zwischen der Frage, ob der Prozess selbst noch lebt, und der Frage, ob er gerade in der Lage ist, Anfragen sinnvoll zu beantworten.
Dieser Artikel behandelt genau diese Trennung: Liveness Probe, Readiness Probe und Startup Probe haben unterschiedliche Aufgaben, unterschiedliche Konsequenzen bei Fehlschlag und sollten deshalb unterschiedliche Endpunkte oder zumindest unterschiedliche Prüftiefen verwenden. Ein einziger, undifferenzierter Health Check für alle drei Probe-Typen ist einer der häufigsten Fehler in Symfony-Kubernetes-Setups.
2. Liveness Probes: wann Kubernetes einen Container neustartet
Die Liveness Probe beantwortet eine einzige Frage: Ist der Prozess im Container noch in einem Zustand, aus dem er sich selbst erholen kann, oder ist er in einem Deadlock oder einer Endlosschleife gefangen, aus der nur ein Neustart hilft. Schlägt die Liveness Probe wiederholt fehl, tötet Kubernetes den Container und startet ihn neu. Genau deshalb sollte dieser Health Check so minimal wie möglich sein: Er darf ausschließlich prüfen, ob der PHP-Prozess selbst antwortet, nicht ob externe Abhängigkeiten erreichbar sind.
Ein Symfony-typischer Fehler ist eine Liveness Probe, die dieselbe Route wie die Readiness Probe nutzt und dabei auch die Datenbankverbindung prüft. Fällt die Datenbank für zehn Sekunden aus, killt Kubernetes plötzlich alle Anwendungspods gleichzeitig, obwohl kein einziger Prozess tatsächlich hängen geblieben ist. Die Datenbank kommt zurück, aber alle Pods starten in diesem Moment gleichzeitig neu, was die Downtime künstlich verlängert statt sie zu verkürzen. Die Liveness Probe für ein sauberes Health Check-Setup prüft daher ausschließlich den lokalen Prozesszustand.
<?php
// src/Controller/HealthController.php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
final class HealthController
{
// Liveness endpoint: only confirms the PHP process itself responds.
// No external dependency check here — a slow database must never
// cause Kubernetes to kill and restart otherwise healthy pods.
#[Route('/health/live', name: 'health_live', methods: ['GET'])]
public function live(): JsonResponse
{
return new JsonResponse(['status' => 'ok'], 200);
}
}
3. Readiness Probes: wann ein Pod tatsächlich Traffic erhält
Die Readiness Probe beantwortet eine andere Frage: Ist der Pod gerade in der Lage, eingehenden Traffic sinnvoll zu bearbeiten. Schlägt dieser Health Check fehl, entfernt Kubernetes den Pod aus dem Service-Endpunkt, ohne ihn zu killen. Der Container läuft weiter, bekommt aber keinen neuen Traffic mehr, bis die Readiness Probe wieder erfolgreich ist. Das ist der entscheidende Unterschied zur Liveness Probe und der Grund, warum externe Abhängigkeiten hier geprüft werden dürfen und sollen.
Für Symfony bedeutet das: Die Readiness Probe prüft, ob die Datenbankverbindung steht, ob der Cache-Layer erreichbar ist und ob gegebenenfalls eine Message-Queue-Verbindung aufgebaut werden kann. Fällt eine dieser Abhängigkeiten aus, nimmt sich der Pod selbst aus der Rotation, während andere, gesunde Pods weiter Traffic bekommen. Das verhindert, dass Nutzeranfragen auf einem Pod landen, der ohnehin nur mit einem Fehler antworten könnte, ohne dass ein einziger Container neugestartet werden müsste.
4. Startup Probes für langsam startende Symfony Anwendungen
Symfony-Anwendungen mit großem Container, vielen Bundles oder einem kalten OPcache können mehrere Sekunden bis Minuten benötigen, bevor die erste Anfrage zuverlässig beantwortet wird. Ohne eine Startup Probe interpretiert Kubernetes eine langsam startende Anwendung fälschlich als gescheiterte Liveness Probe und killt den Container, noch bevor er überhaupt fertig hochgefahren ist. Das führt zu einer Neustart-Schleife, aus der der Pod nie herauskommt, weil jeder Neustart wieder dieselbe Startzeit benötigt.
Die Startup Probe löst dieses Problem, indem sie Liveness und Readiness Probe so lange deaktiviert, bis der erste erfolgreiche Startup-Check eintrifft. Erst danach übernehmen die regulären Probes die Überwachung. Für ein Health Check-Setup mit Symfony ist die Startup Probe deshalb kein optionales Detail, sondern in Umgebungen mit merklicher Bootzeit ein notwendiger Bestandteil, um Neustart-Schleifen bei jedem Deployment und jedem Node-Wechsel zu vermeiden.
# k8s/deployment.yaml — probes section for a Symfony deployment
containers:
- name: symfony-app
image: registry.mironsoft.de/symfony-app:1.4.2
ports:
- containerPort: 9000
# Startup probe: gives the container time to boot before
# liveness/readiness even start evaluating
startupProbe:
httpGet:
path: /health/live
port: 8080
failureThreshold: 30
periodSeconds: 2
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 0
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
5. Einen eigenen Health Check Endpoint in Symfony bauen
Ein sauberer Health Check Endpoint in Symfony gehört nicht in den Anwendungs-Namespace der Business-Logik, sondern in einen eigenen, leichten Controller, der bewusst keine anderen Services der Anwendung berührt außer den explizit zu prüfenden. Wichtig ist außerdem, dass diese Route nicht durch Middleware für Authentifizierung, Session-Handling oder CSRF-Schutz läuft, da diese Schichten selbst wieder Abhängigkeiten wie Session-Storage oder Redis einführen, die den Health Check unnötig verkomplizieren.
In der Praxis empfiehlt sich ein eigener Firewall-Bereich in der Symfony Security-Konfiguration, der die Health-Routen komplett von Authentifizierung ausnimmt. So bleibt der Endpoint auch dann erreichbar, wenn die Session-Infrastruktur selbst gerade Probleme hat, was für einen zuverlässigen Health Check unerlässlich ist. Zusätzlich sollte der Endpoint möglichst wenig Speicher allozieren und keine schweren Services aus dem Container instanziieren, die für die eigentliche Prüfung nicht gebraucht werden.
# config/packages/security.yaml — exempt health routes from authentication
security:
firewalls:
health:
pattern: ^/health
security: false
stateless: true
main:
lazy: true
provider: app_user_provider
6. Abhängigkeiten prüfen: Datenbank, Cache und Message Queue
Der Readiness-Endpoint prüft typischerweise drei Kategorien von Abhängigkeiten: die primäre Datenbankverbindung über eine minimale Query wie SELECT 1, den Cache-Layer über einen einfachen Ping-Befehl an Redis oder Memcached, und optional die Erreichbarkeit einer Message-Queue wie RabbitMQ. Jede dieser Prüfungen sollte ein enges Timeout von unter einer Sekunde haben, damit ein hängender Check nicht selbst zum Problem wird und die gesamte Probe blockiert.
Entscheidend ist außerdem, dass ein einzelner fehlgeschlagener Abhängigkeits-Check nicht zwangsläufig den gesamten Health Check als rot markieren muss. Ist die Message-Queue für einen asynchronen Reporting-Job kurzzeitig nicht erreichbar, während Datenbank und Cache einwandfrei funktionieren, kann es sinnvoller sein, den Pod weiterhin für synchrone HTTP-Anfragen bereitzuhalten und nur einen internen Alarm auszulösen, statt ihn komplett aus der Rotation zu nehmen. Diese Abwägung hängt von der jeweiligen Anwendung ab und sollte bewusst getroffen werden, nicht implizit durch einen pauschalen Alles-oder-nichts-Check.
<?php
// src/Service/HealthCheckService.php
declare(strict_types=1);
namespace App\Service;
use Doctrine\DBAL\Connection;
use Symfony\Component\Cache\Adapter\RedisAdapter;
final class HealthCheckService
{
public function __construct(
private readonly Connection $connection,
private readonly \Redis $redis,
) {
}
/**
* Runs all readiness checks with a strict timeout per dependency.
*
* @return array<string, bool>
*/
public function checkReadiness(): array
{
return [
'database' => $this->checkDatabase(),
'cache' => $this->checkCache(),
];
}
private function checkDatabase(): bool
{
try {
$this->connection->executeQuery('SELECT 1');
return true;
} catch (\Throwable) {
return false;
}
}
private function checkCache(): bool
{
try {
return $this->redis->ping() !== false;
} catch (\Throwable) {
return false;
}
}
}
7. Probes im Kubernetes-Manifest korrekt konfigurieren
Neben der reinen Existenz der Probes entscheidet die Feinkonfiguration über die Stabilität eines Health Check-Setups. periodSeconds bestimmt, wie oft geprüft wird, timeoutSeconds wie lange auf eine Antwort gewartet wird, und failureThreshold wie viele aufeinanderfolgende Fehlschläge nötig sind, bevor Kubernetes reagiert. Für die Liveness Probe empfiehlt sich ein großzügigerer failureThreshold, da ein Neustart die teuerste Konsequenz ist. Für die Readiness Probe darf die Schwelle niedriger liegen, weil das Entfernen aus der Rotation deutlich günstiger und schneller reversibel ist.
Ein weiterer wichtiger Parameter ist initialDelaySeconds, der ohne eine Startup Probe manuell auf die erwartete Boot-Zeit gesetzt werden müsste. Mit einer vorgeschalteten Startup Probe kann dieser Wert für Liveness und Readiness auf null gesetzt werden, weil die Startup Probe bereits sichergestellt hat, dass der Container fertig hochgefahren ist, bevor die regulären Probes überhaupt zu zählen beginnen. Diese Kombination reduziert Rätselraten bei der Wahl der Delay-Werte erheblich.
#!/usr/bin/env bash
# check-probes.sh — verify liveness and readiness endpoints locally
# before rolling the manifest out to the cluster
set -euo pipefail
BASE_URL="${1:-http://localhost:8080}"
echo "Checking liveness endpoint..."
curl -fsS -m 2 "${BASE_URL}/health/live" | grep -q '"status":"ok"' \
&& echo "[OK] liveness endpoint healthy" \
|| { echo "[FAIL] liveness endpoint unhealthy" >&2; exit 1; }
echo "Checking readiness endpoint..."
curl -fsS -m 3 "${BASE_URL}/health/ready" | grep -q '"database":true' \
&& echo "[OK] readiness endpoint healthy" \
|| { echo "[FAIL] readiness endpoint unhealthy" >&2; exit 1; }
# Inspect actual probe results reported by Kubernetes for a running pod
kubectl describe pod -l app=symfony-app | grep -A 3 "Liveness\|Readiness"
8. Häufige Fallstricke: Timeouts, Kaskaden und Fehlalarme
Der häufigste Fallstrick bei einem Health Check für Symfony ist ein zu kurzes Timeout in Kombination mit einem PHP-FPM-Pool, der unter Last bereits an seiner Kapazitätsgrenze arbeitet. Ist jeder FPM-Worker mit einer echten Anfrage beschäftigt, muss die Health-Anfrage in derselben Warteschlange auf einen freien Worker warten, was das Timeout reißen lässt, obwohl die Anwendung eigentlich funktioniert, nur überlastet ist. Ein separater, kleiner FPM-Pool ausschließlich für Health-Endpunkte kann dieses Problem entschärfen.
Eine zweite Falle ist die Kaskade: Prüft der Health Check eine Abhängigkeit, die selbst wieder von einer weiteren Abhängigkeit abhängt, etwa eine API, die intern eine andere Datenbank abfragt, kann ein Ausfall tief in der Kette dazu führen, dass reihenweise unabhängige Services als ungesund markiert werden. Health Checks sollten deshalb möglichst direkt prüfen, was der jeweilige Service selbst braucht, ohne transitive Abhängigkeiten weiterzureichen, die an anderer Stelle bereits eigene Probes haben.
9. Liveness, Readiness und Startup Probe im Vergleich
Die folgende Tabelle fasst zusammen, wofür jede Probe-Art in einem Symfony-Kubernetes-Setup zuständig ist und welche Konsequenz ein Fehlschlag jeweils hat.
| Probe-Typ | Prüft | Bei Fehlschlag | Externe Abhängigkeiten |
|---|---|---|---|
| Liveness Probe | Prozess reagiert überhaupt noch | Container wird gekillt und neugestartet | niemals prüfen |
| Readiness Probe | Anwendung kann Traffic sinnvoll bedienen | Pod wird nur aus der Rotation genommen | Datenbank, Cache, Queue prüfen |
| Startup Probe | Anwendung ist fertig hochgefahren | Liveness/Readiness starten noch nicht | nicht relevant |
Wer diese drei Probe-Typen korrekt trennt, vermeidet die zwei häufigsten Betriebsprobleme in Symfony-Kubernetes-Setups: unnötige Neustarts durch einen zu strengen Health Check und träge startende Pods, die in einer Neustart-Schleife feststecken. Der Aufwand für die saubere Trennung ist gering, der Effekt auf die Betriebsstabilität dagegen erheblich.
Mironsoft
Symfony DevOps, Kubernetes-Betrieb und Observability-Setup
Health Checks, die euren Symfony-Betrieb wirklich absichern?
Wir bauen belastbare Liveness-, Readiness- und Startup-Probes für Symfony in Kubernetes, inklusive dediziertem Health-Endpoint und sauber getrennten Abhängigkeits-Checks.
Probe-Audit
Bestehende Kubernetes-Manifeste auf Fehlkonfigurationen bei Health Checks prüfen
Endpoint-Implementierung
Dedizierten Health-Controller mit Datenbank-, Cache- und Queue-Checks bauen
Observability
Probe-Ergebnisse in Monitoring und Alerting integrieren
10. Zusammenfassung
Ein zuverlässiger Health Check für Symfony in Kubernetes trennt konsequent drei Fragen: Lebt der Prozess noch, kann die Anwendung gerade Traffic bedienen, und ist der Container überhaupt fertig gestartet. Die Liveness Probe bleibt bewusst minimal und prüft nie externe Abhängigkeiten, damit ein Datenbank-Hänger nicht zur Neustart-Kaskade wird. Die Readiness Probe prüft genau diese Abhängigkeiten und nimmt den Pod bei Bedarf sanft aus der Rotation, ohne ihn zu killen.
Die Startup Probe schützt langsam startende Symfony-Anwendungen davor, in einer endlosen Neustart-Schleife gefangen zu bleiben. Ein eigener, von Authentifizierung befreiter Endpoint mit engen Timeouts pro Abhängigkeit rundet ein robustes Health Check-Setup ab. Wer diese Trennung von Anfang an einhält, erspart sich nächtliche Alarme wegen Neustart-Kaskaden, die eigentlich nur ein kurzer, harmloser Netzwerk-Hänger ausgelöst hat.
Symfony Health Checks in Kubernetes — Das Wichtigste auf einen Blick
Liveness Probe
Nur den Prozesszustand prüfen. Nie externe Abhängigkeiten, sonst drohen unnötige Neustart-Kaskaden.
Readiness Probe
Datenbank, Cache und Queue prüfen. Fehlschlag entfernt den Pod nur aus der Rotation, kein Neustart.
Startup Probe
Für langsam startende Anwendungen Pflicht. Verhindert Neustart-Schleifen während des Boot-Vorgangs.
Eigener Endpoint
Ohne Authentifizierung, mit engen Timeouts pro Abhängigkeit, getrennt für Liveness und Readiness.