Unsere Pods meldeten sich gesund, konnten aber keinen Traffic bedienen. Wie das?

Der Health Check:

livenessProbe:
  httpGet:
    path: /health
    port: 8080
  periodSeconds: 10

Der Handler:

@GetMapping("/health")
public String health() {
    return "OK";  // always returns OK
}

Die App startete, verlor ihre Datenbankverbindung, lief gegen den Cache in Timeouts — und antwortete weiter mit „OK“. Kubernetes ließ jeden Pod in der Rotation, der Load Balancer routete weiter, und die Nutzer bekamen Fehler von Pods, die laut Plattform kerngesund waren.

Der erste Fix machte es schlimmer:

Der naheliegende Patch — Datenbank-, Cache- und External-API-Checks in /health einzubauen — landete in derselben Liveness-Probe. Drei Tage später machte die Datenbank ein Failover und war rund 30 Sekunden nicht erreichbar. Die Liveness Probe jedes Pods schlug innerhalb eines Probe-Zyklus fehl, und das Kubelet tat, was Liveness-Fehler verlangen: Es killte und startete alle 40 Pods neu. Jeder Neustart öffnete Connection Pools neu und wärmte Caches auf — gegen eine Datenbank mitten in der Recovery. Die Health Checks selbst wurden zur Überlast — ein Restart-Sturm, der genau auf die Abhängigkeit einhämmerte, auf die alle warteten. Aus einem 30-Sekunden-Blip wurde ein 20-Minuten-Ausfall.

Die Unterscheidung, auf die es ankommt:

  • Liveness: „Hängt dieser Prozess?“ Prüft nur den Prozess — Deadlocks, eine blockierte Event Loop. Niemals geteilte Abhängigkeiten. Ein Fehlschlag bedeutet Neustart, und 40 Pods neu zu starten repariert keine kaputte Datenbank.
  • Readiness: „Kann dieser Pod gerade jetzt bedienen?“ Ein Fehlschlag nimmt den Pod aus den Service-Endpoints, ohne ihn neu zu starten; er kommt zurück, sobald der Check wieder besteht. Dependency-Checks gehören, wenn überhaupt, hierhin — aber koppeln Sie mit Bedacht an geteilte Abhängigkeiten, denn wenn die Datenbank für alle down ist, gehen alle Pods gemeinsam auf unready und Sie bedienen gar nichts mehr. Oft ist eine degradierte Antwort besser als gar keine Endpoints.
  • Startup: deckt langsame Initialisierung ab, damit die Liveness Probe keinen Pod killt, der noch hochfährt.

Was wir heute betreiben:

livenessProbe:
  httpGet:
    path: /health/live     # process only, no dependencies
    port: 8080
  periodSeconds: 10
  failureThreshold: 3
readinessProbe:
  httpGet:
    path: /health/ready    # pod-local checks, e.g. warm caches
    port: 8080
  periodSeconds: 5
  failureThreshold: 2
startupProbe:
  httpGet:
    path: /health/live
    port: 8080
  periodSeconds: 5
  failureThreshold: 30
@GetMapping("/health/ready")
public ResponseEntity<HealthStatus> readiness() {
    HealthStatus status = healthService.check();
    return status.canServe()
        ? ResponseEntity.ok(status)
        : ResponseEntity.status(503).body(status);
}

Dependency-Gesundheit ist komplett aus den Probes herausgewandert — in Metriken und Alerts, wo Menschen, nicht das Kubelet, entscheiden, was zu tun ist. Die verwandte Kaskade, bei der Neustarts eine sich erholende Datenbank mit Verbindungen fluten, hat ihren eigenen Bericht in Autoscaling, das die Datenbank lahmlegte; Probe-Semantik richtig zu setzen ist ein Standardpunkt in unseren Kubernetes- und DevOps-Reviews.

Lektion: Ein Health Check, der keine Gesundheit prüft, ist schlimmer als keiner — und eine Liveness Probe, die zu viel prüft, brennt den Cluster nieder, um es zu beweisen.


← Zurück zu Lessons Learned

Incidents wie dieser sind vermeidbar. Unser Team für AWS Managed Services überwacht Kundenumgebungen 24/7 mit KI-gestütztem Monitoring und Senior Engineers am Pager — damit Geschichten wie diese im Archiv bleiben.