Circuit Breaker Pattern für REST-API-Clients implementieren
AI generated
{ }
GET
Resilience · Fehlertoleranz
Circuit Breaker Pattern für REST-API-Clients
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.

15 Min. Lesezeit Circuit Breaker · Resilience PHP · Symfony

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.

11. FAQ: Circuit Breaker: Das Wichtigste auf einen Blick

1Brauche ich einen Circuit Breaker für jeden externen API-Aufruf?
Für kritische, häufig aufgerufene externe Dienste ja. Für seltene, unkritische Aufrufe ist der Zusatzaufwand oft nicht gerechtfertigt, hier reicht meist ein einfaches Timeout.
2Wie unterscheidet sich Circuit Breaker von einem einfachen Timeout?
Ein Timeout begrenzt nur die Wartezeit einer einzelnen Anfrage. Ein Circuit Breaker verhindert zusätzlich, dass wiederholt fehlschlagende Anfragen den Dienst überhaupt noch erreichen.
3Kann ich eine bestehende PHP-Bibliothek statt Eigenbau nutzen?
Ja, ekino/php-circuit-breaker ist eine verbreitete Wahl für PHP/Symfony-Projekte. Eigenbau lohnt sich meist nur bei sehr spezifischen Anforderungen.
4Was passiert, wenn der Half-Open-Test wieder fehlschlägt?
Der Breaker wechselt zurück zu Open und startet die Timeout-Periode erneut, statt sofort wieder Vollzugriff zu erlauben.
5Sollte der Schwellenwert für alle Dienste gleich sein?
Nein, kritische Zahlungsdienste vertragen oft niedrigere Schwellenwerte für schnelleres Eingreifen als unkritische Zusatzdienste.
6Wie teste ich einen Circuit Breaker ohne echten Dienstausfall?
Mit einem HTTP-Mock-Server, der gezielt Fehler oder Timeouts simuliert, kombiniert mit Integrationstests, die die Zustandsübergänge prüfen.
7Verhindert ein Circuit Breaker auch Datenkonsistenzprobleme?
Nein, er schützt nur vor kaskadierenden Ausfällen durch blockierte Ressourcen. Datenkonsistenz bei Fallback-Antworten muss separat behandelt werden.
8Wo sollte der Circuit-Breaker-Zustand gespeichert werden?
In einem zentralen Speicher wie Redis, wenn mehrere Server-Instanzen denselben externen Dienst aufrufen, damit der Zustand konsistent geteilt wird.
9Kann ein Circuit Breaker selbst zum Single Point of Failure werden?
Nur wenn sein Zustandsspeicher (etwa Redis) selbst instabil ist. Ein Fallback auf lokalen In-Memory-Zustand bei Redis-Ausfall reduziert dieses Risiko.
10Wie lange sollte die Open-Timeout-Periode sein?
Abhängig von der typischen Erholungszeit des Zieldienstes, oft 20 bis 60 Sekunden als Startwert, angepasst anhand beobachteter Ausfallmuster.