Aufbau robuster Systeme mit Framework-Level Circuit Breakers
Wenhao Wang
Dev Intern · Leapcell

Einleitung
In der komplexen Welt moderner verteilter Systeme kann ein einzelner Ausfallpunkt schnell zu einem flächendeckenden Ausfall eskalieren. Dienste kommunizieren ständig miteinander, und die Nichtverfügbarkeit oder langsame Reaktion einer Komponente kann vorgeschaltete Dienste unverhältnismäßig stark beeinträchtigen, was zu einem Dominoeffekt führt, der als kaskadierender Ausfall bekannt ist. Stellen Sie sich eine E-Commerce-Plattform vor, auf der der Inventardienst nicht mehr reagiert. Wenn der Bestellverarbeitungsdienst immer wieder fehlerhafte Anfragen an das Inventar stellt, können seine eigenen Ressourcen aufgebraucht werden, was ihn langsam oder nicht verfügbar macht. Dies kann wiederum die benutzeroberflächengestützte Storefront beeinträchtigen und zu einem vollständigen Systemzusammenbruch führen. Die Verhinderung solcher Szenarien ist entscheidend für die Aufrechterhaltung der Systemstabilität und die Gewährleistung einer positiven Benutzererfahrung. Dieser Artikel befasst sich damit, wie wir diese Risiken proaktiv mindern können, indem wir das Circuit-Breaker-Muster direkt in unseren Backend-Frameworks implementieren und so Fehler effektiv eindämmen und deren Ausbreitung verhindern.
Grundlegende Konzepte verstehen
Bevor wir uns mit den Implementierungsdetails befassen, wollen wir ein gemeinsames Verständnis der wichtigsten Begriffe schaffen.
- Verteiltes System: Ein System, dessen Komponenten sich auf verschiedenen vernetzten Computern befinden und die ihre Aktionen durch den Austausch von Nachrichten koordinieren.
- Kaskadierender Ausfall: Ein Ausfall in einem System, der sich durch aufeinanderfolgende Stufen ausbreitet, seine Auswirkungen fortpflanzt und potenziell ein ganzes miteinander verbundenes System zum Absturz bringt.
- Resilienz: Die Fähigkeit eines Systems, sich von Fehlern zu erholen und weiter zu funktionieren, möglicherweise mit reduzierter Kapazität, anstatt vollständig auszufallen.
- Circuit-Breaker-Muster: Ein Architekturmuster, das verhindern soll, dass eine Anwendung wiederholt versucht, eine Operation auszuführen, die wahrscheinlich fehlschlägt. Es umschließt einen Funktionsaufruf, der fehlschlagen kann, und überwacht die Fehler. Wenn die Fehler einen bestimmten Schwellenwert erreichen, löst der Circuit Breaker aus, und alle nachfolgenden Aufrufe der umschlossenen Funktion geben sofort einen Fehler zurück, ohne einen Versuch zu unternehmen. Dies gibt dem fehlerhaften Dienst Zeit zur Erholung und verhindert, dass der aufrufende Dienst Ressourcen für zum Scheitern verurteilte Aufrufe verschwendet.
Das Circuit-Breaker-Muster arbeitet in drei Zuständen:
- Closed: In diesem Zustand erlaubt der Circuit Breaker, dass Anfragen an die geschützte Operation weitergegeben werden. Wenn ein Fehler auftritt, zeichnet der Circuit Breaker ihn auf. Wenn die Anzahl der Fehler innerhalb eines bestimmten Zeitfensters einen vordefinierten Schwellenwert überschreitet, wechselt der Circuit Breaker in den Zustand Open.
- Open: In diesem Zustand schlägt der Circuit Breaker alle Anfragen sofort fehl, ohne die geschützte Operation aufzurufen. Nach einem konfigurierten Timeout wechselt er in den Zustand Half-Open.
- Half-Open: In diesem Zustand erlaubt der Circuit Breaker einer begrenzten Anzahl von Testanfragen, die geschützte Operation weiterzugeben. Wenn diese Testanfragen erfolgreich sind, setzt der Circuit Breaker den Zustand auf Closed zurück. Schlagen sie fehl, kehrt er sofort für eine weitere Timeout-Periode in den Zustand Open zurück.
Framework-Level Circuit Breaker implementieren
Die Implementierung von Circuit Breakern auf Framework-Ebene bietet erhebliche Vorteile. Sie zentralisiert die Logik zur Fehlertoleranz, reduziert Boilerplate-Code für einzelne Dienste und stellt die konsistente Anwendung des Musters im gesamten System sicher. Wir verwenden eine hypothetische Microservice-Architektur, die in Go mit der Hystrix-Bibliothek geschrieben ist (obwohl die Prinzipien breit auf andere Sprachen und Frameworks wie Java Resilience4j oder Python Tenacity anwendbar sind).
Betrachten Sie ein Szenario, in dem unser Order Service einen Payment Service aufrufen muss. Wir möchten den Order Service vor Fehlern im Payment Service schützen.
Zuerst definieren wir unseren Payment Service-Client.
// payment_client.go package main import ( "errors" "fmt" time "time" ) // PaymentServiceClient simuliert Aufrufe eines externen Zahlungsdienstes type PaymentServiceClient interface { ProcessPayment(orderID string, amount float64) error } type mockPaymentServiceClient struct { failRequests bool failRate int // Prozentsatz der fehlgeschlagenen Anfragen latency time.Duration callCount int } func NewMockPaymentServiceClient(failRequests bool, failRate int, latency time.Duration) *mockPaymentServiceClient { return &mockPaymentServiceClient{ failRequests: failRequests, failRate: failRate, latency: latency, } } func (m *mockPaymentServiceClient) ProcessPayment(orderID string, amount float64) error { m.callCount++ time.Sleep(m.latency) if m.failRequests && m.callCount%100 < m.failRate { fmt.Printf("PaymentServiceClient: Simulating failure for order %s\n", orderID) return errors.New("payment service unavailable or timed out") } if m.callCount%10 == 0 { // Simuliert gelegentlichen Erfolg auch bei Fehlern zum Testen des Half-Open-Zustands fmt.Printf("PaymentServiceClient: Payment processed successfully for order %s\n", orderID) } else { fmt.Printf("PaymentServiceClient: Payment processed successfully for order %s\n", orderID) } return nil }
Nun integrieren wir Hystrix auf Framework-Ebene, vielleicht innerhalb eines benutzerdefinierten HTTP-Clients oder eines Service-Wrappers.
// main.go package main import ( "fmt" "log" time "time" "github.com/afex/hystrix-go/hystrix" ) // PaymentServiceCircuitBreakerClient umschließt den tatsächlichen Payment Service Client mit Hystrix type PaymentServiceCircuitBreakerClient struct { paymentClient PaymentServiceClient commandName string } func NewPaymentServiceCircuitBreakerClient(client PaymentServiceClient, commandName string) *PaymentServiceCircuitBreakerClient { // Konfiguriere Hystrix für diesen spezifischen Befehl hystrix.ConfigureCommand(commandName, hystrix.CommandConfig{ Timeout: 1000, // Timeout für die Befehlsausführung (ms) MaxConcurrentRequests: 10, // Max. erlaubte gleichzeitige Anfragen RequestVolumeThreshold: 5, // Mindestanzahl von Anfragen in einem rollierenden statistischen Fenster, um den Circuitbreaker auszulösen ErrorPercentThreshold: 50, // Prozentsatz der Fehler, um den Circuitbreaker auszulösen SleepWindow: 5000, // Zeit in Millisekunden, nachdem der Circuit geöffnet wurde, in der Hystrix dann einer einzelnen Anfrage erlaubt, durchzugehen }) return &PaymentServiceCircuitBreakerClient{ paymentClient: client, commandName: commandName, } } func (c *PaymentServiceCircuitBreakerClient) ProcessPayment(orderID string, amount float64) error { var err error err = hystrix.Do(c.commandName, func() error { // Dies ist der eigentliche Aufruf des Zahlungsdienstes return c.paymentClient.ProcessPayment(orderID, amount) }, func(e error) error { // Dies ist die Fallback-Funktion. Wird aufgerufen, wenn der Befehl fehlschlägt oder der Circuit geöffnet ist. log.Printf("Fallback ausgelöst für Auftrag %s aufgrund von Fehler: %v", orderID, e) // Hier könnten Sie den Fehler protokollieren, die Zahlung zur erneuten Versuche in eine Warteschlange stellen oder eine Standardantwort zurückgeben. return fmt.Errorf("Zahlungsverarbeitung für Auftrag %s ausgelöst: %w", orderID, e) }) return err } func main() { fmt.Println("Starte Payment Service Circuit Breaker Demodemonstration") // Simuliere Ausfälle und Latenzen des Zahlungsdienstes // Zuerst lassen wir ihn häufig fehlschlagen mockClient := NewMockPaymentServiceClient(true, 70, 50*time.Millisecond) // Umschließe den Client mit dem Circuit Breaker cbClient := NewPaymentServiceCircuitBreakerClient(mockClient, "payment_service_process_payment") fmt.Println("\n--- Phase 1: Hohe Fehlerrate ---") // Simuliere viele Anfragen, um den Circuit auszulösen for i := 0; i < 20; i++ { orderID := fmt.Sprintf("order-%d", i) err := cbClient.ProcessPayment(orderID, 100.0) if err != nil { fmt.Printf("Fehler bei der Zahlungsbearbeitung für %s: %v\n", orderID, err) } else { fmt.Printf("Zahlungsbearbeitung für %s erfolgreich abgeschlossen\n", orderID) } time.Sleep(100 * time.Millisecond) // Simuliere eine leichte Verzögerung zwischen den Anfragen } fmt.Println("\n--- Status Circuit Breaker ---") // Nach einiger Zeit sollte der Circuit geöffnet sein. // Das Hystrix-Dashboard oder Metriken würden dies in einem echten System anzeigen. // Für diese Demo beobachten wir die Fallback-Nachrichten. time.Sleep(2 * time.Second) // Gib dem Circuit etwas Zeit, sich zu öffnen fmt.Println("\n--- Phase 2: Circuit Open - Anfragen werden sofort abgelehnt ---") for i := 20; i < 30; i++ { orderID := fmt.Sprintf("order-%d", i) err := cbClient.ProcessPayment(orderID, 100.0) if err != nil { fmt.Printf("Fehler bei der Zahlungsbearbeitung für %s: %v\n", orderID, err) } else { fmt.Printf("Zahlungsbearbeitung für %s erfolgreich abgeschlossen\n", orderID) } time.Sleep(50 * time.Millisecond) } fmt.Println("\n--- Phase 3: Warten auf SleepWindow, um Half-Open zu ermöglichen ---") fmt.Println("Simuliere die Wiederherstellung des Zahlungsdienstes. Reduziere die Fehlerrate.") // Simuliere die Wiederherstellung des Zahlungsdienstes mockClient.failRequests = false // Keine Fehler mockClient.failRate = 0 time.Sleep(6 * time.Second) // Warte über Hystrix' SleepWindow (5 Sekunden) hinaus fmt.Println("\n--- Phase 4: Half-Open State - Testanfragen gesendet, Circuit sollte schließen ---") for i := 30; i < 40; i++ { orderID := fmt.Sprintf("order-%d", i) err := cbClient.ProcessPayment(orderID, 100.0) if err != nil { fmt.Printf("Fehler bei der Zahlungsbearbeitung für %s: %v\n", orderID, err) } else { fmt.Printf("Zahlungsbearbeitung für %s erfolgreich abgeschlossen\n", orderID) } time.Sleep(100 * time.Millisecond) } fmt.Println("\nDemo beendet.") }
In diesem Beispiel:
- Wir definieren eine
PaymentServiceClient-Schnittstelle und einenmockPaymentServiceClient, um Netzwerkanrufe und Fehler zu simulieren. PaymentServiceCircuitBreakerClientfungiert als Framework-Wrapper. Er nimmt eine tatsächlichePaymentServiceClient-Instanz und einencommandNameentgegen.hystrix.ConfigureCommandrichtet die Schwellenwerte des Circuit Breakers für einen bestimmten Befehlsnamen ein. Diese Konfiguration erfolgt einmal, normalerweise beim Start der Anwendung oder der Dienstinitialisierung.- Die Methode
ProcessPaymentverwendet dannhystrix.Do, um die tatsächliche Zahlungslogik auszuführen. Sie bietet auch einefallback-Funktion, die aufgerufen wird, wenn der primäre Befehl fehlschlägt oder der Circuit geöffnet ist. Der Fallback verhindert, dass der aufrufende Dienst blockiert oder sofort fehlschlägt.
Die Ausgabe wird deutlich zeigen:
- Anfängliche Fehler, die zum Öffnen des Circuits führen.
- Anfragen, die sofort mit Fallback-Fehlern abgelehnt werden, wenn der Circuit geöffnet ist.
- Nach dem
SleepWindowkönnen einige Testanfragen durchgehen (Half-Open), und wenn sie erfolgreich sind, schließt sich der Circuit.
Anwendungsszenarien:
- Aufrufe externer APIs: Schützen Sie Ihre Dienste vor unzuverlässigen Drittanbieter-APIs.
- Datenbankzugriff: Verhindern Sie eine Überlastung der Datenbank bei langsamen Abfragen oder Verbindungsproblemen.
- Inter-Service-Kommunikation: Schützen Sie vorgeschaltete Dienste vor Ausfällen in nachgelagerten Microservices.
- Caching-Schichten: Wenn Ihr Cache-Dienst nicht verfügbar wird, kann der Circuit Breaker direkte Datenbankabfragen verhindern, bis er sich erholt, und gegebenenfalls veraltete Daten oder einen Fallback verwenden.
Fazit
Die Implementierung des Circuit-Breaker-Musters auf Framework-Ebene ist eine wirkungsvolle Strategie zum Aufbau robuster Backend-Systeme. Sie kapselt die Fehlerbehandlung, bietet einen konsistenten Ansatz zur Fehlertoleranz und verhindert vor allem, dass kleinere Probleme zu katastrophalen kaskadierenden Ausfällen eskalieren. Indem Fehler isoliert und sofortiges Feedback oder Fallback-Mechanismen bereitgestellt werden, ermöglichen Circuit Breaker, dass Ihre Anwendungen bei widrigen Bedingungen anstatt abzustürzen, anmutig degradieren, was ihre Stabilität und Zuverlässigkeit erheblich verbessert. Nutzen Sie dieses Muster, um Systeme zu entwickeln, die nicht nur funktionieren, sondern wirklich bestehen.

