Het Circuit Breaker-patroon in Go: Cascade-foutsituaties voorkomen

Voorkom cascadefouten in Go-microservices.

Inhoud

Een circuitbreker voorkomt dat je Go-service een falende afhankelijkheid blijft bombarderen, waardoor cascaderende falingen worden voorkomen die goroutines, sockets en geheugen consumeren totdat het hele systeem instort.

Het lastige deel is niet de statemachine. Het is beslissen waar de breker thuishoort, wat als fout wordt beschouwd, hoe deze interacteert met time-outs en herpogingen, en wat je service moet doen wanneer de circuit is geopend.

circuit breaker

In Go is het circuitbrekerpatroon vooral handig bij uitgaande calls: HTTP-API’s, betalingsgateways, zoekservices, e-mailproviders, LLM-gateways, interne microservices en andere afhankelijkheden die traag, overbelast of gedeeltelijk onbeschikbaar kunnen worden. Bij goed gebruik vermindert een circuitbreker cascaderende falingen. Bij slecht gebruik wordt het een obscure faalmodus.

Welk probleem lost een circuitbreker op?

Gedistribueerde systemen falen zelden schoon.

Een afhankelijkheid is misschien niet helemaal down. Het kan:

  • 500-fouten teruggeven
  • 429 rate limit-responses teruggeven
  • TCP-verbindingen accepteren maar nooit antwoorden
  • in 30 seconden reageren in plaats van 300 milliseconden
  • alleen falen voor sommige requests
  • overbelast zijn omdat elke client tegelijkertijd opnieuw probeert

Het ergste geval is vaak geen harde fout. Het is een trage afhankelijkheid.

Trage calls consumeren goroutines, sockets, databaseverbindingen, geheugen en werkcapaciteit. Als je service blijft wachten op een afhankelijkheid die al ongezond is, kan je service ook ongezond worden.

Een circuitbreker voorkomt dit door snel te falen nadat de afhankelijkheid een foutdrempel overschrijdt.

In plaats van dit voor altijd te doen:

request -> call afhankelijkheid -> wachten -> time-out -> opnieuw proberen -> wachten -> falen

doet de service uiteindelijk dit:

request -> circuit geopend -> retourneer fallback of fout direct

Dat snelle falen is niet altijd prettig, maar het is voorspelbaar. Voorspelbaar falen is makkelijker te beheren dan een trage ineenstorting.

De drie toestanden van een circuitbreker

De meeste circuitbrekers gebruiken drie toestanden.

Gesloten

Het circuit is gesloten tijdens normale werking.

Requests worden doorgeleid. De breker registreert successen en fouten. Als het aantal of de verhouding van fouten een drempel overschrijdt, opent de breker.

Gesloten betekent niet “voor altijd veilig”. Het betekent “verkeer wordt momenteel toegestaan”.

Geopend

Het circuit is geopend wanneer de afhankelijkheid als ongezond wordt beschouwd.

Requests worden direct afgewezen. De service moet een fallback, gecachte response, gedegradeerde response of een duidelijke upstream-fout teruggeven.

Geopend lost het probleem van de afhankelijkheid niet op. Het geeft de afhankelijkheid tijd om te herstellen en beschermt de aanroeper tegen het verspillen van resources.

Half-open

Na een afkoelperiode gaat de breker over in een half-open staat.

Alleen een beperkt aantal proefrequests worden doorgeleid. Als ze slagen, sluit de breker. Als ze falen, opent de breker opnieuw.

Half-open is belangrijk omdat het twee slechte uitersten vermijdt:

  • de afhankelijkheid nooit meer proberen
  • volledig verkeer te snel terugsturen

De toestandsveranderingen zien er zo uit:

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

Circuitbreker vs Time-out vs Retry

Een veelgemaakte fout is het behandelen van circuitbrekers, retries en time-outs als uitwisselbaar. Ze zijn gerelateerd, maar lossen verschillende problemen op.

Time-out

Een time-out begrenst hoe lang één operatie kan draaien.

In Go betekent dit meestal het doorgeven van een context.Context met een deadline of time-out aan de uitgaande call.

Een time-out beantwoordt deze vraag:

Hoe lang ben ik bereid te wachten op deze ene call?

Retry

