Circuit Breaker-mönstret i Go: Stoppa kaskadfall

Stoppa kaskadfel i Go-mikrotjänster.

Sidinnehåll

En strömbrytare stoppar ditt Go-tjänste från att bombas mot en misslyckad beroendekomponent, vilket förhindrer kaskadfall som förbrukar goroutiner, socketar och minne tills hela systemet kollapsar.

Den svåra delen är inte tillståndsmaskinen. Det handlar om att avgöra var strömbrytaren hör hemma, vad som räknas som ett fel, hur den interagerar med timeout och återförsök, och vad din tjänst ska göra när strömbrytaren är öppen.

strömbrytare

I Go är strömbrytarmönstret särskilt användbart för utåtriktade anrop: HTTP-API:er, betalningsgatewayar, söktjänster, e-postleverantörer, LLM-gatewayar, interna mikrotjänster och andra beroenden som kan bli långsamma, överbelastade eller delvis otillgängliga. Används väl minskar en strömbrytare kaskadfall. Används dåligt blir den ett ytterligare oklart felmeddelande.

Vilket problem löser en strömbrytare?

Distribuerade system misslyckas sällan på ett rent sätt.

Ett beroende kanske inte är helt nere. Det kan:

  • returnera 500-fel
  • returnera 429-svar för hastighetsbegränsning
  • acceptera TCP-anslutningar men aldrig svara
  • svara på 30 sekunder istället för 300 millisekunder
  • misslyckas endast för vissa begäran
  • vara överbelastad eftersom varje klient försöker igen samtidigt

Det värsta fallet är ofta inte ett hårt fel. Det är ett långsamt beroende.

Långsamma anrop förbrukar goroutiner, socketar, databasanslutningar, minne och kapacitet för arbetstrådar. Om din tjänst fortsätter vänta på ett beroende som redan är ohälsosamt, kan din tjänst också bli ohälsosam.

En strömbrytare förhindrar detta genom att misslyckas snabbt efter att beroendet har passerat en felgräns.

Istället för att göra detta för evigt:

begäran -> anropa beroende -> vänta -> timeout -> försök igen -> vänta -> misslyckas

gör tjänsten till slut detta:

begäran -> strömbrytare öppen -> returnera fallback eller fel omedelbart

Detta snabba misslyckande är inte alltid trevligt, men det är förutsägbart. Förutsägbart misslyckande är lättare att operera än en långsam kollaps.

De tre tillstånden för en strömbrytare

De flesta strömbrytare använder tre tillstånd.

Stängd

Strömbrytaren är stängd under normal drift.

Begäran släpps igenom. Brytaren registrerar lyckade och misslyckade försök. Om antalet eller förhållandet mellan fel passerar en tröskel öppnas brytaren.

Stängd betyder inte “säker för evigt”. Det betyder “trafik är för närvarande tillåten”.

Öppen

Strömbrytaren är öppen när beroendet anses vara ohälsosamt.

Begäran avslås omedelbart. Tjänsten bör returnera en fallback, cachat svar, nedgraderat svar eller ett tydligt upstream-fel.

Öppen lägger inte fixa beroendet. Det ger beroendet tid att återhämta sig och skyddar anroparen från att slösa resurser.

Halvöppen

Efter en avkylningsperiod går brytaren in i ett halvöppet tillstånd.

Endast ett begränsat antal provbegäran släpps igenom. Om de lyckas stängs brytaren. Om de misslyckas öppnas brytaren igen.

Halvöppen är viktigt eftersom det undviker två dåliga extrema:

  • aldrig försöka med beroendet igen
  • skicka full trafik tillbaka för snabbt

Tillståndstransitionerna ser ut så här:

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

Strömbrytare vs Timeout vs Återförsök

Ett vanligt misstag är att behandla strömbrytare, återförsök och timeouts som utbytbara. De är relaterade, men de löser olika problem.

Timeout

En timeout begränsar hur länge en operation kan köras.

I Go innebär detta oftast att man skickar in en context.Context med ett deadline eller timeout i det utåtriktade anropet.

