Das Circuit Breaker Pattern in Go: Verhinderung kaskadierender Ausfälle

Verhindern Sie Kaskadeneffekte in Go-Mikrodiensten.

Inhaltsverzeichnis

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.

Schalter

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:

stateDiagram-v2 [*] --> Closed Closed --> Open: Failure threshold reached Open --> HalfOpen: Timeout elapsed HalfOpen --> Closed: Trial succeeds HalfOpen --> Open: Trial fails

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:

graph LR A[Handler] --> B[Application Service] B --> C[Dependency Client] C --> D[Circuit Breaker] D --> E[HTTP / RPC / DB / Queue]

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

Abonnieren

Neue Beiträge zu Systemen, Infrastruktur und KI-Engineering.