Een retry herhaalt een operatie wanneer de fout tijdelijk kan zijn.

Retries zijn handig voor korte netwerkglitches, tijdelijke 503-responses, connection resets en andere transient fouten.

Een retry beantwoordt deze vraag:

Moet ik deze call opnieuw proberen?

Circuitbreker

Een circuitbreker stopt calls wanneer de afhankelijkheid waarschijnlijk ongezond is.

Het beantwoordt deze vraag:

Moet ik deze afhankelijkheid nu überhaupt aanroepen?

Rate Limiter

Een rate limiter regelt hoeveel verkeer over tijd wordt toegestaan.

Het beantwoordt deze vraag:

Hoeveel verkeer moet deze aanroeper verzenden?

Bulkhead

Een bulkhead isoleert resources zodat één afhankelijkheid niet alles kan consumeren.

Het beantwoordt deze vraag:

Hoeveel van mijn service kan deze afhankelijkheid beschadigen?

Deze patronen zijn het sterkst wanneer ze samen worden gebruikt. Een circuitbreker zonder time-outs is zwak. Retries zonder jitter kunnen retry-stormen creëren. Een fallback zonder metrics kan een outage verbergen.

Wanneer je een circuitbreker in Go moet gebruiken

Gebruik een circuitbreker wanneer je service een afhankelijkheid aanroept die onafhankelijk van je service kan falen.

Goede kandidaten zijn:

  • externe HTTP-API’s
  • betalingsverwerkers
  • e-mail- en SMS-providers
  • zoekservices
  • aanbevelingservices
  • LLM-inferentiegateways
  • interne microservice-endpoints
  • third-party SaaS-API’s
  • trage of overbelaste read-side services

Circuitbrekers zijn vooral handig wanneer de aanroeper gracieus kan degraderen.

Bijvoorbeeld:

  • gecachte productdata teruggeven
  • een aanbevelingsblok overslaan
  • een betalingsprovider als tijdelijk onbeschikbaar markeren
  • werk voor later in de wachtrij plaatsen
  • een partiële response teruggeven
  • snel falen met een duidelijke tijdelijke fout

De belangrijke vraag is niet “kan deze call falen?” Alles kan falen. De betere vraag is:

Als deze afhankelijkheid faalt, moeten we dan volledig verkeer naar deze sturen?

Als het antwoord nee is, kan een circuitbreker helpen.

Wanneer je geen circuitbreker moet gebruiken

Voeg geen circuitbreker toe aan elke functie alleen omdat het patroon verantwoord klinkt.

Een circuitbreker is meestal niet nuttig voor:

  • lokale in-process functiecalls
  • simpele CRUD binnen een monolith
  • validatielogica
  • deterministische businessregels
  • CPU-only lokale operaties
  • codepaden waar geen nuttige fallback bestaat
  • write-operaties die niet idempotent zijn
  • afhankelijkheden die al beschermd worden door een sterkere workflowlaag

Een circuitbreker vervangt ook geen basis hygiëne:

  • time-outs instellen
  • context propageren
  • connection pools correct gebruiken
  • fouten expliciet afhandelen
  • retries veilig maken
  • faalfrequenties observeren

Een slechte circuitbreker kan een systeem moeilijker te doorzien maken. Het kan het echte probleem verbergen, verkeer te agressief afwijzen of verwarrend gedrag creëren tijdens herstel.

De iets gepolariseerde regel is simpel:

Voeg circuitbrekers toe aan afhankelijkheidsranden, niet overal.

Het kiezen van een Go circuitbrekerbibliotheek

Je kunt een basis circuitbreker zelf implementeren, maar de meeste productie-Go-services moeten een bibliotheek gebruiken.

De meest voorkomende simpele keuze is sony/gobreaker.

Het geeft je:

  • gesloten, geopend en half-open toestanden
  • configureerbare foutdrempels
  • configureerbare time-out voor de geopende staat
  • callbacks voor toestandswijzigingen
  • request tellers
  • generische ondersteuning in v2
  • een klein API-oppervlak

Voor grotere resilience-pipelines kun je ook kijken naar bibliotheken die meerdere policies samenstellen, zoals retry, time-out, fallback, rate limiting, bulkhead-isolatie en circuit breaking. Dat kan nuttig zijn wanneer je een enkele resilience-laag rond een operatie wilt.