En timeout svarar på denna fråga:

Hur länge är jag villig att vänta på detta enskilda anrop?

Återförsök

Ett återförsök upprepar en operation när felet kan vara tillfälligt.

Återförsök är användbara för korta nätverksstörningar, tillfälliga 503-svar, återställning av anslutningar och andra tillfälliga fel.

Ett återförsök svarar på denna fråga:

Ska jag försöka med detta anrop igen?

Strömbrytare

En strömbrytare stoppar anrop när beroendet troligen är ohälsosamt.

Den svarar på denna fråga:

Ska jag anropa detta beroende alls just nu?

Hastighetsbegränsare

En hastighetsbegränsare styr hur mycket trafik som tillåts över tid.

Den svarar på denna fråga:

Hur mycket trafik ska denna anropare skicka?

Bulkhead

En bulkhead isolerar resurser så att ett beroende inte kan förbruka allt.

Den svarar på denna fråga:

Hur mycket av min tjänst kan detta beroende skada?

Dessa mönster är starkast när de används tillsammans. En strömbrytare utan timeouts är svag. Återförsök utan jitter kan skapa återförsöksstormar. En fallback utan metrisk kan dölja en driftstopp.

När ska man använda en strömbrytare i Go

Använd en strömbrytare när din tjänst anropar ett beroende som kan misslyckas oberoende av din tjänst.

God kandidater inkluderar:

  • externa HTTP-API:er
  • betalningsprocessorer
  • e-post- och SMS-leverantörer
  • söktjänster
  • rekommendationstjänster
  • LLM-inferensgatewayar
  • interna mikrotjänständpunkter
  • tredjeparts SaaS-API:er
  • långsamma eller överbelastade lässida-tjänster

Strömbrytare är särskilt användbara när anroparen kan degradera smidigt.

Till exempel:

  • returnera cachad produktdata
  • hoppa över ett rekommendationsblock
  • markera en betalningsleverantör som tillfälligt otillgänglig
  • köa arbete för senare
  • returnera ett delvis svar
  • misslyckas snabbt med ett tydligt tillfälligt fel

Den viktiga frågan är inte “kan detta anrop misslyckas?”. Allt kan misslyckas. Den bättre frågan är:

Om detta beroende misslyckas, ska vi fortsätta skicka full trafik till det?

Om svaret är nej, kan en strömbrytare hjälpa.

När ska man inte använda en strömbrytare

Lägg inte till en strömbrytare till varje funktion bara för att mönstret låter ansvarsfullt.

En strömbrytare är oftast inte användbar för:

  • lokala in-process-funktionsanrop
  • enkel CRUD inuti en monolit
  • valideringslogik
  • deterministiska affärsregler
  • CPU-enkla lokala operationer
  • kodsökvägar där ingen användbar fallback finns
  • skrivoperationer som inte är idempotenta
  • beroenden som redan skyddas av ett starkare arbetsflödesskikt

En strömbrytare ersätter också inte grundläggande hygien:

  • sätt timeouts
  • propagera context
  • använd anslutningspooler korrekt
  • hantera fel explicit
  • gör återförsök säkra
  • observera felhastigheter

En dålig strömbrytare kan göra ett system svårare att resonera kring. Den kan dölja det verkliga problemet, avslå trafik för aggressivt eller skapa förvirrande beteende under återhämtning.

Den något åsiktsfulla regeln är enkel:

Lägg till strömbrytare vid gränserna för beroenden, inte överallt.

Att välja en Go-strömbrytarbibliotek

Du kan implementera en grundläggande strömbrytare själv, men de flesta produktions Go-tjänster bör använda ett bibliotek.

Det vanligaste enkla valet är sony/gobreaker.

Det ger dig:

  • stängda, öppna och halvöppna tillstånd
  • konfigurerbara feltrösklar
  • konfigurerbar timeout för öppet tillstånd
  • callback-funktioner för tillståndsförändringar
  • begäranräknare
  • generiskt stöd i v2
  • en liten API-yta

