Wie kaskadierende Ausfälle entstehen und wie ein Circuit Breaker sie strukturell verhindert
Wenn ein externer REST-API-Dienst langsam antwortet oder komplett ausfällt, ohne dass der eigene Client reagiert, stauen sich Anfragen, Threads oder Worker blockieren, und der Ausfall breitet sich auf die eigene Anwendung aus. Das Circuit Breaker Pattern durchbricht diese Kette, indem es nach wiederholten Fehlschlägen aufhört, den fehlerhaften Dienst überhaupt noch anzusprechen, und stattdessen sofort einen Fallback liefert.
Inhaltsverzeichnis
- 1. Wie ein einzelner langsamer Dienst die ganze Anwendung lahmlegt
- 2. Die drei Zustände: Closed, Open, Half-Open
- 3. Schwellenwerte richtig konfigurieren statt willkürlich raten
- 4. Fallback-Strategien für den geöffneten Zustand
- 5. Circuit Breaker mit Symfonys HttpClient kombinieren
- 6. Warum Circuit Breaker und Retry sich ergänzen, nicht ersetzen
- 7. Circuit-Breaker-Zustandswechsel beobachtbar machen
- 8. Circuit-Breaker-Verhalten gezielt testen
- 9. Circuit Breaker im Überblick
- 10. Zusammenfassung
- 11. FAQ
1. Wie ein einzelner langsamer Dienst die ganze Anwendung lahmlegt
Ein klassisches Fehlerbild in verteilten Systemen entsteht, wenn ein Backend-Dienst A einen externen REST-API-Dienst B synchron aufruft und B plötzlich sehr langsam antwortet, statt sauber einen Fehler zurückzugeben. Ohne explizites Timeout-Handling wartet jeder Request an A, der B aufruft, entsprechend lange, wodurch sich Worker-Prozesse oder PHP-FPM-Threads bei A stauen und irgendwann alle verfügbaren Worker mit wartenden Anfragen an das langsame B blockiert sind.
Das Ergebnis ist, dass A für ALLE Anfragen nicht mehr erreichbar ist, nicht nur für die, die tatsächlich B benötigen, weil schlicht keine freien Worker mehr übrig sind. Ein einzelner langsamer Abhängigkeitsdienst kann so eine komplette Anwendung lahmlegen, obwohl der eigentliche Fehler weit entfernt in einem fremden System liegt. Genau dieses Muster verhindert das Circuit Breaker Pattern strukturell.
2. Die drei Zustände: Closed, Open, Half-Open
Ein Circuit Breaker funktioniert konzeptionell wie ein elektrischer Sicherungsschalter mit drei Zuständen. Im Zustand Closed werden alle Anfragen normal durchgelassen, während Fehlschläge kontinuierlich gezählt werden. Überschreitet die Fehlerrate innerhalb eines Zeitfensters einen konfigurierten Schwellenwert, wechselt der Breaker in den Zustand Open.
Im Zustand Open werden Anfragen an den fehlerhaften Dienst gar nicht erst gesendet, sondern sofort mit einem Fehler oder Fallback beantwortet, was den fehlerhaften Dienst entlastet und die eigene Anwendung vor weiteren blockierenden Wartezeiten schützt. Nach einer konfigurierten Timeout-Periode wechselt der Breaker in den Zustand Half-Open, in dem eine begrenzte Anzahl von Testanfragen durchgelassen wird, um zu prüfen, ob sich der Dienst erholt hat. Sind diese Testanfragen erfolgreich, wechselt der Breaker zurück zu Closed, andernfalls zurück zu Open.
<?php
declare(strict_types=1);
enum CircuitState: string
{
case Closed = 'closed';
case Open = 'open';
case HalfOpen = 'half_open';
}
final class CircuitBreaker
{
public function __construct(
private readonly \Redis $redis,
private readonly string $serviceKey,
private readonly int $failureThreshold = 5,
private readonly int $openTimeoutSeconds = 30,
) {
}
public function getState(): CircuitState
{
$state = $this->redis->get("cb:{$this->serviceKey}:state");
return $state ? CircuitState::from($state) : CircuitState::Closed;
}
public function recordSuccess(): void
{
$this->redis->del("cb:{$this->serviceKey}:failures");
$this->redis->set("cb:{$this->serviceKey}:state", CircuitState::Closed->value);
}
public function recordFailure(): void
{
$failures = $this->redis->incr("cb:{$this->serviceKey}:failures");
if ($failures >= $this->failureThreshold) {
$this->redis->set("cb:{$this->serviceKey}:state", CircuitState::Open->value);
$this->redis->expire("cb:{$this->serviceKey}:state", $this->openTimeoutSeconds);
}
}
}
3. Schwellenwerte richtig konfigurieren statt willkürlich raten
Ein zu niedriger Fehlerschwellenwert öffnet den Breaker bei jedem kurzen, harmlosen Ausreißer, was den Dienst häufiger als nötig unerreichbar macht und die Verfügbarkeit unnötig senkt. Ein zu hoher Schwellenwert lässt dagegen zu viele fehlgeschlagene Anfragen durch, bevor der Breaker überhaupt eingreift, wodurch der Schutzeffekt zu spät einsetzt.
Eine bewährte Praxis ist, den Schwellenwert nicht als absolute Fehleranzahl, sondern als Fehlerrate innerhalb eines gleitenden Zeitfensters zu definieren (etwa mehr als 50 Prozent Fehler bei mindestens 10 Anfragen in den letzten 60 Sekunden), um sowohl Anwendungen mit niedrigem als auch mit hohem Traffic sinnvoll abzudecken. Diese Werte sollten anhand beobachteter, realer Fehlerraten in Produktion kalibriert werden, nicht anhand einer pauschalen Bibliotheksvorgabe.
4. Fallback-Strategien für den geöffneten Zustand
Ein Circuit Breaker allein löst nur die Hälfte des Problems: sobald der Breaker offen ist, muss die Anwendung trotzdem sinnvoll auf die fehlende Antwort reagieren. Mögliche Fallback-Strategien reichen von zwischengespeicherten, leicht veralteten Daten (etwa der letzte bekannte Produktpreis aus einem Cache) über einen degradierten, aber funktionalen Modus (etwa eine Suche ohne Personalisierung) bis zu einer klaren Fehlermeldung an den Nutzer.
Welche Fallback-Strategie sinnvoll ist, hängt stark vom Geschäftskontext ab: Bei einem Preisdienst kann ein leicht veralteter, gecachter Preis akzeptabel sein, bei einem Zahlungsdienst dagegen nicht, wo ein klarer Fehler und ein späterer Retry der einzig vertretbare Weg ist. Diese Entscheidung sollte explizit getroffen und nicht implizit dem Circuit-Breaker-Code überlassen werden.
5. Circuit Breaker mit Symfonys HttpClient kombinieren
Symfonys HttpClient-Komponente bietet keinen eingebauten Circuit Breaker, lässt sich aber gut mit einem eigenen oder einer dedizierten Bibliothek wie ekino/php-circuit-breaker kombinieren, indem der Circuit-Breaker-Check vor jedem HttpClient-Aufruf steht und das Ergebnis nach dem Aufruf zurückgemeldet wird. Ein sauberer Ansatz kapselt diese Logik in einem eigenen Decorator um den HttpClientInterface, sodass der Rest der Anwendung den Circuit Breaker transparent nutzt, ohne ihn explizit aufzurufen.
Wichtig ist, den Circuit-Breaker-Status pro externem Dienst getrennt zu führen, nicht global für alle ausgehenden HTTP-Aufrufe. Ein Ausfall von Zahlungsdienst A sollte den unabhängigen Versanddienst B nicht ebenfalls blockieren, weshalb der Redis-Schlüssel im Beispiel oben explizit den `serviceKey` enthält.
6. Warum Circuit Breaker und Retry sich ergänzen, nicht ersetzen
Retry-Logik und Circuit Breaker lösen unterschiedliche Probleme und werden oft fälschlich als Alternativen behandelt. Retry hilft bei kurzzeitigen, transienten Fehlern (ein einzelner verlorener Netzwerk-Paket, ein kurzer Garbage-Collection-Pause beim Zieldienst) durch erneuten Versuch nach kurzer Wartezeit. Bei einem dauerhaft ausgefallenen oder überlasteten Dienst verschlimmert wiederholtes Retry das Problem aber nur, weil es die Last auf den bereits überlasteten Dienst noch erhöht.
Ein Circuit Breaker verhindert genau dieses Verschlimmern, indem er nach einer bestimmten Fehleranzahl komplett aufhört, den Dienst anzusprechen. Die sinnvolle Kombination ist deshalb: Retry für einzelne, kurzzeitige Fehler innerhalb des Closed-Zustands, Circuit Breaker als übergeordneter Schutz, der bei anhaltenden Fehlern eingreift, bevor Retry-Versuche die Situation weiter verschärfen.
7. Circuit-Breaker-Zustandswechsel beobachtbar machen
Ein Circuit Breaker, der unbemerkt im Open-Zustand verharrt, kann tagelang unbemerkt Fallback-Daten ausliefern, ohne dass jemand merkt, dass der eigentliche Dienst schon lange wieder verfügbar wäre, falls die Half-Open-Prüfung fehlerhaft konfiguriert ist. Jeder Zustandswechsel sollte deshalb geloggt und idealerweise als Metrik an ein Monitoring-System wie Prometheus gesendet werden, mit Alerts bei längeren Open-Phasen.
Ein Dashboard, das pro externem Dienst den aktuellen Circuit-Breaker-Zustand und die Historie der Zustandswechsel zeigt, macht die Resilience-Schicht der Anwendung sichtbar, statt sie als unsichtbare Infrastruktur zu behandeln. Das ist besonders wichtig, weil ein dauerhaft offener Breaker ein klares Symptom für ein tieferliegendes Problem bei einem Abhängigkeitsdienst ist, das eigene Aufmerksamkeit verdient.
Sinnvoll ist außerdem, die Anzahl der Zustandswechsel pro Tag als eigene Metrik zu erfassen: Ein Breaker, der mehrfach täglich zwischen Closed und Open hin und her springt, deutet auf einen falsch kalibrierten Schwellenwert oder auf einen Abhängigkeitsdienst mit grundsätzlicher Instabilität hin, die getrennt vom Circuit-Breaker-Verhalten selbst untersucht werden sollte.
8. Circuit-Breaker-Verhalten gezielt testen
Das Verhalten eines Circuit Breakers lässt sich nicht sinnvoll allein durch Unit-Tests der Zustandsübergänge validieren, weil das eigentliche Ziel ist, wie sich die gesamte Anwendung unter simuliertem Ausfall eines externen Dienstes verhält. Integrationstests, die einen HTTP-Mock-Server gezielt Fehler oder Timeouts zurückgeben lassen, prüfen, ob der Breaker nach dem konfigurierten Schwellenwert tatsächlich öffnet und ob der Fallback-Pfad korrekt greift.
Zusätzlich lohnt sich ein gezielter Chaos-Engineering-artiger Test in einer Staging-Umgebung, bei dem ein realer Abhängigkeitsdienst temporär künstlich verlangsamt oder abgeschaltet wird, um zu beobachten, ob die Anwendung wie erwartet reagiert, statt sich auf die Korrektheit der Unit-Tests allein zu verlassen.
9. Circuit Breaker im Überblick
Die folgende Tabelle fasst die Kernaspekte des Patterns zusammen, um es von verwandten Resilience-Mustern abzugrenzen.
| Zustand | Verhalten | Trigger für Wechsel |
|---|---|---|
| Closed | Alle Anfragen normal durchgelassen | Fehlerrate übersteigt Schwellenwert → Open |
| Open | Anfragen sofort abgelehnt, Fallback greift | Timeout-Periode abgelaufen → Half-Open |
| Half-Open | Begrenzte Testanfragen durchgelassen | Erfolg → Closed, Fehler → zurück zu Open |
| Fallback (in Open) | Cache, degradierter Modus oder Fehlermeldung | Bleibt aktiv bis Rückkehr zu Closed |
Mironsoft
OpenAPI-Design, Symfony-APIs und API-Sicherheit
APIs, die externe Teams ohne Rückfragen integrieren können?
Wir prüfen bestehende REST-APIs auf inkonsistente Fehlerformate, fehlende OpenAPI-Dokumentation und Sicherheitslücken und bauen daraus eine API, die klar dokumentiert, versioniert und gegen Missbrauch abgesichert ist.
API-Review
OpenAPI-Spezifikation, Fehlerformate und Statuscodes auf Konsistenz prüfen.
Symfony-Umsetzung
DTOs, Serializer und Validator für saubere, typsichere Request/Response-Modelle einsetzen.
Security-Audit
Rate-Limiting, Auth-Schemes und Input-Validierung gegen echte Angriffsflächen absichern.
10. Zusammenfassung
Circuit Breaker: Das Wichtigste auf einen Blick
Kernproblem
Ohne Circuit Breaker stauen sich Worker bei einem langsamen Abhängigkeitsdienst, bis die gesamte Anwendung blockiert ist.
Drei Zustände
Closed lässt alles durch, Open blockt sofort mit Fallback, Half-Open testet vorsichtig die Erholung.
Ergänzt Retry
Retry löst kurzzeitige Fehler, Circuit Breaker verhindert, dass Retry einen bereits überlasteten Dienst weiter belastet.
Monitoring Pflicht
Jeder Zustandswechsel muss sichtbar sein, sonst bleiben Fallback-Daten unbemerkt tagelang aktiv.