Voor veel Go-services is gobreaker echter voldoende.

Go circuitbrekerpakketten vergeleken

Go bevat geen ingebouwde circuitbreker in de standaardbibliotheek. In de praktijk kies je meestal tussen een kleine circuitbrekerbibliotheek, een groter resilience-framework of een oudere Hystrix-stijl pakket.

Voor de meeste nieuwe Go-services is de beslissing simpel:

  • gebruik sony/gobreaker als je een kleine, gefocuste circuitbreker wilt
  • gebruik failsafe-go als je circuitbrekers wilt combineren met retries, time-outs, fallbacks, bulkheads, rate limits en andere resilience policies
  • vermijd het starten van nieuwe projecten met hystrix-go tenzij je al legacy code hebt die deze gebruikt
Pakket Best voor Sterke punten Trade-offs
sony/gobreaker/v2 Simpele circuitbrekers rond HTTP/RPC-clients Klein API, generische v2-ondersteuning, duidelijk toestandsmodel, eenvoudig om afhankelijkheidsclients te wrappen Lost alleen circuit breaking op; retries, time-outs en fallbacks moeten apart worden samengesteld
failsafe-go Volledige resilience policy-compositie Retry, fallback, circuitbreker, time-out, bulkhead, rate limiter, cache, hedge, adaptive limiter en adaptive throttler policies Meer concepten om te leren; zwaarder dan nodig als je alleen een basis breker wilt
afex/hystrix-go Legacy Hystrix-stijl systemen Bekende Hystrix-concepten, command-stijl uitvoering, historisch gebruik Ouder design; niet de beste standaard voor nieuwe Go-services
go-kit/kit/circuitbreaker Go kit endpoint-based services Past bij Go kit middleware-stijl en endpoint-architectuur Voornamelijk nuttig als je service al Go kit gebruikt
cep21/circuit Hystrix-achtig circuitbrekergedrag Meer uitgebreide Hystrix-stijl aanpak Minder gebruikelijk als simpele standaard; kan meer zijn dan nodig voor kleine services

Mijn standaardadvies is opzettelijk saai: begin met sony/gobreaker/v2 als je alleen een circuitbreker nodig hebt. Ga voor failsafe-go als je een complete resilience policy op één plek wilt uitdrukken.

Die split houdt de architectuur schoon. Een kleine serviceclient heeft geen volledig resilience-framework nodig om alleen te stoppen met het aanroepen van een falende afhankelijkheid. Maar een gateway, aggregator, API-client SDK of high-traffic integratielaag kan profiteren van samengestelde policies.

Installeren van gobreaker

Gebruik het v2-pakket voor nieuwe code:

go get github.com/sony/gobreaker/v2

Importeer het vervolgens:

import "github.com/sony/gobreaker/v2"

Een basis circuitbreker in Go

Hier is een klein voorbeeld rond een HTTP-call.

package main

import (
    "context"
    "errors"
    "fmt"
    "io"
    "net/http"
    "time"

    "github.com/sony/gobreaker/v2"
)