För större resilienspipeliner kan du också titta på bibliotek som sammansätter flera policyer, såsom återförsök, timeout, fallback, hastighetsbegränsning, bulkhead-isolering och strömbrytning. Det kan vara användbart när du vill ha ett enda resiliensskikt runt en operation.

För många Go-tjänster räcker dock gobreaker.

Jämförelse av Go-strömbrytarpaket

Go inkluderar inte en inbyggd strömbrytare i standardbiblioteket. I praktiken väljer du oftast mellan ett litet strömbrytarbibliotek, ett större resiliensramverk eller ett äldre Hystrix-styld paket.

För de flesta nya Go-tjänster är beslutet enkelt:

  • använd sony/gobreaker om du vill ha en liten, fokuserad strömbrytare
  • använd failsafe-go om du vill ha strömbrytare sammansatta med återförsök, timeouts, fallbacks, bulkheads, hastighetsbegränsningar och andra resilienspolicyer
  • undvik att starta nya projekt med hystrix-go om du inte redan har legacy-kod som använder det
Paket Bästa för Styrkor Avvägningar
sony/gobreaker/v2 Enkla strömbrytare runt HTTP/RPC-klienter Liten API, generiskt v2-stöd, tydligt tillståndsmodell, lätt att wrapa beroendeklienter Löser endast strömbrytning; återförsök, timeouts och fallbacks måste sammansättas separat
failsafe-go Full resilienspolicy-sammansättning Återförsök, fallback, strömbrytare, timeout, bulkhead, hastighetsbegränsare, cache, hedge, adaptiv begränsare och adaptiv throttle-policy Mer koncept att lära sig; tyngre än nödvändigt om du bara vill ha en grundläggande brytare
afex/hystrix-go Legacy Hystrix-stylda system Bekanta Hystrix-koncept, kommandostyld exekvering, historisk användning Äldre design; inte det bästa standardvalet för nya Go-tjänster
go-kit/kit/circuitbreaker Go-kit-baserade tjänster med ändpunkter Passar Go-kit-middlaware-stil och ändpunktarkitektur Mest användbart om din tjänst redan använder Go-kit
cep21/circuit Hystrix-liknande strömbrytarbeteende Mer funktionell Hystrix-styld approach Mindre vanligt som enkelt standardval; kan vara mer än nödvändigt för små tjänster

Mitt standardrekommendation är avsiktligt tråkigt: börja med sony/gobreaker/v2 när du bara behöver en strömbrytare. Välj failsafe-go när du vill uttrycka en komplett resilienspolicy på en plats.

Denna uppdelning håller arkitekturen ren. En liten tjänsteklient behöver inte ett fullständigt resiliensramverk bara för att sluta anropa ett misslyckat beroende. Men en gateway, aggregator, API-klient SDK eller ett högt trafikerat integrationsskikt kan gynnas av sammansatta policyer.

Installera gobreaker

Använd v2-paketet för ny kod:

go get github.com/sony/gobreaker/v2

Importera det sedan:

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

En grundläggande strömbrytare i Go

Här är ett litet exempel kring ett HTTP-anrop.

package main

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

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

var ErrTemporaryUnavailable = errors.New("dependency temporarily unavailable")

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 changed from %s to %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 returned %d", resp.StatusCode)
        }

        if resp.StatusCode == http.StatusNotFound {
            return nil, fmt.Errorf("user not found")
        }

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

Detta är inte en komplett produktionsklient, men det visar formen:

  • brytaren wrapar det utåtriktade anropet
  • HTTP-begäran får en context
  • HTTP-klienten har en timeout
  • server-sida-fel räknas som brytare-fel
  • öppna strömbrytare-fel mappas till ett applikationsfel

Konfigurera gobreaker-inställningar

De viktigaste inställningarna är värt att förstå.

Name

Name identifierar brytaren.

Använd ett stabilt, specifikt namn:

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

Undvik vaga namn som:

http-client
external-call
default

Du kommer att vilja ha detta namn i loggar och metrisk.

MaxRequests

MaxRequests styr hur många begäran som tillåts medan brytaren är halvöppen.

