Circuit-Breaker-Muster in Go: Kaskadierende Ausfälle stoppen
Beenden Sie Kaskadierende Ausfälle in Go-Mikroservices.
Ein Circuit Breaker verhindert, dass Ihr Go-Dienst eine fehlerhafte Abhängigkeit mit Anfragen überflutet. Er verhindert kaskadierende Ausfälle, die Goroutinen, Sockets und Speicher verbrauchen, bis das gesamte System zusammenbricht.
Der schwierige Teil ist nicht die Zustandsmaschine. Es ist die Entscheidung darüber, wo der Breaker platziert gehört, was als Fehler zählt, wie er mit Timeouts und Wiederholungsversuchen interagiert und was Ihr Dienst tun soll, wenn die Schaltung geöffnet ist.

In Go ist das Circuit-Breaker-Muster besonders nützlich bei ausgehenden Aufrufen: HTTP-APIs, Zahlungs-Gateways, Suchdienste, E-Mail-Anbieter, LLM-Gateways, interne Microservices und andere Abhängigkeiten, die langsam, überlastet oder teilweise nicht verfügbar werden können. Richtig eingesetzt reduziert ein Circuit Breaker kaskadierende Ausfälle. Schlecht eingesetzt, wird er zu einem weiteren obskuren Ausfallmodus.
Welches Problem löst ein Circuit Breaker?
Verteilte Systeme fallen selten sauber aus.
Eine Abhängigkeit ist vielleicht nicht vollständig ausgefallen. Sie könnte:
- 500-Fehler zurückgeben
- 429-Rate-Limit-Antworten zurückgeben
- TCP-Verbindungen annehmen, aber nie antworten
- in 30 Sekunden statt in 300 Millisekunden antworten
- nur für einige Anfragen fehlschlagen
- überlastet sein, weil jeder Client gleichzeitig wiederholt
Der schlimmste Fall ist oft nicht ein harter Ausfall. Es ist eine langsame Abhängigkeit.
Langsame Aufrufe verbrauchen Goroutinen, Sockets, Datenbankverbindungen, Speicher und Worker-Kapazität. Wenn Ihr Dienst weiterhin auf eine Abhängigkeit wartet, die bereits ungesund ist, kann auch Ihr Dienst ungesund werden.
Ein Circuit Breaker verhindert dies, indem er schnell fehlschlägt, sobald die Abhängigkeit einen Schwellenwert überschreitet.
Anstatt dies endlos zu tun:
Anfrage -> Abhängigkeit aufrufen -> warten -> Timeout -> Wiederholung -> warten -> fehlschlagen
tut der Dienst schließlich Folgendes:
Anfrage -> Schaltung offen -> Fallback oder Fehler sofort zurückgeben
Dieser schnelle Fehler ist nicht immer angenehm, aber er ist vorhersehbar. Vorhersehbarer Fehler ist einfacher zu betreiben als ein langsamer Zusammenbruch.
Die drei Zustände des Circuit Breakers
Die meisten Circuit Breaker verwenden drei Zustände.
Geschlossen (Closed)
Die Schaltung ist während des normalen Betriebs geschlossen.
Anfragen werden durchgelassen. Der Breaker zeichnet Erfolge und Fehler auf. Wenn die Anzahl oder das Verhältnis der Fehler einen Schwellenwert überschreitet, öffnet sich der Breaker.
Geschlossen bedeutet nicht “für immer sicher”. Es bedeutet “Verkehr ist derzeit erlaubt”.
Offen (Open)
Die Schaltung ist offen, wenn die Abhängigkeit als ungesund gilt.
Anfragen werden sofort abgelehnt. Der Dienst sollte einen Fallback, eine zwischengespeicherte Antwort, eine degradierte Antwort oder einen klaren Upstream-Fehler zurückgeben.
Offen behebt die Abhängigkeit nicht. Es gibt der Abhängigkeit Zeit zur Erholung und schützt den Aufrufer vor dem Verschleudern von Ressourcen.
Halb offen (Half-Open)
Nach einer Abkühlphase wechselt der Breaker in einen halb offenen Zustand.
Nur eine begrenzte Anzahl von Testanfragen wird durchgelassen. Wenn diese erfolgreich sind, schließt sich der Breaker. Wenn sie fehlschlagen, öffnet sich der Breaker erneut.
Halb offen ist wichtig, um zwei schlechte Extreme zu vermeiden:
- die Abhängigkeit niemals wieder zu versuchen
- den vollen Verkehr zu schnell zurückzuschicken
Die Zustandsübergänge sehen wie folgt aus:
Circuit Breaker vs. Timeout vs. Wiederholung
Ein häufiger Fehler besteht darin, Circuit Breaker, Wiederholungen und Timeouts als austauschbar zu betrachten. Sie sind verwandt, lösen jedoch unterschiedliche Probleme.
Timeout
Ein Timeout begrenzt, wie lange eine Operation laufen darf.
In Go bedeutet dies normalerweise, einen context.Context mit einer Frist oder einem Timeout an den ausgehenden Aufruf zu übergeben.
Ein Timeout beantwortet diese Frage:
Wie lange bin ich bereit, für diesen einen Aufruf zu warten?
Wiederholung (Retry)
Eine Wiederholung führt eine Operation erneut aus, wenn der Fehler vorübergehend sein könnte.
Wiederholungen sind nützlich für kurze Netzwerkstörungen, vorübergehende 503-Antworten, Verbindungsresets und andere transiente Ausfälle.
Eine Wiederholung beantwortet diese Frage:
Sollte ich diesen Aufruf erneut versuchen?
Circuit Breaker
Ein Circuit Breaker stoppt Aufrufe, wenn die Abhängigkeit wahrscheinlich ungesund ist.
Er beantwortet diese Frage:
Sollte ich diese Abhängigkeit überhaupt jetzt aufrufen?
Rate Limiter
Ein Rate Limiter steuert, wie viel Verkehr über einen Zeitraum hinweg erlaubt ist.
Er beantwortet diese Frage:
Wie viel Verkehr sollte dieser Aufrufer senden?
Bulkhead
Eine Bulkhead isoliert Ressourcen, damit eine Abhängigkeit nicht alles verbrauchen kann.
Sie beantwortet diese Frage:
Wie viel Schaden kann diese Abhängigkeit an meinem Dienst anrichten?
Diese Muster sind am effektivsten, wenn sie gemeinsam verwendet werden. Ein Circuit Breaker ohne Timeouts ist schwach. Wiederholungen ohne Jitter können zu Wiederholungsstürmen führen. Ein Fallback ohne Metriken kann einen Ausfall verbergen.
Wann sollte man einen Circuit Breaker in Go verwenden?
Verwenden Sie einen Circuit Breaker, wenn Ihr Dienst eine Abhängigkeit aufruft, die unabhängig von Ihrem Dienst ausfallen kann.
Gute Kandidaten sind:
- externe HTTP-APIs
- Zahlungsprozessoren
- E-Mail- und SMS-Anbieter
- Suchdienste
- Empfehlungsdienste
- LLM-Inferenz-Gateways
- interne Microservice-Endpunkte
- Drittanbieter-SaaS-APIs
- langsame oder überlastete Read-Side-Dienste
Circuit Breaker sind besonders nützlich, wenn der Aufrufer elegant degradieren kann.
Zum Beispiel:
- zwischengespeicherte Produktdaten zurückgeben
- einen Empfehlungsbereich überspringen
- einen Zahlungsanbieter als vorübergehend nicht verfügbar markieren
- Arbeit für später in eine Dead-Letter-Warteschlange einreihen, als Sicherheitsnetz für Nachrichten, die auch nach der Erholung der Abhängigkeit nie erfolgreich sind
- eine teilweise Antwort zurückgeben
- schnell mit einem klaren temporären Fehler fehlschlagen
Die wichtige Frage ist nicht: “Kann dieser Aufruf fehlschlagen?” Alles kann fehlschlagen. Die bessere Frage lautet:
Wenn diese Abhängigkeit ausfällt, sollten wir weiterhin vollen Verkehr daran senden?
Wenn die Antwort nein lautet, kann ein Circuit Breaker helfen.
Wann sollte man keinen Circuit Breaker verwenden?
Fügen Sie nicht jedem Funktionsaufruf einen Circuit Breaker hinzu, nur weil das Muster verantwortungsvoll klingt.
Ein Circuit Breaker ist in der Regel nicht nützlich für:
- lokale In-Prozess-Funktionsaufrufe
- einfache CRUD-Vorgänge innerhalb eines Monolithen
- Validierungslogik
- deterministische Geschäftsregeln
- CPU-basierte lokale Vorgänge
- Codepfade, bei denen kein nützlicher Fallback existiert
- Schreibvorgänge, die nicht idempotent sind
- Abhängigkeiten, die bereits durch eine stärkere Workflow-Schicht geschützt sind
Ein Circuit Breaker ersetzt auch keine grundlegende Hygiene:
- Timeouts setzen
- Kontext weitergeben
- Verbindungspools korrekt verwenden
- Fehler explizit behandeln
- Wiederholungen sicher gestalten
- Fehlerraten beobachten
Ein schlechter Circuit Breaker kann ein System schwerer verständlich machen. Er kann das eigentliche Problem verbergen, Verkehr zu aggressiv ablehnen oder verwirrendes Verhalten während der Erzeugung erzeugen.
Die leicht eigenständige Regel ist einfach:
Fügen Sie Circuit Breaker an Abhängigkeitsgrenzen hinzu, nicht überall.
Wahl einer Go Circuit Breaker Bibliothek
Sie können einen Basis-Circuit-Breaker selbst implementieren, aber die meisten produktiven Go-Dienste sollten eine Bibliothek verwenden.
Die häufigste einfache Wahl ist sony/gobreaker.
Es bietet Ihnen:
- geschlossene, offene und halb offene Zustände
- konfigurierbare Fehlerschwellenwerte
- konfigurierbares Timeout im offenen Zustand
- Callbacks bei Zustandsänderungen
- Anfrageszähler
- generische Unterstützung in v2
- eine kleine API-Oberfläche
Für größere Resilienz-Pipelines können Sie auch Bibliotheken betrachten, die mehrere Richtlinien zusammensetzen, wie Wiederholung, Timeout, Fallback, Ratenbegrenzung, Bulkhead-Isolation und Circuit Breaking. Das kann nützlich sein, wenn Sie eine einzelne Resilienzschicht um eine Operation herum wünschen.
Für viele Go-Dienste ist jedoch gobreaker ausreichend.
Vergleich der Go Circuit Breaker Pakete
Go enthält keinen integrierten Circuit Breaker in der Standardbibliothek. In der Praxis wählen Sie عادةً zwischen einer kleinen Circuit-Breaker-Bibliothek, einem größeren Resilienz-Framework oder einem älteren Hystrix-ähnlichen Paket.
Für die meisten neuen Go-Dienste ist die Entscheidung einfach:
- verwenden Sie
sony/gobreaker, wenn Sie einen kleinen, fokussierten Circuit Breaker wünschen - verwenden Sie
failsafe-go, wenn Sie Circuit Breaker mit Wiederholungen, Timeouts, Fallbacks, Bulkheads, Ratenbegrenzungen und anderen Resilienz-Richtlinien zusammenstellen möchten - vermeiden Sie die Neuentwicklung von Projekten auf Basis von
hystrix-go, es sei denn, Sie haben bereits Legacy-Code, der es verwendet
| Paket | Beste Verwendung | Stärken | Abwägungen |
|---|---|---|---|
sony/gobreaker/v2 |
Einfache Circuit Breaker um HTTP/RPC-Clients | Kleine API, generische v2-Unterstützung, klares Zustandsmodell, einfach zum Umwickeln von Abhängigkeitsclients | Löst nur Circuit Breaking; Wiederholungen, Timeouts und Fallbacks müssen separat zusammengesetzt werden |
failsafe-go |
Vollständige Resilienz-Richtlinien-Zusammensetzung | Wiederholung, Fallback, Circuit Breaker, Timeout, Bulkhead, Rate Limiter, Cache, Hedge, adaptiver Limiter und adaptiver Throttler | Mehr Konzepte zu lernen; schwerer als nötig, wenn Sie nur einen Basis-Breaker wollen |
afex/hystrix-go |
Legacy Hystrix-ähnliche Systeme | Vertraute Hystrix-Konzepte, Befehlsausführung, historische Nutzung | Älteres Design; nicht die beste Standardwahl für neue Go-Dienste |
go-kit/kit/circuitbreaker |
Go-kit Endpunkt-basierte Dienste | Passt zum Go-kit Middleware-Stil und Endpunkt-Architektur | Meist nützlich, wenn Ihr Dienst bereits Go kit verwendet |
cep21/circuit |
Hystrix-ähnliches Circuit-Breaker-Verhalten | Funktionsreicheres Hystrix-ähnliches Ansatz | Weniger verbreitet als einfache Standardwahl; kann mehr sein als für kleine Dienste nötig |
Meine Standardempfehlung ist aus gutem Grund langweilig: Beginnen Sie mit sony/gobreaker/v2, wenn Sie nur einen Circuit Breaker benötigen. Greifen Sie zu failsafe-go, wenn Sie eine vollständige Resilienzrichtlinie an einer Stelle ausdrücken möchten.
Diese Trennung hält die Architektur sauber. Ein kleiner Service-Client benötigt kein volles Resilienz-Framework, nur um einen ausfallenden Abhängigkeitsaufruf zu stoppen. Aber ein Gateway, Aggregator, API-Client-SDK oder eine hochfrequenten Integrations-Schicht kann von zusammengesetzten Richtlinien profitieren.
Installieren von gobreaker
Verwenden Sie das v2-Paket für neuen Code:
go get github.com/sony/gobreaker/v2
Dann importieren Sie es:
import "github.com/sony/gobreaker/v2"
Ein Basis-Circuit-Breaker in Go
Hier ist ein kleines Beispiel um einen HTTP-Aufruf herum.
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/sony/gobreaker/v2"
)
var ErrTemporaryUnavailable = errors.New("Abhängigkeit vorübergehend nicht verfügbar")
type UserClient struct {
baseURL string
http *http.Client
cb *gobreaker.CircuitBreaker[[]byte]
}
func NewUserClient(baseURL string) *UserClient {
settings := gobreaker.Settings{
Name: "user-service",
MaxRequests: 3,
Interval: 30 * time.Second,
Timeout: 10 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures >= 5
},
OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
fmt.Printf("Circuit Breaker %s geändert von %s zu %s\n", name, from, to)
},
}
return &UserClient{
baseURL: baseURL,
http: &http.Client{
Timeout: 3 * time.Second,
},
cb: gobreaker.NewCircuitBreaker[[]byte](settings),
}
}
func (c *UserClient) GetUser(ctx context.Context, userID string) ([]byte, error) {
result, err := c.cb.Execute(func() ([]byte, error) {
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
c.baseURL+"/users/"+userID,
nil,
)
if err != nil {
return nil, err
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 500 {
return nil, fmt.Errorf("User-Service gab %d zurück", resp.StatusCode)
}
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("Benutzer nicht gefunden")
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("Client-Fehler im User-Service: %d", resp.StatusCode)
}
return io.ReadAll(resp.Body)
})
if errors.Is(err, gobreaker.ErrOpenState) {
return nil, ErrTemporaryUnavailable
}
if errors.Is(err, gobreaker.ErrTooManyRequests) {
return nil, ErrTemporaryUnavailable
}
return result, err
}
Dies ist kein vollständiger Produktions-Client, aber er zeigt die Struktur:
- der Breaker umwickelt den ausgehenden Aufruf
- die HTTP-Anfrage erhält einen Kontext
- der HTTP-Client hat ein Timeout
- serverseitige Fehler zählen als Breaker-Fehler
- Fehler bei offener Schaltung werden in einen Anwendungsfehler umgewandelt
Konfigurieren der gobreaker-Einstellungen
Die wichtigsten Einstellungen sind es wert, verstanden zu werden.
Name
Name identifiziert den Breaker.
Verwenden Sie einen stabilen, spezifischen Namen:
payment-api
search-service
llm-gateway
user-service
Vermeiden Sie vage Namen wie:
http-client
external-call
default
Sie werden diesen Namen in Logs und Metriken benötigen.
MaxRequests
MaxRequests steuert, wie viele Anfragen während des halb offenen Zustands erlaubt sind.
Eine kleine Zahl ist normalerweise sicherer. Der Zweck von halb offen ist es, die Erholung zu testen, nicht sofort vollen Verkehr zu senden.
Interval
Interval steuert, wann interne Zähler gelöscht werden, während der Breaker geschlossen ist.
Wenn er null ist, werden Zähler nicht automatisch gelöscht. Ein nicht-null Intervall gibt dem Breaker ein rollierendes Gedächtnisfenster, obwohl es nicht derselbe wie eine vollständige Gleitfenster-Implementierung ist.
Timeout
Timeout steuert, wie lange der Breaker offen bleibt, bevor er zu halb offen wechselt.
Wenn das Timeout zu kurz ist, wird Ihr Dienst weiterhin eine Abhängigkeit abfragen, die sich nicht erholt hat. Wenn es zu lang ist, wird die Erholung verzögert.
Beginnen Sie mit etwas Konservativem, wie 10 bis 30 Sekunden, und passen Sie es dann basierend auf Produktionsmetriken an.
ReadyToTrip
ReadyToTrip entscheidet, wann der Breaker geöffnet werden sollte.
Eine einfache Regel ist aufeinanderfolgende Fehler:
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures >= 5
}
Das ist einfach zu verstehen, passt aber möglicherweise nicht für Dienste mit hohem Volumen.
Eine andere Option ist das Fehlerverhältnis nach einer Mindestanzahl von Anfragen:
ReadyToTrip: func(counts gobreaker.Counts) bool {
total := counts.Requests
failures := counts.TotalFailures
if total < 20 {
return false
}
return float64(failures)/float64(total) >= 0.5
}
Dies vermeidet das Öffnen der Schaltung nach einer winzigen Stichprobengröße.
OnStateChange
OnStateChange ist der Ort, an dem Sie Logs oder Metriken erzeugen sollten.
Zeichnen Sie mindestens:
- Breaker-Name
- alter Zustand
- neuer Zustand
- Zeitstempel
Für Produktionssysteme stellen Sie den Breaker-Zustand als Metrik bereit. Logs sind nützlich für das Debugging, aber Metriken sind besser für Alarmierung und Dashboards.
IsSuccessful
IsSuccessful ermöglicht es Ihnen zu entscheiden, welche Fehler als Ausfälle zählen.
Das ist wichtig.
Nicht jeder Fehler sollte den Breaker öffnen. Zum Beispiel kann ein 404 Not Found vom User-Service ein gültiges Geschäftsergebnis sein. Ein 400 Bad Request könnte die Schuld des Aufrufers sein, nicht die der Abhängigkeit.
Ein 503 Service Unavailable, Timeout, Verbindungsreset oder 429 Too Many Requests kann ein echtes Signal für die Abhängigkeitsgesundheit sein.
Seien Sie hier vorsichtig. Das Zählen der falschen Fehler ist eine der einfachsten Möglichkeiten, einen lauten Circuit Breaker zu bauen.
Was sollte als Fehler zählen?
Hier kommt Ingenieursurteil ins Spiel.
Zählen Sie normalerweise diese als Fehler:
- Netzwerk-Timeouts
- Verbindung abgelehnt
- Verbindungsreset
- HTTP 500
- HTTP 502
- HTTP 503
- HTTP 504
- wiederholte 429-Antworten
- fehlerhafte Antworten von der Abhängigkeit
- Context-Deadline während des ausgehenden Aufrufs überschritten
Zählen Sie normalerweise diese nicht als Abhängigkeitsfehler:
- Validierungsfehler
- lokale Serialisierungsfehler
- erwartete 404-Antworten
- Autorisierungsfehler auf Client-Seite
- Ablehnung durch Geschäftsregeln
- Benutzer-Eingabefehler
Der Breaker sollte die Abhängigkeitsgesundheit abbilden, nicht allgemeine Anwendungsfehler.
Circuit Breaker und context.Context
In Go sollten Circuit Breaker context.Context nicht ersetzen.
Ein Circuit Breaker entscheidet, ob ein Aufruf versucht werden soll. Ein Kontext steuert, wie lange dieser Aufruf laufen darf und ob er gestoppt werden soll, wenn der Aufrufer verschwunden ist.
Ein guter ausgehender Aufruf sollte normalerweise beide haben:
ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
defer cancel()
data, err := client.GetUser(ctx, userID)
Der Kontext sollte durch die Aufrufkette fließen:
eingehender Anforderungskontext
-> Service-Methode
-> Client-Methode
-> HTTP-Anfrage
-> Abhängigkeit
Vermeiden Sie das Erstellen detachierter Hintergrundkontexte innerhalb von anfragebezogenem Code. Wenn die Benutzeranfrage abgebrochen wird, sollte die nachgelagerte Arbeit normalerweise ebenfalls gestoppt werden.
Die ruhige Regel lautet:
Der Breaker schützt das System. Der Kontext schützt die Anfrage.
Normalerweise benötigen Sie beide.
Circuit Breaker und Wiederholungen (Retries)
Wiederholungen und Circuit Breaker können gut zusammenarbeiten, aber die Reihenfolge ist wichtig.
Der sicherste Standard ist:
Timeout pro Versuch
Wiederholung mit Backoff und Jitter
Circuit Breaker um den Abhängigkeitsaufruf herum
Aber es gibt keine universelle Antwort. Überlegen Sie, was Sie zählen möchten.
Wenn jeder Wiederholungsversuch durch den Breaker läuft, kann eine Benutzeranfrage mehrere Fehler beitragen. Das kann den Breaker schneller öffnen, was gut oder schlecht sein kann.
Wenn der Breaker den gesamten Wiederholungsvorgang umwickelt, sieht der Breaker einen endgültigen Erfolg oder Fehler pro Benutzeranfrage. Das ist ruhiger, kann aber die Anzahl der fehlgeschlagenen Versuche verbergen.
Für viele Anwendungsdienste ist diese Struktur vernünftig:
Benutzeranfrage
-> Circuit Breaker
-> Wiederholungsrichtlinie
-> Ein HTTP-Versuch mit Timeout
Das bedeutet, dass der Breaker trackt, ob der Abhängigkeitsvorgang für den Aufrufer letztlich funktioniert hat.
Für niedrigere Clients kann diese Struktur auch Sinn machen:
Benutzeranfrage
-> Wiederholungsrichtlinie
-> Circuit Breaker
-> Ein HTTP-Versuch mit Timeout
Das bedeutet, dass der Breaker jeden Versuch schützt.
Die wichtigere Regel ist diese:
Wiederholen Sie nicht blind.
Verwenden Sie:
- eine kleine maximale Wiederholungszahl
- exponentiellen Backoff
- Jitter
- Timeouts pro Versuch
- eine allgemeine Anfragfrist
- Idempotenz für Schreibvorgänge
- Metriken für Wiederholungsversuche
Ohne diese können Wiederholungen einen kleinen Ausfall in einen größeren verwandeln. Für eine tiefere Behandlung der Wiederholungssicherheit siehe Idempotenz in verteilten Systemen, die tatsächlich funktioniert.
Circuit Breaker und Idempotenz
Circuit Breaker treten oft neben Wiederholungen auf, und Wiederholungen werfen die Frage der Idempotenz auf.
Für Lesevorgänge ist das Wiederholen normalerweise sicher.
Für Schreibvorgänge kann das Wiederholen gefährlich sein.
Betrachten Sie diesen Zahlungsaufruf:
POST /charge
Wenn die Anfrage timeoutet, ist die Zahlung fehlgeschlagen? Vielleicht. Hat sie erfolgreich abgeschlossen, aber die Antwort ging verloren? Auch das ist möglich.
Wenn Sie ohne Idempotenz-Schlüssel wiederholen, können Sie zweimal berechnen.
Für Schreibvorgänge verwenden Sie eine oder mehrere dieser Methoden:
- Idempotenz-Schlüssel
- Anfrages-IDs
- Vorgangs-IDs
- eindeutige Einschränkungen
- Transaktive Outbox
- Workflow-Orchestrierung
- explizite Rekoniliation
Ein Circuit Breaker kann Sie davon abhalten, einen ausfallenden Zahlungsanbieter weiterhin aufzurufen, aber er kann unsichere Wiederholungen nicht sicher machen.
Circuit Breaker und Fallbacks
Wenn die Schaltung offen ist, benötigt Ihr Dienst einen Plan.
Mögliche Fallback-Strategien sind:
- zwischengespeicherte Daten zurückgeben
- veraltete Daten mit einer Warnung zurückgeben
- einen nicht-kritischen Abschnitt auslassen
- Arbeit für später in eine Warteschlange stellen
- zu einem anderen Anbieter wechseln
- einen temporären Fehler zurückgeben
- degradierte Funktionalität anzeigen
- die Anfrage schnell fehlschlagen lassen
Ein Fallback sollte ehrlich sein.
Zum Beispiel ist dies normalerweise gut:
{
"status": "temporär_nicht_verfügbar",
"message": "Empfehlungen sind vorübergehend nicht verfügbar"
}
Dies ist riskant:
{
"recommendations": []
}
Eine leere Liste kann wie ein gültiges Ergebnis aussehen. Sie kann einen Ausfall verbergen, Benutzer verwirren und das Debugging erschweren.
Stille Fallbacks sind verlockend. Sie sind auch gefährlich.
Circuit Breaker und Observability
Ein Circuit Breaker ohne Observability ist hauptsächlich ein Überraschungsgenerator.
Verfolgen Sie mindestens diese Metriken:
- aktueller Breaker-Zustand
- Zustandsänderungen
- erlaubte Aufrufe
- abgelehnte Aufrufe
- Erfolge
- Fehler
- Timeouts
- Fallback-Antworten
- Wiederholungsversuche
- Downstream-Latenz
- Downstream-Statuscodes
Nützliche Labels sind:
- Breaker-Name
- Abhängigkeitsname
- Operationsname
- Statusklasse
- Fehlerkategorie
Vermeiden Sie Labels mit hoher Kardinalität wie Benutzer-ID, vollständige URL, Anfrages-ID oder rohe Fehlermeldungen.
Sie sollten in der Lage sein, diese Fragen aus Dashboards zu beantworten:
- Welche Circuit Breaker sind gerade offen?
- Wie oft öffnen sie sich?
- Welche Abhängigkeit hat das Öffnen verursacht?
- Sehen Benutzer Fallback-Antworten?
- Hat sich die Latenz nach dem Öffnen des Breakers verbessert?
- Ist das Wiederholungsvolumen vor dem Öffnen des Breakers gesprungen?
- Hat sich die Abhängigkeit erholt?
Wenn Sie den Breaker nicht beobachten können, können Sie ihn nicht optimieren. Für strukturiertes Logging, das gut mit Metriken passt, siehe Strukturiertes Logging in Go mit slog.
Eine produktionsfreundlichere HTTP-Client-Struktur
Vermeiden Sie für echte Dienste das Verteilen der Circuit-Breaker-Logik über Handler hinweg.
Erstellen Sie ein kleines Client-Paket um die Abhängigkeit herum.
Beispielstruktur:
internal/
userservice/
client.go
errors.go
metrics.go
Der Handler sollte die Details von gobreaker nicht kennen. Er sollte eine domänenbezogene Client-Methode abhängig machen:
type UserService interface {
GetUser(ctx context.Context, userID string) (*User, error)
}
Dann kann die Implementierung Folgendes enthalten:
- HTTP-Anfrageerstellung
- Kontext-Weitergabe
- Breaker-Ausführung
- Statuscode-Handhabung
- Antwort-Decodierung
- Metriken
- Fehlerzuordnung
Dies hält die Resilienzrichtlinie nahe an der Abhängigkeitsgrenze. Mehr zur Fehlerklassifizierung an Grenzen finden Sie in Go Fehlerbehandlungsarchitektur: Grenzen und Muster.
Wo Circuit Breaker in der Anwendungsarchitektur passen
Das Circuit-Breaker-Muster gehört an Integrationsgrenzen.
In einer Go-Anwendung bedeutet das normalerweise:
Halten Sie den Breaker nach Möglichkeit aus der Geschäftslogik heraus.
Die Geschäftsschicht sollte Domänenfehler wie diese verstehen:
Zahlungsanbieter nicht verfügbar
Empfehlungen nicht verfügbar
Profil-Service Timeout
Sie muss die gobreaker-Zustände nicht verstehen.
Diese Trennung hält die Architektur sauber:
- Transportbelange bleiben in Clients
- Resilienzrichtlinie bleibt nahe an Abhängigkeiten
- Domänenlogik bleibt lesbar
- Handler bleiben dünn
- Tests sind einfacher zu schreiben
Dieser Artikel ist Teil des Themas App Architecture in Production — zusammen mit Idempotenz-, Outbox-, Saga- und Orchestrierungsleitfäden in Integration Patterns.
Häufige Fehler
Fehler 1: Kein Timeout
Ein Circuit Breaker stoppt langsame Aufrufe nicht magisch, es sei denn, die Aufrufe kehren zurück.
Wenn der ausgehende Vorgang ewig hängen kann, sieht der Breaker einen Fehler möglicherweise nicht schnell genug.
Verwenden Sie immer Timeouts.
Fehler 2: Ein globaler Breaker für alles
Verwenden Sie nicht einen Breaker für alle Abhängigkeiten.
Ein ausfallender E-Mail-Anbieter sollte die Schaltung für Ihren Zahlungsanbieter nicht öffnen. Ein langsamer Such-Endpunkt sollte Benutzerprofilaufrufe nicht blockieren.
Verwenden Sie separate Breaker für separate Abhängigkeitsoperationen, wenn ihre Fehlermodi unterschiedlich sind.
Fehler 3: Zählen von Aufruferfehlern als Abhängigkeitsfehler
Wenn Ihr Dienst schlechte Eingaben sendet und 400 Bad Request empfängt, ist das normalerweise kein Downstream-Ausfall.
Schulen Sie den Breaker nicht auf Ihre eigenen Fehler.
Fehler 4: Wiederholen von nicht-idempotenten Schreibvorgängen
Wiederholungen sind nicht kostenlos. Sie können Schreibvorgänge, Zahlungen, Nachrichten oder Nebeneffekte duplizieren.
Machen Sie Schreibvorgänge idempotent, bevor Sie sie wiederholen.
Fehler 5: Verbergen von Ausfällen hinter Fallbacks
Fallbacks sollten elegant degradieren, nicht die Realität verfälschen.
Wenn eine Abhängigkeit ausgefallen ist, sollten Ihre Metriken und Logs das offensichtlich machen.
Fehler 6: Optimieren ohne Produktionsdaten
Schwellenwerte, die aus Beispielen kopiert wurden, sind nur Startpunkte.
Optimieren Sie basierend auf:
- Anfrages Volumen
- normaler Fehlerrate
- Abhängigkeits-Latenz
- Benutzer-Auswirkung
- Erholungszeit
- Fallback-Qualität
Fehler 7: Verwenden von Circuit Breakern statt Kapazitätsmanagement
Ein Circuit Breaker ist kein Ersatz für:
- Load Shedding
- Ratenbegrenzung
- Warteschlangengrenzen
- Autoscaling
- Datenbank-Tuning
- Verbindungspool-Grenzen
- Upstream-Quotas
Es ist ein Teil einer Resilienzstrategie.
Praktische Standardwerte
Für einen typischen Go-Dienst, der eine interne HTTP-Abhängigkeit aufruft, könnte ein vernünftiger Startpunkt sein:
HTTP-Client-Timeout: 2 bis 5 Sekunden
pro-Anfrage-Kontext-Timeout: basierend auf Caller-SLA
Breaker-Fehlerrregel: 5 aufeinanderfolgende Fehler oder 50 Prozent Fehler nach 20 Anfragen
Offenes Timeout: 10 bis 30 Sekunden
Halb-offene Anfragen: 1 bis 5
Wiederholungszahl: 1 bis 3 Versuche
Wiederholungs-Backoff: exponentiell mit Jitter
Dies sind keine universellen Werte. Sie sind sicher-ish Startpunkte.
Für benutzerorientierte APIs halten Sie die gesamten Latenzbudgets eng. Für Hintergrundjobs können Sie längere Wartezeiten tolerieren. Für Zahlungsanbieter seien Sie mit Wiederholungen und Idempotenz viel vorsichtiger.
Circuit-Breaker-Checkliste
Bevor Sie einen Circuit Breaker hinzufügen, beantworten Sie diese Fragen:
- Welche Abhängigkeit wird geschützt?
- Welcher Vorgang wird geschützt?
- Welche Fehler zählen als Abhängigkeitsfehler?
- Welche Fehler sollten vom Breaker ignoriert werden?
- Welches Timeout gilt für jeden Aufruf?
- Sind Wiederholungen erlaubt?
- Sind Schreibvorgänge idempotent?
- Was passiert, wenn die Schaltung offen ist?
- Gibt es einen Fallback?
- Ist der Fallback in Metriken sichtbar?
- Wer wird alarmiert, wenn die Schaltung weiterhin öffnet?
- Wie wird der Breaker nach dem Deployment optimiert?
Wenn Sie diese Fragen nicht beantworten können, kann das Hinzufügen eines Breakers mehr Verwirrung als Resilienz erzeugen.
Testen von Circuit Breakern in Go
Testen Sie das Verhalten, nicht die interne Zustandsmaschine der Bibliothek.
Nützliche Tests sind:
- Abhängigkeit gelingt und Antwort wird zurückgegeben
- Abhängigkeit fehlschlägt wiederholt und Schaltung öffnet sich
- offene Schaltung gibt einen temporären Fehler zurück
- Client-seitige Validierungsfehler lösen den Breaker nicht aus
- Context-Timeout wird respektiert
- Fallback-Antwort wird wie erwartet zurückgegeben
- Metriken werden bei Zustandsänderungen erzeugt
Verwenden Sie Fake-HTTP-Server für Integrationstests:
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "unavailable", http.StatusServiceUnavailable)
}))
defer server.Close()
Für Unit-Tests verbergen Sie die Abhängigkeit hinter einer Schnittstelle und injizieren eine Fake-Implementierung.
Halten Sie Tests deterministisch. Vermeiden Sie das Schlafen für lange reale Zeiträume. Konfigurieren Sie kurze Breaker-Timeouts in Tests. Mehr zum Testen von Go-Code mit Fake-Zeit und isolierten Blasen finden Sie in Testen von gleichzeitigem Go-Code mit testing/synctest.
Sollten Sie Ihren eigenen Circuit Breaker bauen?
Einen kleinen Circuit Breaker zu bauen, ist eine gute Übung. Es hilft Ihnen, die Zustandsmaschine zu verstehen.
Für Produktionscode bevorzugen Sie eine gewartete Bibliothek, es sei denn, Ihre Anforderungen sind sehr spezifisch.
Ein Produktions-Breaker muss Folgendes handhaben:
- Parallelität
- Zustandsübergänge
- Zähler
- Halb-Offen-Sonden
- Callbacks
- benutzerdefinierte Fehlerklassifizierung
- Race-freies Verhalten
- vorhersehbare Fehlerbehandlung
Das ist nicht unmöglich, aber es ist leicht, subtil falsch zu liegen.
Die langweilige Bibliothek ist normalerweise die bessere Wahl.
Fazit
Das Circuit-Breaker-Muster ist keine magische Zuverlässigkeitsstaub.
In Go funktioniert es am besten, wenn es Teil eines kleinen, expliziten Resilienz-Stacks ist:
Context-Timeout
+ Wiederholung mit Backoff und Jitter
+ Circuit Breaker
+ Fallback
+ Metriken
Das Muster ist am nützlichsten an Abhängigkeitsgrenzen, insbesondere um Remote-Dienste herum, die langsam oder teilweise nicht verfügbar werden können.
Verwenden Sie es, um kaskadierende Ausfälle zu stoppen. Verwenden Sie es, um schnell zu fehlschlagen, wenn eine Abhängigkeit eindeutig ungesund ist. Verwenden Sie es, um überlasteten Systemen Raum zur Erholung zu geben.
Aber verwenden Sie es nicht als Ausrede, Timeouts, Idempotenz, Observability oder saubere Architektur zu ignorieren.
Ein guter Circuit Breaker macht Fehler klarer und günstiger. Ein schlechter macht Fehler nur mysteriöser.
Referenzen
- Go Microservices for AI/ML Orchestration — breiterer Orchestrierungskontext, in den Circuit Breaker passen
- Saga Pattern in Distributed Transactions — verteilte Transaktionsmuster, die mit Circuit Breakern zusammenarbeiten
- Idempotency in Distributed Systems — Wiederholungssicherheit und idempotente Vorgänge
- Transactional Outbox Pattern in Go — zuverlässige Ereignislieferung neben Resilienzmustern
- Go Error Handling Architecture — Fehlerklassifizierung an Abhängigkeitsgrenzen
- Testing Concurrent Go Code with synctest — Testen von Async-Verhalten mit Circuit Breakern
- Structured Logging in Go with slog — Observability neben Circuit Breakern
github.com/sony/gobreaker/v2— offizielles gobreaker v2-Paket- Go Context Cancellation and Timeouts — Kontextmuster, die mit Circuit Breakern zusammenarbeiten