var ErrTemporaryUnavailable = errors.New("afhankelijkheid tijdelijk onbeschikbaar")

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("circuitbreker %s veranderd van %s naar %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 gaf %d terug", resp.StatusCode)
        }

        if resp.StatusCode == http.StatusNotFound {
            return nil, fmt.Errorf("gebruiker niet gevonden")
        }

        if resp.StatusCode >= 400 {
            return nil, fmt.Errorf("user service client fout: %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
}

Dit is geen complete productieclient, maar het toont de vorm:

  • de breker wrapt de uitgaande call
  • de HTTP-request ontvangt een context
  • de HTTP-client heeft een time-out
  • server-side fouten tellen als brekerfouten
  • geopende circuitfouten worden gemapt naar een applicatiefout

Configureren van gobreaker-instellingen

De belangrijkste instellingen zijn het waard om te begrijpen.

Name

Name identificeert de breker.

Gebruik een stabiele, specifieke naam:

payment-api
search-service
llm-gateway
user-service

Vermijd vage namen zoals:

http-client
external-call
default

Je wilt deze naam in logs en metrics.

MaxRequests

MaxRequests regelt hoeveel requests worden toegestaan terwijl de breker half-open is.

Een klein aantal is meestal veiliger. Het doel van half-open is om herstel te testen, niet om direct volledig verkeer te sturen.

Interval

Interval regelt wanneer interne tellers worden gewist terwijl de breker gesloten is.

Als het nul is, worden tellers niet automatisch gewist. Een niet-nul interval geeft de breker een rollend geheugenvenster, hoewel het niet hetzelfde is als een volledige sliding window-implementatie.

Timeout

Timeout regelt hoe lang de breker geopend blijft voordat hij overgaat naar half-open.

Als de time-out te kort is, zal je service blijven proeven van een afhankelijkheid die niet heeft hersteld. Als het te lang is, zal herstel worden vertraagd.

Begin met iets conservatiefs, zoals 10 tot 30 seconden, en tune vervolgens op basis van productiemetrics.

ReadyToTrip

ReadyToTrip beslist wanneer de breker moet openen.

Een simpele regel is opeenvolgende fouten:

ReadyToTrip: func(counts gobreaker.Counts) bool {
    return counts.ConsecutiveFailures >= 5
}

Dat is makkelijk te doorzien, maar het is misschien niet juist voor high-volume services.

Een andere optie is foutverhouding na een minimaal aantal requests:

ReadyToTrip: func(counts gobreaker.Counts) bool {
    total := counts.Requests
    failures := counts.TotalFailures

    if total < 20 {
        return false
    }

    return float64(failures)/float64(total) >= 0.5
}

Dit voorkomt dat het circuit opent na een zeer kleine steekproefomvang.

OnStateChange

OnStateChange is waar je logs of metrics zou moeten uitsturen.

Registreer ten minste:

  • breker naam
  • oude staat
  • nieuwe staat
  • tijdstip

Voor productsystemen, expose brekerstaat als metric. Logs zijn nuttig voor debugging, maar metrics zijn beter voor alerting en dashboards.

IsSuccessful

IsSuccessful laat je beslissen welke fouten als falen tellen.

Dit is belangrijk.

Niet elke fout moet de breker openen. Bijvoorbeeld, een 404 Not Found van een user service kan een geldig business-resultaat zijn. Een 400 Bad Request kan de schuld van de aanroeper zijn, niet de afhankelijkheid.

Een 503 Service Unavailable, time-out, connection reset of 429 Too Many Requests kan een echt afhankelijkheidsgezondheidssignaal zijn.

Wees hier voorzichtig. Het tellen van de verkeerde fouten is een van de makkelijkste manieren om een lawaaiige circuitbreker te bouwen.

Wat moet als fout worden geteld?

Hier is engineering judgement belangrijk.

Tel meestal deze als fouten:

  • netwerk time-outs
  • connection refused
  • connection reset
  • HTTP 500
  • HTTP 502
  • HTTP 503
  • HTTP 504
  • herhaalde 429-responses
  • misvormde responses van de afhankelijkheid
  • context deadline exceeded tijdens de uitgaande call

Tel meestal deze niet als afhankelijkheidsfouten:

  • validatiefouten
  • lokale serialisatiefouten
  • verwachte 404-responses
  • aanroeper-side autorisatiefouten
  • businessregelafwijzingen
  • gebruikersinputfouten

De breker moet afhankelijkheidsgezondheid vertegenwoordigen, niet algemene applicatiefouten.

Circuitbrekers en context.Context

In Go mogen circuitbrekers context.Context niet vervangen.

Een circuitbreker beslist of er een call moet worden geprobeerd. Een context regelt hoe lang die call mag draaien en of deze moet stoppen wanneer de aanroeper weg is.

Een goede uitgaande call moet meestal beide hebben:

ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
defer cancel()

data, err := client.GetUser(ctx, userID)

De context moet door de callketen stromen:

inkomend request context
-> service methode
-> client methode
-> HTTP request
-> afhankelijkheid

Vermijd het creëren van losse background contexts binnen request-scoped code. Als het gebruikersrequest wordt geannuleerd, moet het downstream werk meestal ook stoppen.

De kalme regel is:

De breker beschermt het systeem. De context beschermt het request.

Je hebt normaal gesproken beide nodig.

Circuitbrekers en Retries

Retries en circuitbrekers kunnen goed samenwerken, maar de volgorde is belangrijk.

De veiligste standaard is:

time-out per poging
retry met backoff en jitter
circuitbreker rond de afhankelijkheidscall

Maar er is geen universeel antwoord. Denk na over wat je wilt tellen.

Als elke retry-poging door de breker gaat, kan één gebruikersrequest meerdere fouten bijdragen. Dat kan de breker sneller openen, wat goed of slecht kan zijn.

Als de breker de hele retry-operatie wrapt, ziet de breker één definitieve succes of fout per gebruikersrequest. Dat is rustiger, maar het kan het aantal mislukte pogingen verbergen.

Voor veel applicatieservices is deze vorm redelijk:

gebruikersrequest
-> circuitbreker
   -> retry policy
      -> één HTTP-poging met time-out

Dat betekent dat de breker bijhoudt of de afhankelijkheidsoperatie uiteindelijk werkte voor de aanroeper.

Voor lower-level clients kan deze vorm ook zinvol zijn:

gebruikersrequest
-> retry policy
   -> circuitbreker
      -> één HTTP-poging met time-out

Dat betekent dat de breker elke poging beschermt.

De belangrijkere regel is deze:

Probeer niet blindelings opnieuw.

Gebruik:

  • een klein maximaal aantal retries
  • exponentiële backoff
  • jitter
  • time-outs per poging
  • een algeheel request-deadline
  • idempotentie voor writes
  • metrics voor retry-pogingen

Zonder die dingen kan een retry een kleine outage omzetten in een grotere. Voor een dieper behandeling van retry-veiligheid, zie Idempotentie in gedistribueerde systemen die daadwerkelijk werkt.

Circuitbrekers en Idempotentie

Circuitbrekers verschijnen vaak naast retries, en retries roepen de vraag van idempotentie op.

Voor read-operaties is het opnieuw proberen meestal veilig.

Voor write-operaties kan het opnieuw proberen gevaarlijk zijn.

Overweeg deze betalingscall:

POST /charge

Als het request time-out geeft, is de betaling mislukt? Misschien. Is het geslaagd maar het antwoord verloren? Ook misschien.

Als je opnieuw probeert zonder een idempotentie-key, kun je twee keer debiteren.

Voor write-operaties, gebruik één of meer van deze:

  • idempotentie-keys
  • request IDs
  • operatie IDs
  • unieke constraints
  • transactionele outbox
  • workflow orchestration
  • expliciete reconciliatie

Een circuitbreker kan voorkomen dat je blijft bellen met een falende betalingsprovider, maar het kan onveilige retries niet veilig maken.

Circuitbrekers en Fallbacks

Wanneer het circuit geopend is, heeft je service een plan nodig.

Mogelijke fallback-strategieën zijn:

  • gecachte data teruggeven
  • verouderde data teruggeven met een waarschuwing
  • een niet-kritieke sectie weglaten
  • werk voor later in de wachtrij plaatsen
  • overschakelen naar een andere provider
  • een tijdelijke fout teruggeven
  • gedegradeerde functionaliteit tonen
  • het request snel laten falen

Een fallback moet eerlijk zijn.

Bijvoorbeeld, dit is meestal goed:

{
  "status": "temporary_unavailable",
  "message": "Aanbevelingen zijn tijdelijk onbeschikbaar"
}

Dit is riskant:

{
  "recommendations": []
}

Een lege lijst kan er uitzien als een geldig resultaat. Het kan een outage verbergen, gebruikers verwarren en debugging moeilijker maken.

Stille fallbacks zijn verleidelijk. Ze zijn ook gevaarlijk.

Circuitbrekers en Observability

Een circuitbreker zonder observability is voornamelijk een verrassingengenerator.

Track ten minste deze metrics:

  • huidige brekerstaat
  • toestandswijzigingen
  • toegestane calls
  • afgewezen calls
  • successen
  • fouten
  • time-outs
  • fallback-responses
  • retry-pogingen
  • downstream latentie
  • downstream statuscodes

Nuttige labels zijn:

  • breker naam
  • afhankelijkheidsnaam
  • operatienaam
  • statusklasse
  • foutcategorie

Vermijd high-cardinality labels zoals gebruikers-ID, volledige URL, request-ID of ruwe foutberichten.

Je moet deze vragen kunnen beantwoorden vanuit dashboards:

  • Welke circuitbrekers zijn nu geopend?
  • Hoe vaak openen ze?
  • Welke afhankelijkheid veroorzaakte het openen?
  • Zien gebruikers fallback-responses?
  • Is de latentie verbeterd nadat de breker opende?
  • Is het retry-volume gestegen voordat de breker opende?
  • Is de afhankelijkheid hersteld?

Als je de breker niet kunt observeren, kun je hem niet tunen. Voor gestructureerd logging dat goed past bij metrics, zie Gestructureerd logging in Go met slog.

Een meer productie-vriendelijke HTTP-clientvorm

Voor echte services, vermijd het verspreiden van circuitbrekerlogica over handlers.

Maak een klein client-pakket rond de afhankelijkheid.

Voorbeeldstructuur:

internal/
  userservice/
    client.go
    errors.go
    metrics.go

De handler moet de details van gobreaker niet kennen. Het moet afhankelijk zijn van een domein-level clientmethode:

type UserService interface {
    GetUser(ctx context.Context, userID string) (*User, error)
}

Dan kan de implementatie bevatten:

  • HTTP-request creatie
  • context propagatie
  • breker uitvoering
  • statuscode afhandeling
  • response decoding
  • metrics
  • foutmapping

Dit houdt de resilience-policy dicht bij de afhankelijkheidsrand. Voor meer over foutclassificatie aan randen, zie Go Error Handling Architecture: Boundaries and Patterns.

Waar circuitbrekers passen in applicatie-architectuur

Het circuitbrekerpatroon hoort thuis aan integratiegrenzen.

In een Go-applicatie betekent dat meestal:

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

Houd de breker buiten businesslogic wanneer mogelijk.

De businesslaag moet domeinfouten begrijpen zoals:

betalingsprovider onbeschikbaar
aanbevelingen onbeschikbaar
profielservice time-out

Het hoeft niet gobreaker-toestanden te begrijpen.

Deze scheiding houdt de architectuur schoon:

  • transportzorgpunten blijven in clients
  • resilience-policy blijft bij afhankelijkheden
  • dominelogica blijft leesbaar
  • handlers blijven dun
  • tests zijn makkelijker te schrijven

Veelgemaakte fouten

Fout 1: Geen Time-out

Een circuitbreker stopt trage calls niet magisch tenzij de calls terugkeren.

Als de uitgaande operatie voor altijd kan hangen, ziet de breker misschien niet snel genoeg een fout.

Gebruik altijd time-outs.

Fout 2: Eén globale breker voor alles

Gebruik niet één breker voor alle afhankelijkheden.

Een falende e-mailprovider moet het circuit niet openen voor je betalingsprovider. Een trage zoekendpoint moet profielcalls niet blokkeren.

Gebruik aparte brekers voor aparte afhankelijkheidsoperaties wanneer hun faalmodi verschillen.

Fout 3: Aanroeperfouten tellen als afhankelijkheidsfouten

Als je service slechte input stuurt en 400 Bad Request ontvangt, is dat meestal geen downstream-outage.

Train de breker niet op je eigen bugs.

Fout 4: Niet-idempotente writes opnieuw proberen

Retries zijn niet gratis. Ze kunnen writes, betalingen, berichten of side effects dupliceren.

Maak writes idempotent voordat je ze opnieuw probeert.

Fout 5: Outages verbergen achter fallbacks

Fallbacks moeten gracieus degraderen, niet de realiteit vervalsen.

Als een afhankelijkheid down is, moeten je metrics en logs dat duidelijk maken.

Fout 6: Tunen zonder productiedata

Drempels gekopieerd van voorbeelden zijn alleen startpunten.

Tune op basis van:

  • requestvolume
  • normale foutfrequentie
  • afhankelijkheidslatentie
  • gebruikersimpact
  • hersteltijd
  • fallback-kwaliteit

Fout 7: Circuitbrekers gebruiken in plaats van capaciteitsbeheer

Een circuitbreker is geen vervanging voor:

  • load shedding
  • rate limiting
  • wachtrijlimieten
  • autoscaling
  • database-tuning
  • connection pool limieten
  • upstream quotas

Het is één onderdeel van een resilience-strategie.

Praktische standaarden

Voor een typische Go-service die een interne HTTP-afhankelijkheid aanroept, kan een redelijk startpunt zijn:

HTTP-client time-out: 2 tot 5 seconden
per-request context time-out: gebaseerd op aanroeper SLA
breker faalregel: 5 opeenvolgende fouten of 50 procent fout na 20 requests
open time-out: 10 tot 30 seconden
half-open requests: 1 tot 5
retry count: 1 tot 3 pogingen
retry backoff: exponentieel met jitter

Dit zijn geen universele waarden. Ze zijn veilige startpunten.

Voor user-facing APIs, houd totale latentiebudgetten strak. Voor background jobs, kun je langere wachttijden tolereren. Voor betalingsproviders, wees veel voorzichtiger met retries en idempotentie.

Circuitbreker checklist

Voordat je een circuitbreker toevoegt, beantwoord deze vragen:

  • Welke afhankelijkheid wordt beschermd?
  • Welke operatie wordt beschermd?
  • Welke fouten tellen als afhankelijkheidsfout?
  • Welke fouten moeten door de breker worden genegeerd?
  • Welke time-out geldt voor elke call?
  • Zijn retries toegestaan?
  • Zijn writes idempotent?
  • Wat gebeurt er wanneer het circuit geopend is?
  • Is er een fallback?
  • Is de fallback zichtbaar in metrics?
  • Wie krijgt een alert als het circuit blijft openen?
  • Hoe zal de breker worden getuned na deploy?

Als je deze niet kunt beantwoorden, kan het toevoegen van een breker meer verwarring creëren dan resilience.

Circuitbrekers testen in Go

Test gedrag, niet de interne statemachine van de bibliotheek.

Nuttige tests zijn:

  • afhankelijkheid slaagt en response wordt teruggegeven
  • afhankelijkheid faalt herhaaldelijk en circuit opent
  • geopend circuit geeft een tijdelijke fout terug
  • client-side validatiefouten trippen de breker niet
  • context time-out wordt gerespecteerd
  • fallback response wordt teruggegeven wanneer verwacht
  • metrics worden uitgezonden bij toestandswijzigingen

Gebruik fake HTTP-servers voor integratie-stijl tests:

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    http.Error(w, "onbeschikbaar", http.StatusServiceUnavailable)
}))
defer server.Close()