Ett litet antal är oftast säkrare. Syftet med halvöppen är att testa återhämtning, inte att skicka full trafik omedelbart.

Interval

Interval styr när interna räkningar nollställs medan brytaren är stängd.

Om det är noll nollställs räkningarna inte automatiskt. Ett icke-noll intervall ger brytaren ett rullande minnesfönster, även om det inte är samma sak som en fullständig glidande fönsterimplementation.

Timeout

Timeout styr hur länge brytaren stannar öppen innan den flyttar till halvöppen.

Om timeouten är för kort kommer din tjänst att fortsätta sondra ett beroende som inte har återhämtat sig. Om den är för lång kommer återhämtningen att fördröjas.

Börja med något konservativt, som 10 till 30 sekunder, och finjustera sedan från produktionsmetrisk.

ReadyToTrip

ReadyToTrip avgör när brytaren ska öppnas.

En enkel regel är konsekutiva fel:

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

Det är lätt att resonera kring, men det kan inte vara rätt för högvolumtjänster.

Ett annat alternativ är felprocent efter ett minimum antal begäran:

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

    if total < 20 {
        return false
    }

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

Detta undviker att öppna strömbrytaren efter en liten stickprovstorlek.

OnStateChange

OnStateChange är där du bör emittera loggar eller metrisk.

Som minst, registrera:

  • brytarnamn
  • gammalt tillstånd
  • nytt tillstånd
  • tidsstämpel

För produktionsystem, exponera brytarstatus som en metrisk. Loggar är användbara för felsökning, men metrisk är bättre för aviseringar och dashboards.

IsSuccessful

IsSuccessful låter dig avgöra vilka fel som räknas som fel.

Detta är viktigt.

Inte varje fel bör öppna brytaren. Till exempel kan en 404 Not Found från en användartjänst vara ett giltigt affärsresultat. En 400 Bad Request kan vara anroparens fel, inte beroendets fel.

En 503 Service Unavailable, timeout, anslutningsåterställning eller 429 Too Many Requests kan vara en verklig beroendehälsosignal.

Var försiktig här. Att räkna fel fel är ett av de lättaste sätten att bygga en bullrig strömbrytare.

Vad ska räknas som fel?

Här spelar ingenjörsmässigt omdöme in.

Räkna oftast dessa som fel:

  • nätverkstimeouts
  • anslutning nekad
  • anslutning återställd
  • HTTP 500
  • HTTP 502
  • HTTP 503
  • HTTP 504
  • upprepade 429-svar
  • felaktiga svar från beroendet
  • context deadline exceeded under det utåtriktade anropet

Räkna oftast inte dessa som beroendefel:

  • valideringsfel
  • lokala serialiseringsfel
  • förväntade 404-svar
  • anroparsida-autoriseringar
  • avslag av affärsregler
  • användarinmatningsfel

Brytaren ska representera beroendets hälsa, inte allmänt applikationsfel.

Strömbrytare och context.Context

I Go ska strömbrytare inte ersätta context.Context.

En strömbrytare avgör om ett anrop ska försökas. En context styr hur länge det anropet får köras och om det ska stoppas när anroparen är borta.

Ett bra utåtriktat anrop bör oftast ha båda:

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

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

Contexten ska flöda genom anropkedjan:

inkommande begäran context
-> tjänstemetod
-> klientmetod
-> HTTP-begäran
-> beroende

Undvik att skapa lösa bakgrundscontexter inuti begäran-omfattande kod. Om användarens begäran avbryts, bör den nedströms arbete oftast också stoppas.

Den lugna regeln är:

Brytaren skyddar systemet. Contexten skyddar begäran.

Du behöver normalt båda.

Strömbrytare och återförsök

Återförsök och strömbrytare kan fungera bra tillsammans, men ordningen spelar roll.

Den säkraste standardinställningen är:

timeout per försök
återförsök med backoff och jitter
strömbrytare runt beroende-anropet

Men det finns inget universellt svar. Tänk på vad du vill räkna.

