Das Circuit Breaker Pattern in Go: Verhinderung kaskadierender Ausfälle
Verhindern Sie Kaskadeneffekte in Go-Mikrodiensten.
Ein Schalter (Circuit Breaker) verhindert, dass Ihr Go-Service eine fehlerhafte Abhängigkeit übermäßig stark belastet, und stoppt damit kaskadierende Ausfälle, die Goroutinen, Sockets und Speicher verbrauchen, bis das gesamte System zusammenbricht.
Das Schwierige ist nicht der Zustandsautomat. Es geht darum zu entscheiden, wo der Schalter platziert wird, was als Fehler gilt, wie er mit Timeouts und Wiederholungsversuchen interagiert und was Ihr Service tun soll, wenn der Schalter offen ist.

In Go ist das Circuit-Breaker-Muster besonders bei ausgehenden Aufrufen nützlich: HTTP-APIs, Zahlungsgateways, Suchdienste, E-Mail-Anbieter, LLM-Gateways, interne Microservices und andere Abhängigkeiten, die langsam, überlastet oder teilweise nicht verfügbar werden können. Bei richtiger Anwendung reduziert ein Schalter kaskadierende Ausfälle. Bei falscher Anwendung wird er zu einer weiteren unklaren Fehlerquelle.
Welches Problem löst ein Circuit Breaker?
Verteilte Systeme brechen selten sauber.
Eine Abhängigkeit ist möglicherweise nicht vollständig ausgefallen. Sie könnte:
- 500-Fehler zurückgeben
- 429-Ratenbegrenzungsantworten zurückgeben
- TCP-Verbindungen akzeptieren, aber niemals antworten
- in 30 Sekunden statt in 300 Millisekunden antworten
- nur bei einigen Anfragen fehlschlagen
- überlastet sein, weil jeder Client gleichzeitig neu versucht
Der schlimmste Fall ist oft kein harter Ausfall. Es ist eine langsame Abhängigkeit.
Langsame Aufrufe verbrauchen Goroutinen, Sockets, Datenbankanbindungen, Speicher und Worker-Kapazität. Wenn Ihr Service weiterhin auf eine Abhängigkeit wartet, die bereits nicht gesund ist, kann Ihr Service ebenfalls ungesund werden.
Ein Schalter verhindert dies, indem er schnell fehlschlägt, sobald die Abhängigkeit eine Fehlergrenze überschreitet.
Statt dies für immer zu tun:
Anfrage -> Abhängigkeit aufrufen -> warten -> Timeout -> neu versuchen -> warten -> fehlschlagen
macht der Service irgendwann dies:
Anfrage -> Schalter offen -> Fallback oder Fehler sofort zurückgeben
Dieses schnelle Fehlschlagen ist nicht immer angenehm, aber es ist vorhersehbar. Vorhersehbares Fehlschlagen ist einfacher zu betreiben als ein langsamer Zusammenbruch.
Die drei Zustände des Circuit Breakers
Die meisten Schalter verwenden drei Zustände.
Geschlossen
Der Schalter ist während des normalen Betriebs geschlossen.
Anfragen werden durchgelassen. Der Schalter zeichnet Erfolge und Misserfolge auf. Wenn die Anzahl oder das Verhältnis der Fehler eine Schwelle überschreitet, öffnet sich der Schalter.
Geschlossen bedeutet nicht „für immer sicher“. Es bedeutet „Verkehr wird derzeit zugelassen“.
Offen
Der Schalter ist offen, wenn die Abhängigkeit als nicht gesund gilt.
Anfragen werden sofort abgelehnt. Der Service sollte einen Fallback, eine zwischengespeicherte Antwort, eine degradierte Antwort oder einen klaren Upstream-Fehler zurückgeben.
Offen repariert die Abhängigkeit nicht. Es gibt der Abhängigkeit Zeit zur Erholung und schützt den Aufrufer vor der Verschwendung von Ressourcen.
Halb-Offen
Nach einer Abkühlungsphase betritt der Schalter einen halb-offenen Zustand.
Nur eine begrenzte Anzahl von Testanfragen wird durchgelassen. Wenn sie erfolgreich sind, schließt sich der Schalter. Wenn sie fehlschlagen, öffnet sich der Schalter erneut.
Halb-offen ist wichtig, weil es zwei schlechte Extreme vermeidet:
- die Abhängigkeit nie wieder zu versuchen
- den vollständigen Verkehr zu schnell zurückzusenden
Die Zustandsübergänge sehen so aus:
Circuit Breaker vs. Timeout vs. Retry
Ein häufiger Fehler ist, Schalter, Wiederholungsversuche und Timeouts als austauschbar zu behandeln. Sie sind verwandt, lösen aber unterschiedliche Probleme.
Timeout
Ein Timeout begrenzt, wie lange eine Operation laufen darf.
In Go bedeutet dies in der Regel, einen context.Context mit einer Frist oder einem Timeout in den ausgehenden Aufruf zu übergeben.
Ein Timeout beantwortet diese Frage:
Wie lange bin ich bereit, auf diesen einen Aufruf zu warten?
Retry (Wiederholungsversuch)
Ein Wiederholungsversuch wiederholt eine Operation, wenn der Fehler vorübergehend sein könnte.
Wiederholungsversuche sind nützlich für kurze Netzwerkprobleme, temporäre 503-Antworten, Verbindungsresets und andere vorübergehende Fehler.
Ein Wiederholungsversuch beantwortet diese Frage:
Soll ich diesen Aufruf noch einmal versuchen?
Circuit Breaker (Schalter)
Ein Schalter stoppt Aufrufe, wenn die Abhängigkeit wahrscheinlich nicht gesund ist.
Er beantwortet diese Frage:
Soll ich diese Abhängigkeit jetzt überhaupt aufrufen?
Rate Limiter (Ratenbegrenzer)
Ein Ratenbegrenzer steuert, wie viel Verkehr über einen bestimmten Zeitraum erlaubt ist.
Er beantwortet diese Frage:
Wie viel Verkehr sollte dieser Aufrufer senden?
Bulkhead (Trennwand)
Eine Trennwand isoliert Ressourcen, damit eine Abhängigkeit nicht alles verbrauchen kann.
Sie beantwortet diese Frage:
Wie viel von meinem Service kann diese Abhängigkeit beschädigen?
Diese Muster sind am stärksten, wenn sie gemeinsam verwendet werden. Ein Schalter ohne Timeouts ist schwach. Wiederholungsversuche ohne Jitter können Retry-Storms erzeugen. Ein Fallback ohne Metriken kann einen Ausfall verdecken.
Wann man einen Circuit Breaker in Go verwendet
Verwenden Sie einen Schalter, wenn Ihr Service eine Abhängigkeit aufruft, die unabhängig von Ihrem Service fehlschlagen kann.
Gute Kandidaten sind:
- externe HTTP-APIs
- Zahlungsprozessoren
- E-Mail- und SMS-Anbieter
- Suchdienste
- Empfehlungsdienste
- LLM-Inferenzgateways
- interne Microservice-Endpunkte
- SaaS-APIs von Drittanbietern
- langsame oder überlastete Read-Side-Dienste
Schalter sind besonders nützlich, wenn der Aufrufer gracefully degradieren kann.
Zum Beispiel:
- zwischengespeicherte Produktdaten zurückgeben
- einen Empfehlungsblock überspringen
- einen Zahlungsanbieter als vorübergehend nicht verfügbar markieren
- Arbeit für später in die Warteschlange stellen
- eine partielle 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 ist:
Wenn diese Abhängigkeit fehlschlägt, sollten wir weiterhin den vollen Verkehr an sie senden?
Wenn die Antwort nein ist, kann ein Schalter helfen.
Wann man keinen Circuit Breaker verwenden sollte
Fügen Sie nicht jeder Funktion einen Schalter hinzu, nur weil das Muster verantwortungsvoll klingt.
Ein Schalter ist in der Regel nicht nützlich für:
- lokale In-Process-Funktionsaufrufe
- einfache CRUD-Operationen innerhalb eines Monolithen
- Validierungslogik
- deterministische Geschäftsregeln
- CPU-only-lokale Operationen
- Code-Pfade, bei denen kein nützlicher Fallback existiert
- Schreiboperationen, die nicht idempotent sind
- Abhängigkeiten, die bereits durch eine stärkere Workflow-Schicht geschützt sind
Ein Schalter ersetzt auch keine grundlegende Hygiene:
- Timeouts setzen
- Kontext propagieren
- Verbindungspools korrekt verwenden
- Fehler explizit behandeln
- Wiederholungsversuche sicher machen
- Fehlerraten beobachten
Ein schlechter Schalter kann ein System schwerer verständlich machen. Er kann das eigentliche Problem verdecken, Verkehr zu aggressiv ablehnen oder während der Erholung verwirrendes Verhalten erzeugen.
Die leicht opinionierte Regel ist einfach:
Fügen Sie Schalter an Abhängigkeitsgrenzen hinzu, nicht überall.
Auswahl einer Go Circuit Breaker Bibliothek
Sie können einen grundlegenden Schalter selbst implementieren, aber die meisten produktiven Go-Services sollten eine Bibliothek verwenden.
Die häufigste einfache Wahl ist sony/gobreaker.
Es bietet Ihnen:
- geschlossene, offene und halb-offene Zustände
- konfigurierbare Fehlerschwellen
- konfigurierbares Timeout im offenen Zustand
- Rückruf-Funktionen bei Zustandsänderungen
- Request-Zä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 kombinieren, wie Wiederholungsversuche, Timeouts, Fallbacks, Ratenbegrenzung, Trennwand-Isolierung und Schalter. Das kann nützlich sein, wenn Sie eine einzige Resilienzschicht um eine Operation herum wünschen.
Für viele Go-Services reicht jedoch gobreaker aus.
Vergleich von Go Circuit Breaker Paketen
Go enthält keinen integrierten Schalter in der Standardbibliothek. In der Praxis wählen Sie normalerweise zwischen einer kleinen Schalterbibliothek, einem größeren Resilienz-Framework oder einem älteren Hystrix-ähnlichen Paket.
Für die meisten neuen Go-Services ist die Entscheidung einfach:
- verwenden Sie
sony/gobreaker, wenn Sie einen kleinen, fokussierten Schalter möchten - verwenden Sie
failsafe-go, wenn Sie Schalter mit Wiederholungsversuchen, Timeouts, Fallbacks, Trennwänden, Ratenbegrenzungen und anderen Resilienzrichtlinien kombinieren möchten - vermeiden Sie neue Projekte mit
hystrix-go, es sei denn, Sie haben bereits Legacy-Code, der es verwendet
| Paket | Am besten für | Stärken | Kompromisse |
|---|---|---|---|
sony/gobreaker/v2 |
Einfache Schalter um HTTP/RPC-Clients | Kleine API, generische v2-Unterstützung, klares Zustandsmodell, leicht um Abhängigkeits-Clients zu wickeln | Löst nur das Schalter-Problem; Wiederholungsversuche, Timeouts und Fallbacks müssen separat kombiniert werden |
failsafe-go |
Vollständige Resilienzrichtlinien-Kombination | Wiederholungsversuch, Fallback, Schalter, Timeout, Trennwand, Ratenbegrenzer, Cache, Hedge, adaptiver Begrenzer und adaptiver Throttler-Richtlinien | Mehr Konzepte zu lernen; schwerer als nötig, wenn Sie nur einen grundlegenden Schalter möchten |
afex/hystrix-go |
Legacy Hystrix-Systeme | Bekannte Hystrix-Konzepte, Befehlsstil-Ausführung, historische Nutzung | Älteres Design; nicht die beste Standardeinstellung für neue Go-Services |
go-kit/kit/circuitbreaker |
Go-Kit-Endpunkt-basierte Services | Passt zum Go-Kit-Middleware-Stil und zur Endpunkt-Architektur | Meist nützlich, wenn Ihr Service bereits Go-Kit verwendet |
cep21/circuit |
Hystrix-ähnliches Schalterverhalten | Feature-reicherer Hystrix-ähnlicher Ansatz | Weniger verbreitet als einfache Standardeinstellung; möglicherweise mehr als nötig für kleine Services |
Meine Standardempfehlung ist absichtlich langweilig: Beginnen Sie mit sony/gobreaker/v2, wenn Sie nur einen Schalter benötigen. Greifen Sie zu failsafe-go, wenn Sie eine vollständige Resilienzrichtlinie an einem Ort ausdrücken möchten.
Diese Trennung hält die Architektur sauber. Ein kleiner Service-Client benötigt kein vollständiges Resilienz-Framework, nur um den Aufruf einer fehlerhaften Abhängigkeit zu stoppen. Aber ein Gateway, Aggregator, API-Client-SDK oder High-Traffic-Integrationsschicht kann von kombinierten Richtlinien profitieren.
Installation von gobreaker
Verwenden Sie das v2-Paket für neuen Code:
go get github.com/sony/gobreaker/v2
Importieren Sie es dann:
import "github.com/sony/gobreaker/v2"
Ein grundlegender 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("Schalter %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("Benutzer-Service hat %d zurückgegeben", resp.StatusCode)
}
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("Benutzer nicht gefunden")
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("Benutzer-Service Client-Fehler: %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 Produktionsclient, aber es zeigt die Struktur:
- der Schalter wickelt den ausgehenden Aufruf
- die HTTP-Anfrage erhält einen Kontext
- der HTTP-Client hat ein Timeout
- Serverseitige Fehler zählen als Schalterfehler
- Offene Schalterfehler werden in einen Anwendungsfehler abgebildet
Konfigurieren von gobreaker Einstellungen
Die wichtigsten Einstellungen sind es wert, verstanden zu werden.
Name
Name identifiziert den Schalter.
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 erlaubt sind, während der Schalter halb-offen ist.
Eine kleine Zahl ist in der Regel 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 Schalter geschlossen ist.
Wenn es null ist, werden Zähler nicht automatisch gelöscht. Ein nicht-null-Intervall gibt dem Schalter ein rollendes Speicherfenster, obwohl es nicht dasselbe ist wie eine vollständige Sliding-Window-Implementierung.
Timeout
Timeout steuert, wie lange der Schalter offen bleibt, bevor er in den halb-offenen Zustand wechselt.
Wenn das Timeout zu kurz ist, wird Ihr Service weiterhin eine Abhängigkeit prüfen, 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 optimieren Sie dann basierend auf Produktionsmetriken.
ReadyToTrip
ReadyToTrip entscheidet, wann der Schalter öffnen soll.
Eine einfache Regel sind aufeinanderfolgende Fehler:
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures >= 5
}
Das ist leicht zu verstehen, aber es mag nicht richtig sein für High-Volume-Services.
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 des Schalters nach einer winzigen Stichprobengröße.
OnStateChange
OnStateChange ist der Ort, an dem Sie Logs oder Metriken ausgeben sollten.
Mindestens aufzeichnen:
- Schaltername
- alter Zustand
- neuer Zustand
- Zeitstempel
Für Produktionssysteme, stellen Sie den Schalterzustand als Metrik zur Verfügung. Logs sind nützlich für die Fehlerbehebung, aber Metriken sind besser für Warnungen und Dashboards.
IsSuccessful
IsSuccessful lässt Sie entscheiden, welche Fehler als Fehler zählen.
Das ist wichtig.
Nicht jeder Fehler sollte den Schalter öffnen. Zum Beispiel kann ein 404 Not Found von einem Benutzer-Service ein gültiges Geschäftsergebnis sein. Ein 400 Bad Request könnte das Verschulden des Aufrufers sein, nicht das der Abhängigkeit.
Ein 503 Service Unavailable, Timeout, Verbindungsreset oder 429 Too Many Requests kann ein echtes Signal für die Gesundheit der Abhängigkeit sein.
Seien Sie hier vorsichtig. Die falschen Fehler zu zählen ist eine der einfachsten Möglichkeiten, einen lauten Schalter zu bauen.
Was sollte als Fehler zählen?
Hier kommt die ingenieurtechnische Beurteilung zum Tragen.
Zählen Sie in der Regel diese als Fehler:
- Netzwerk-Timeouts
- Verbindung abgelehnt
- Verbindung zurückgesetzt
- HTTP 500
- HTTP 502
- HTTP 503
- HTTP 504
- wiederholte 429-Antworten
- fehlerhafte Antworten von der Abhängigkeit
- Kontext-Frist überschritten während des ausgehenden Aufrufs
Zählen Sie in der Regel diese NICHT als Abhängigkeitsfehler:
- Validierungsfehler
- lokale Serialisierungsfehler
- erwartete 404-Antworten
- Autorisierungsfehler auf der Aufruferseite
- Ablehnungen von Geschäftsregeln
- Benutzereingabefehler
Der Schalter sollte die Gesundheit der Abhängigkeit repräsentieren, nicht den allgemeinen Anwendungsfehler.
Circuit Breaker und context.Context
In Go sollten Schalter context.Context nicht ersetzen.
Ein Schalter entscheidet, ob ein Aufruf versucht werden soll. Ein Kontext steuert, wie lange dieser Aufruf laufen darf und ob er stoppen soll, wenn der Aufrufer weg ist.
Ein guter ausgehender Aufruf sollte in der Regel 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 Anfragekontext
-> Service-Methode
-> Client-Methode
-> HTTP-Anfrage
-> Abhängigkeit
Vermeiden Sie die Erstellung von abgekoppelten Hintergrundkontexten innerhalb von anfragespezifischem Code. Wenn die Benutzeranfrage abgebrochen wird, sollte die nachgelagerte Arbeit in der Regel auch stoppen.
Die ruhige Regel ist:
Der Schalter schützt das System. Der Kontext schützt die Anfrage.
Sie benötigen normalerweise beide.
Circuit Breaker und Wiederholungsversuche
Wiederholungsversuche und Schalter können gut zusammenarbeiten, aber die Reihenfolge ist wichtig.
Die sicherste Standardeinstellung ist:
Timeout pro Versuch
Wiederholungsversuch mit Backoff und Jitter
Schalter um den Abhängigkeitsaufruf
Aber es gibt keine universelle Antwort. Denken Sie darüber nach, was Sie zählen möchten.
Wenn jeder Wiederholungsversuch durch den Schalter geht, kann eine Benutzeranfrage mehrere Fehler beitragen. Das kann den Schalter schneller öffnen, was gut oder schlecht sein kann.
Wenn der Schalter die gesamte Wiederholungsoperation umwickelt, sieht der Schalter einen endgültigen Erfolg oder Misserfolg pro Benutzeranfrage. Das ist ruhiger, aber es kann die Anzahl der fehlgeschlagenen Versuche verdecken.
Für viele Anwendungsdienste ist diese Form angemessen:
Benutzeranfrage
-> Schalter
-> Wiederholungsrichtlinie
-> eine HTTP-Anfrage mit Timeout
Das bedeutet, dass der Schalter verfolgt, ob die Abhängigkeitsoperation letztendlich für den Aufrufer funktioniert hat.
Für niedrigere Client-Ebenen kann diese Form auch Sinn machen:
Benutzeranfrage
-> Wiederholungsrichtlinie
-> Schalter
-> eine HTTP-Anfrage mit Timeout
Das bedeutet, dass der Schalter jeden Versuch schützt.
Die wichtigere Regel ist diese:
Versuchen Sie nicht blind neu.
Verwenden Sie:
- eine kleine maximale Anzahl von Wiederholungsversuchen
- exponentiellen Backoff
- Jitter
- Timeouts pro Versuch
- eine Gesamtanfragefrist
- Idempotenz für Schreibvorgänge
- Metriken für Wiederholungsversuche
Ohne diese kann ein kleiner Ausfall zu einem größeren werden. Für eine tiefere Behandlung der Sicherheit von Wiederholungsversuchen siehe Idempotenz in verteilten Systemen, die tatsächlich funktioniert.
Circuit Breaker und Idempotenz
Schalter erscheinen oft neben Wiederholungsversuchen, und Wiederholungsversuche werfen die Frage der Idempotenz auf.
Für Leseoperationen ist das erneute Versuchen in der Regel sicher.
Für Schreiboperationen kann das erneute Versuchen gefährlich sein.
Betrachten Sie diesen Zahlungsauftrag:
POST /charge
Wenn die Anfrage ein Timeout hat, ist die Zahlung fehlgeschlagen? Vielleicht. Ist sie erfolgreich, aber die Antwort wurde verloren? Auch vielleicht.
Wenn Sie ohne einen Idempotenz-Schlüssel neu versuchen, laden Sie möglicherweise doppelt auf.
Verwenden Sie für Schreiboperationen eine oder mehrere der folgenden Optionen:
- Idempotenz-Schlüssel
- Anfrage-IDs
- Operations-IDs
- eindeutige Einschränkungen
- transaktionaler Outbox
- Workflow-Orchestrierung
- explizite Versöhnung
Ein Schalter kann verhindern, dass Sie weiterhin einen fehlerhaften Zahlungsanbieter aufrufen, aber er kann unsichere Wiederholungsversuche nicht sicher machen.
Circuit Breaker und Fallbacks
Wenn der Schalter offen ist, braucht Ihr Service einen Plan.
Mögliche Fallback-Strategien umfassen:
- zwischengespeicherte Daten zurückgeben
- veraltete Daten mit einer Warnung zurückgeben
- einen nicht kritischen Abschnitt weglassen
- Arbeit für später in die 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 in der Regel gut:
{
"status": "temporary_unavailable",
"message": "Empfehlungen sind vorübergehend nicht verfügbar"
}
Das ist riskant:
{
"recommendations": []
}
Eine leere Liste kann wie ein gültiges Ergebnis aussehen. Sie kann einen Ausfall verdecken, Benutzer verwirren und die Fehlerbehebung erschweren.
Stille Fallbacks sind verlockend. Sie sind auch gefährlich.
Circuit Breaker und Observability
Ein Schalter ohne Observability ist im Wesentlichen ein Überraschungsgenerator.
Verfolgen Sie mindestens diese Metriken:
- aktueller Schalterzustand
- Zustandsänderungen
- erlaubte Aufrufe
- abgelehnte Aufrufe
- Erfolge
- Fehler
- Timeouts
- Fallback-Antworten
- Wiederholungsversuche
- Downstream-Latenz
- Downstream-Statuscodes
Nützliche Labels umfassen:
- Schaltername
- Abhängigkeitsname
- Operationsname
- Statusklasse
- Fehlerkategorie
Vermeiden Sie Labels mit hoher Kardinalität wie Benutzer-ID, vollständige URL, Anfrage-ID oder Rohfehlermeldungen.
Sie sollten diese Fragen von Dashboards aus beantworten können:
- Welche Schalter 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 Schalters verbessert?
- Ist das Wiederholungsvolumen vor dem Öffnen des Schalters gestiegen?
- Hat sich die Abhängigkeit erholt?
Wenn Sie den Schalter nicht beobachten können, können Sie ihn nicht optimieren. Für strukturiertes Logging, das gut mit Metriken zusammenpasst, siehe Strukturiertes Logging in Go mit slog.
Eine produktionsfreundlichere HTTP-Client-Struktur
Vermeiden Sie es bei echten Services, die Schalterlogik über Handler zu verteilen.
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 von einer domänenspezifischen Client-Methode abhängen:
type UserService interface {
GetUser(ctx context.Context, userID string) (*User, error)
}
Dann kann die Implementierung enthalten:
- HTTP-Anfrageerstellung
- Kontextpropagierung
- Schalterausführung
- Statuscode-Handhabung
- Antwortdekodierung
- Metriken
- Fehlerabbildung
Dies hält die Resilienzrichtlinie nahe an der Abhängigkeitsgrenze. Für mehr über Fehlerklassifizierung an Grenzen, siehe 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 in der Regel:
Halten Sie den Schalter nach Möglichkeit aus der Geschäftslogik heraus.
Die Geschäftsschicht sollte Domänenfehler wie verstehen:
Zahlungsanbieter nicht verfügbar
Empfehlungen nicht verfügbar
Profil-Service-Timeout
Sie muss nicht den Zustand von gobreaker 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
Häufige Fehler
Fehler 1: Kein Timeout
Ein Schalter stoppt langsame Aufrufe nicht magisch, es sei denn, die Aufrufe kehren zurück.
Wenn die ausgehende Operation für immer hängen bleiben kann, sieht der Schalter möglicherweise nicht schnell genug einen Fehler.
Verwenden Sie immer Timeouts.
Fehler 2: Ein globaler Schalter für alles
Verwenden Sie nicht einen Schalter für alle Abhängigkeiten.
Ein fehlerhafter E-Mail-Anbieter sollte den Schalter für Ihren Zahlungsanbieter nicht öffnen. Ein langsamer Suchendpunkt sollte Benutzerprofilaufrufe nicht blockieren.
Verwenden Sie separate Schalter für separate Abhängigkeitsoperationen, wenn ihre Fehlermodi unterschiedlich sind.
Fehler 3: Zählen von Aufruferfehlern als Abhängigkeitsfehler
Wenn Ihr Service schlechte Eingaben sendet und 400 Bad Request empfängt, ist das in der Regel kein Downstream-Ausfall.
Trainieren Sie den Schalter nicht auf Ihren eigenen Bugs.
Fehler 4: Erneutes Versuchen nicht-idempotenter Schreibvorgänge
Wiederholungsversuche sind nicht kostenlos. Sie können Schreibvorgänge, Zahlungen, Nachrichten oder Nebeneffekte duplizieren.
Machen Sie Schreibvorgänge idempotent, bevor Sie sie erneut versuchen.
Fehler 5: Ausfälle hinter Fallbacks verbergen
Fallbacks sollten graceful degradieren, nicht die Realität fälschen.
Wenn eine Abhängigkeit ausgefallen ist, sollten Ihre Metriken und Logs das offensichtlich machen.
Fehler 6: Optimierung ohne Produktionsdaten
Schwellenwerte, die aus Beispielen kopiert wurden, sind nur Ausgangspunkte.
Optimieren Sie basierend auf:
- Anfragevolumen
- normaler Fehlerrate
- Abhängigkeitslatenz
- Benutzerimpact
- Erholungszeit
- Fallback-Qualität
Fehler 7: Verwendung von Circuit Breakern anstelle von Kapazitätsmanagement
Ein Schalter ist kein Ersatz für:
- Lastabwurf
- Ratenbegrenzung
- Warteschlangenlimits
- Autoscaling
- Datenbank-Tuning
- Verbindungspoollimits
- Upstream-Quoten
Es ist ein Teil einer Resilienzstrategie.
Praktische Standardeinstellungen
Für einen typischen Go-Service, der eine interne HTTP-Abhängigkeit aufruft, könnte ein vernünftiger Ausgangspunkt sein:
HTTP-Client-Timeout: 2 bis 5 Sekunden
Timeout pro Anfrage-Kontext: basierend auf Aufrufer-SLA
Schalter-Fehlerregel: 5 aufeinanderfolgende Fehler oder 50 Prozent Fehler nach 20 Anfragen
Offenes Timeout: 10 bis 30 Sekunden
Halb-offene Anfragen: 1 bis 5
Anzahl der Wiederholungsversuche: 1 bis 3 Versuche
Wiederholungs-Backoff: Exponentiell mit Jitter
Das sind keine universellen Werte. Es sind eher sichere Ausgangspunkte.
Halten Sie die Gesamtbudgets für Latenz bei benutzerausgerichteten APIs eng. Für Hintergrundjobs können Sie längere Wartezeiten tolerieren. Bei Zahlungsanbietern seien Sie mit Wiederholungsversuchen und Idempotenz viel vorsichtiger.
Circuit Breaker Checkliste
Beantworten Sie diese Fragen, bevor Sie einen Schalter hinzufügen:
- Welche Abhängigkeit wird geschützt?
- Welche Operation wird geschützt?
- Welche Fehler zählen als Abhängigkeitsfehler?
- Welche Fehler sollten vom Schalter ignoriert werden?
- Welches Timeout gilt für jeden Aufruf?
- Sind Wiederholungsversuche erlaubt?
- Sind Schreibvorgänge idempotent?
- Was passiert, wenn der Schalter offen ist?
- Gibt es einen Fallback?
- Ist der Fallback in Metriken sichtbar?
- Wer wird alarmiert, wenn der Schalter weiterhin öffnet?
- Wie wird der Schalter nach der Bereitstellung optimiert?
Wenn Sie diese nicht beantworten können, kann das Hinzufügen eines Schalters mehr Verwirrung als Resilienz schaffen.
Testen von Circuit Breakern in Go
Testen Sie das Verhalten, nicht den internen Zustandsautomaten der Bibliothek.
Nützliche Tests umfassen:
- Abhängigkeit erfolgreich und Antwort wird zurückgegeben
- Abhängigkeit schlägt wiederholt fehl und Schalter öffnet sich
- Offener Schalter gibt einen temporären Fehler zurück
- Clientseitige Validierungsfehler lösen den Schalter nicht aus
- Kontext-Timeout wird respektiert
- Fallback-Antwort wird zurückgegeben, wenn erwartet
- Metriken werden bei Zustandsänderungen ausgegeben
Verwenden Sie fake HTTP-Server für Integrationstests:
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "nicht verfügbar", http.StatusServiceUnavailable)
}))
defer server.Close()
Verstecken Sie die Abhängigkeit hinter einer Schnittstelle für Unit-Tests und injizieren Sie eine fake Implementierung.
Halten Sie Tests deterministisch. Vermeiden Sie das Schlafen für lange reale Dauern. Konfigurieren Sie kurze Schalter-Timeouts in Tests. Für mehr über das Testen von koncurrentem Go-Code mit fake Zeit und isolierten Blasen, siehe Testen von koncurrentem Go-Code mit testing/synctest.
Sollten Sie Ihren eigenen Circuit Breaker bauen?
Das Bauen eines kleinen Schalters ist eine gute Lernübung. Es hilft Ihnen, den Zustandsautomaten zu verstehen.
Für Produktionscode bevorzugen Sie eine gepflegte Bibliothek, es sei denn, Ihre Bedürfnisse sind sehr spezifisch.
Ein Produktions-Schalter muss Folgendes handhaben:
- Konkurrenz
- Zustandsübergänge
- Zähler
- Halb-offene Proben
- Rückrufe
- benutzerdefinierte Fehlerklassifizierung
- race-free Verhalten
- vorhersehbares Fehlerhandling
Das ist nicht unmöglich, aber es ist leicht, subtil falsch zu machen.
Die langweilige Bibliothek ist in der Regel die bessere Wahl.
Fazit
Das Circuit-Breaker-Muster ist kein magischer Zuverlässigkeitsstaub.
In Go funktioniert es am besten, wenn es Teil eines kleinen, expliziten Resilienz-Stacks ist:
Kontext-Timeout
+ Wiederholungsversuch mit Backoff und Jitter
+ Schalter
+ Fallback
+ Metriken
Das Muster ist am nützlichsten an Abhängigkeitsgrenzen, insbesondere um entfernte Services, 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 nicht gesund ist. Verwenden Sie es, um überlasteten Systemen Raum zur Erholung zu geben.
Aber verwenden Sie es nicht als Entschuldigung, Timeouts, Idempotenz, Observability oder saubere Architektur zu ignorieren.
Ein guter Schalter macht Fehler klarer und günstiger. Ein schlechter macht sie nur mysteriöser.
Referenzen
- Go Microservices für AI/ML Orchestrierung — breiterer Orchestrierungskontext, in dem Schalter passen
- Saga-Muster in verteilten Transaktionen — Muster für verteilte Transaktionen, die mit Schaltern kombiniert werden
- Idempotenz in verteilten Systemen — Sicherheit von Wiederholungsversuchen und idempotente Operationen
- Transaktionaler Outbox-Muster in Go — zuverlässige Ereignislieferung neben Resilienzmustern
- Go Fehlerbehandlungsarchitektur — Fehlerklassifizierung an Abhängigkeitsgrenzen
- Testen von koncurrentem Go-Code mit synctest — Testen von asynchronem Verhalten mit Schaltern
- Strukturiertes Logging in Go mit slog — Observability neben Schaltern
github.com/sony/gobreaker/v2— offizielles gobreaker v2 Paket- Go Context Abbruch und Timeouts — Kontextmuster, die mit Schaltern kombiniert werden