Voor unit tests, verberg de afhankelijkheid achter een interface en injecteer een fake implementatie.

Houd tests deterministisch. Vermijd het slapen voor lange echte duur. Configureer korte breker time-outs in tests. Voor meer over het testen van concurrente Go-code met fake time en geïsoleerde bubbles, zie Concurrente Go-code testen met testing/synctest.

Zou je je eigen circuitbreker moeten bouwen?

Het bouwen van een kleine circuitbreker is een goede oefening. Het helpt je de statemachine te begrijpen.

Voor productcode, prefer een onderhouden bibliotheek tenzij je behoeften zeer specifiek zijn.

Een productbreker moet omgaan met:

  • concurrentie
  • toestandsovergangen
  • tellers
  • half-open proeven
  • callbacks
  • custom foutclassificatie
  • race-free gedrag
  • voorspelbare foutafhandeling

Dat is niet onmogelijk, maar het is makkelijk subtiel fout te krijgen.

De saaie bibliotheek is meestal de betere keuze.

Conclusie

Het circuitbrekerpatroon is geen magische betrouwbaarheidsstof.

In Go werkt het het beste wanneer het deel is van een kleine, expliciete resilience-stack:

context time-out
+ retry met backoff en jitter
+ circuitbreker
+ fallback
+ metrics

Het patroon is het nuttigst aan afhankelijkheidsranden, vooral rond remote services die traag of gedeeltelijk onbeschikbaar kunnen worden.

Gebruik het om cascaderende fouten te stoppen. Gebruik het om snel te falen wanneer een afhankelijkheid duidelijk ongezond is. Gebruik het om overbelaste systemen ruimte te geven om te herstellen.

Maar gebruik het niet als excuus om time-outs, idempotentie, observability of schone architectuur te negeren.

Een goede circuitbreker maakt falen duidelijker en goedkoper. Een slechte maakt falen alleen maar mysterieuzer.

Referenties

Abonneren

Ontvang nieuwe berichten over systemen, infrastructuur en AI-engineering.