Om varje återförsöksförsök passerar genom brytaren, kan en användarbeggäran bidra med flera fel. Det kan öppna brytaren snabbare, vilket kan vara bra eller dåligt.

Om brytaren wrapar hela återförsöksoperationen, ser brytaren ett slutgiltigt framgång eller misslyckande per användarbeggäran. Det är lugnare, men det kan dölja antalet misslyckade försök.

För många applikationstjänster är denna form rimlig:

användarbeggäran
-> strömbrytare
   -> återförsökspolicy
      -> ett HTTP-försök med timeout

Det betyder att brytaren spår om beroendeoperationen ultimativt fungerade för anroparen.

För lägre-nivå-klienter kan denna form också göra mening:

användarbeggäran
-> återförsökspolicy
   -> strömbrytare
      -> ett HTTP-försök med timeout

Det betyder att brytaren skyddar varje försök.

Den viktigare regeln är denna:

Försök inte blindt igen.

Använd:

  • en liten maximal återförsöksräkning
  • exponentiell backoff
  • jitter
  • timeouts per försök
  • en övergripande begäran-deadline
  • idempotens för skrivningar
  • metrisk för återförsöksförsök

Utan dessa kan återförsök förvandla en liten driftstopp till en större. För en djupare behandling av återförsökssäkerhet, se Idempotens i distribuerade system som faktiskt fungerar.

Strömbrytare och idempotens

Strömbrytare dyker ofta upp bredvid återförsök, och återförsök väcker frågan om idempotens.

För läsoperationer är återförsök oftast säkert.

För skrivoperationer kan återförsök vara farligt.

Överväg detta betalningsanrop:

POST /charge

Om begäran timeoutar, misslyckades betalningen? Kanske. Lyckades den men svaret förlorades? Även kanske.

Om du försöker igen utan en idempotensnyckel, kan du ladda två gånger.

För skrivoperationer, använd en eller flera av dessa:

  • idempotensnycklar
  • begäran-ID:n
  • operations-ID:n
  • unika begränsningar
  • transaktionsutbox
  • arbetsflödesorchestrering
  • explicit samordning

En strömbrytare kan stoppa dig från att fortsätta anropa en misslyckad betalningsleverantör, men den kan inte göra osäkra återförsök säkra.

Strömbrytare och fallbacks

När strömbrytaren är öppen behöver din tjänst en plan.

Möjliga fallback-strategier inkluderar:

  • returnera cachad data
  • returnera gammal data med en varning
  • utesluta ett icke-kritiskt avsnitt
  • köa arbete för senare
  • byta till en annan leverantör
  • returnera ett tillfälligt fel
  • visa nedgraderad funktionalitet
  • misslyckas begäran snabbt

En fallback bör vara ärlig.

Till exempel, detta är oftast bra:

{
  "status": "temporary_unavailable",
  "message": "Rekommendationer är tillfälligt otillgängliga"
}

Detta är riskabelt:

{
  "recommendations": []
}

En tom lista kan se ut som ett giltigt resultat. Det kan dölja en driftstopp, förvirra användare och göra felsökning svårare.

Tysta fallbacks är frestande. De är också farliga.

Strömbrytare och observabilitet

En strömbrytare utan observabilitet är mestadels en överraskningsgenerator.

Spår minst dessa metrisk:

  • aktuellt brytartilstånd
  • tillståndsförändringar
  • tillåtna anrop
  • avvisade anrop
  • framgångar
  • misslyckanden
  • timeouts
  • fallback-svar
  • återförsöksförsök
  • nedströms latens
  • nedströms statuskoder

Användbara etiketter inkluderar:

  • brytarnamn
  • beroendens namn
  • operationsnamn
  • statusklass
  • felkategori

Undvik höghögkardinalitetsetiketter som användar-ID, full URL, begäran-ID eller råa felmeddelanden.

Du bör kunna svara på dessa frågor från dashboards:

  • Vilka strömbrytare är öppna just nu?
  • Hur ofta öppnar de?
  • Vilket beroende orsakade öppningen?
  • Ser användare fallback-svar?
  • Förbättrades latensen efter att brytaren öppnades?
  • Ökade återförsöksvolymen innan brytaren öppnades?
  • Återhämtade sig beroendet?

Om du inte kan observera brytaren, kan du inte finjustera den. För strukturerad loggning som passar bra med metrisk, se Strukturerad loggning i Go med slog.

En mer produktionsvänlig HTTP-klientform

För verkliga tjänster, undvik att sprida strömbrytalogik över hanterare.

Skapa ett litet klientpaket runt beroendet.

Exempelstruktur:

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

Hanteraren bör inte känna till detaljerna i gobreaker. Den bör bero på ett domän-nivå klientmetod:

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

Då kan implementationen innehålla:

  • HTTP-begäran skapning
  • context-propagering
  • brytare-exekvering
  • statuskodhantering
  • svardekodning
  • metrisk
  • felmappning

Detta håller resilienspolicyn nära beroendegränsen. För mer om felklassificering vid gränser, se Go-felhanteringsarkitektur: Gränser och mönster.

Var strömbrytare passar i applikationsarkitektur

Strömbrytarmönstret hör hemma vid integrationsgränser.

I en Go-applikation betyder det oftast:

graph LR A[Hanterare] --> B[Applikationstjänst] B --> C[Beroendeklient] C --> D[Strömbrytare] D --> E[HTTP / RPC / DB / Kö]

Håll brytaren utanför affärslogik när möjligt.

Affärslaget bör förstå domänfel som:

betalningsleverantör otillgänglig
rekommendationer otillgängliga
profil tjänst timeout

Det behöver inte förstå gobreaker-tillstånd.

Denna separation håller arkitekturen ren:

  • transportbryggor stannar i klienter
  • resilienspolicyn stannar nära beroenden
  • domänlogik stannar läsbar
  • hanterare stannar tunna
  • tester är lättare att skriva

Den här artikeln är en del av App-arkitektur i produktion ämnet — tillsammans med idempotens, utbox, saga och orchestrering guider i Integrationsmönster.

Vanliga misstag

Misstag 1: Ingen timeout

En strömbrytare stoppar inte magiskt långsamma anrop om inte anropen returnerar.

Om det utåtriktade operationen kan hänga för evigt, kan brytaren inte se ett fel snabbt nog.

Använd alltid timeouts.

Misstag 2: En global brytare för allt

Använd inte en brytare för alla beroenden.

En misslyckad e-postleverantör bör inte öppna strömbrytaren för din betalningsleverantör. En långsam sökundepunkt bör inte blockera användarprofilanrop.

Använd separata brytare för separata beroendeoperationer när deras felmeddelanden skiljer sig.

Misstag 3: Att räkna anroparsfel som beroendefel

Om din tjänst skickar dålig inmatning och får 400 Bad Request, är det oftast inte en nedströms driftstopp.

Träna inte brytaren på dina egna buggar.

Misstag 4: Återförsök med icke-idempotenta skrivningar

Återförsök är inte gratis. De kan duplicera skrivningar, betalningar, meddelanden eller sidoeffekter.

Gör skrivningar idempotenta innan du försöker igen.

Misstag 5: Dölja driftstopp bakom fallbacks

Fallbacks bör degradera smidigt, inte förfalska verkligheten.

Om ett beroende är nere, bör dina metrisk och loggor göra det uppenbart.

Misstag 6: Finjustering utan produktionsdata

Trösklar kopierade från exempel är bara startpunkter.

Finjustera baserat på:

  • begäranvolym
  • normal felhastighet
  • beroendelatens
  • användarimpact
  • återhämtningstid
  • fallback-kvalitet

Misstag 7: Använda strömbrytare istället för kapacitetsstyrning

En strömbrytare är inte en ersättning för:

  • lastavlastning
  • hastighetsbegränsning
  • köbegränsningar
  • autoskalning
  • databasjustering
  • anslutningspoolbegränsningar
  • upstream-kvoter

Det är en del av en resiliensstrategi.

Praktiska standardinställningar

För en typisk Go-tjänst som anropar ett internt HTTP-beroende, kan en rimlig startpunkt vara:

HTTP-klient timeout: 2 till 5 sekunder
timeout per begäran context: baserat på anroparens SLA
brytare felregel: 5 konsekutiva fel eller 50 procent fel efter 20 begäran
öppen timeout: 10 till 30 sekunder
halvöppna begäran: 1 till 5
återförsöksräkning: 1 till 3 försök
återförsök backoff: exponentiell med jitter

Detta är inte universella värden. De är ganska säkra startpunkter.

För användarvänliga API:er, håll totala latensbudgetar tighta. För bakgrundsjobb, kan du tolerera längre väntetider. För betalningsleverantörer, var mycket mer försiktig med återförsök och idempotens.

Strömbrytare checklist

Innan du lägger till en strömbrytare, svara på dessa frågor:

  • Vilket beroende skyddas?
  • Vilken operation skyddas?
  • Vilka fel räknas som beroendefel?
  • Vilka fel bör ignoreras av brytaren?
  • Vilken timeout gäller för varje anrop?
  • Är återförsök tillåtna?
  • Är skrivningar idempotenta?
  • Vad händer när strömbrytaren är öppen?
  • Finns det en fallback?
  • Syns fallbacken i metrisk?
  • Vem aviseras om strömbrytaren fortsätter att öppnas?
  • Hur kommer brytaren att finjusteras efter deployment?

Om du inte kan svara på dessa, kan det att lägga till en brytare skapa mer förvirring än resiliens.

Testa strömbrytare i Go

Testa beteende, inte bibliotekets interna tillståndsmaskin.

Användbara tester inkluderar:

  • beroende lyckas och svar returneras
  • beroende misslyckas upprepade gånger och strömbrytare öppnas
  • öppen strömbrytare returnerar ett tillfälligt fel
  • klientsida-valideringsfel inte utlöser brytaren
  • context timeout respekteras
  • fallback-svar returneras när förväntat
  • metrisk emitteras vid tillståndsförändringar

Använd falska HTTP-servrar för integrations-stil tester:

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

För enhetstester, göm beroendet bakom ett gränssnitt och injicera en falsk implementation.

Håll tester deterministiska. Undvik att sova för långa verkliga durationer. Konfigurera korta brytare-timeouts i tester. För mer om testning av koncurrent Go-kod med falsk tid och isolerade bubblor, se Testa koncurrent Go-kod med testing/synctest.

Borde du bygga din egen strömbrytare?

Att bygga en liten strömbrytare är en bra övning. Det hjälper dig att förstå tillståndsmaskinen.

För produktionskod, föredra ett underhålls bibliotek om inte dina behov är mycket specifika.

En produktionsbrytare behöver hantera:

  • konkurrens
  • tillståndstransitioner
  • räknare
  • halvöppna prober
  • callback-funktioner
  • anpassad felklassificering
  • race-free beteende
  • förutsägbart felhantering

Det är inte omöjligt, men det är lätt att få subtilt fel.

Det tråkiga biblioteket är oftast det bättre valet.

Slutsats

Strömbrytarmönstret är inte magiskt pålitlighetspulver.

I Go fungerar det bäst när det är en del av en liten, explicit resiliensstack:

context timeout
+ återförsök med backoff och jitter
+ strömbrytare
+ fallback
+ metrisk

Mönstret är mest användbart vid beroendegränser, särskilt runt fjärrtjänster som kan bli långsamma eller delvis otillgängliga.

Använd det för att stoppa kaskadfall. Använd det för att misslyckas snabbt när ett beroende är tydligt ohälsosamt. Använd det för att ge överbelastade system utrymme att återhämta sig.

Men använd det inte som ursäkt för att ignorera timeouts, idempotens, observabilitet eller ren arkitektur.

En bra strömbrytare gör misslyckande tydligare och billigare. En dålig en gör bara misslyckande mer mysteriös.

Referenser

Prenumerera

Få nya inlägg om system, infrastruktur och AI-ingenjörskonst.