Pattern Circuit Breaker in Go: Fermare i guasti a cascata
Prevenire i guasti a cascata nei microservizi Go.
Un circuit breaker impedisce al tuo servizio Go di bombardare una dipendenza in errore, prevenendo guasti a cascata che consumano goroutine, socket e memoria fino al collasso dell’intero sistema.
La parte difficile non è la macchina a stati. È decidere dove posizionare il breaker, cosa costituisce un errore, come interagisce con i timeout e i retry, e cosa dovrebbe fare il tuo servizio quando il circuito è aperto.

In Go, il pattern del circuit breaker è particolarmente utile per le chiamate in uscita: API HTTP, gateway di pagamento, servizi di ricerca, provider di email, gateway LLM, microservizi interni e altre dipendenze che possono diventare lente, sovraccaricate o parzialmente non disponibili. Se usato bene, un circuit breaker riduce i guasti a cascata. Se usato male, diventa un altro modo oscuro per fallire.
Qual è il problema che risolve un Circuit Breaker?
I sistemi distribuiti raramente falliscono in modo pulito.
Una dipendenza potrebbe non essere completamente giù. Potrebbe essere:
- restituendo errori 500
- restituendo risposte 429 per limiti di rateo
- accettando connessioni TCP ma senza mai rispondere
- rispondendo in 30 secondi invece di 300 millisecondi
- fallendo solo per alcune richieste
- sovraccaricata perché ogni cliente sta riprovando contemporaneamente
Il peggior caso spesso non è un errore netto. È una dipendenza lenta.
Le chiamate lente consumano goroutine, socket, connessioni al database, memoria e capacità dei worker. Se il tuo servizio continua ad aspettare una dipendenza che è già in stato di non salute, anche il tuo servizio può diventare non sano.
Un circuit breaker previene questo fallendo rapidamente dopo che la dipendenza supera una soglia di errore.
Invece di fare questo all’infinito:
richiesta -> chiamata dipendenza -> attesa -> timeout -> retry -> attesa -> errore
il servizio alla fine fa questo:
richiesta -> circuito aperto -> restituisce fallback o errore immediatamente
Questo errore rapido non è sempre piacevole, ma è prevedibile. Un errore prevedibile è più facile da gestire rispetto a un collasso lento.
I tre stati del Circuit Breaker
La maggior parte dei circuit breaker utilizza tre stati.
Chiuso
Il circuito è chiuso durante il normale funzionamento.
Le richieste sono consentite. Il breaker registra successi e errori. Se il numero o la proporzione di errori supera una soglia, il breaker si apre.
Chiuso non significa “sicuro per sempre”. Significa “il traffico è attualmente consentito”.
Aperto
Il circuito è aperto quando la dipendenza è considerata non sana.
Le richieste vengono rifiutate immediatamente. Il servizio dovrebbe restituire un fallback, una risposta in cache, una risposta degradata o un errore upstream chiaro.
Aperto non risolve la dipendenza. Dà alla dipendenza il tempo di recuperare e protegge il chiamante dal spreco di risorse.
Semi-aperto (Half-Open)
Dopo un periodo di raffreddamento, il breaker entra in uno stato semi-aperto.
Solo un numero limitato di richieste di prova è consentito. Se hanno successo, il breaker si chiude. Se falliscono, il breaker si apre di nuovo.
Lo stato semi-aperto è importante perché evita due estremi negativi:
- non provare mai più la dipendenza
- inviare tutto il traffico indietro troppo rapidamente
Le transizioni di stato sono così:
Circuit Breaker vs Timeout vs Retry
Un errore comune è considerare circuit breaker, retry e timeout come intercambiabili. Sono correlati, ma risolvono problemi diversi.
Timeout
Un timeout limita quanto tempo può durare un’operazione.
In Go, questo di solito significa passare un context.Context con una scadenza o un timeout nella chiamata in uscita.
Un timeout risponde a questa domanda:
Quanto sono disposto ad aspettare per questa singola chiamata?
Retry
Un retry ripete un’operazione quando l’errore potrebbe essere temporaneo.
I retry sono utili per piccoli glitch di rete, risposte temporanee 503, reset di connessione e altri errori transitori.
Un retry risponde a questa domanda:
Dovrei provare di nuovo questa chiamata?
Circuit Breaker
Un circuit breaker ferma le chiamate quando la dipendenza è probabilmente non sana.
Risponde a questa domanda:
Dovrei chiamare questa dipendenza adesso?
Rate Limiter
Un rate limiter controlla quanto traffico è consentito nel tempo.
Risponde a questa domanda:
Quanto traffico dovrebbe inviare questo chiamante?
Bulkhead (Paratia)
Una paratia isola le risorse in modo che una dipendenza non possa consumare tutto.
Risponde a questa domanda:
Quanto del mio servizio può danneggiare questa dipendenza?
Questi pattern sono più forti quando usati insieme. Un circuit breaker senza timeout è debole. I retry senza jitter possono creare tempeste di retry. Un fallback senza metriche può nascondere un’interruzione.
Quando usare un Circuit Breaker in Go
Usa un circuit breaker quando il tuo servizio chiama una dipendenza che può fallire indipendentemente dal tuo servizio.
Buoni candidati includono:
- API HTTP esterne
- processori di pagamento
- provider di email e SMS
- servizi di ricerca
- servizi di raccomandazione
- gateway di inferenza LLM
- endpoint di microservizi interni
- API SaaS di terze parti
- servizi di lettura lenti o sovraccarichi
I circuit breaker sono particolarmente utili quando il chiamante può degradarsi in modo elegante.
Ad esempio:
- restituire dati prodotti in cache
- saltare un blocco di raccomandazioni
- segnare un provider di pagamento come temporaneamente non disponibile
- accodare il lavoro per più tardi
- restituire una risposta parziale
- fallire rapidamente con un errore temporaneo chiaro
La domanda importante non è “questa chiamata può fallire?”. Tutto può fallire. La domanda migliore è:
Se questa dipendenza sta fallendo, dovremmo continuare a inviare tutto il traffico ad essa?
Se la risposta è no, un circuit breaker può aiutare.
Quando non usare un Circuit Breaker
Non aggiungere un circuit breaker a ogni funzione solo perché il pattern sembra responsabile.
Un circuit breaker di solito non è utile per:
- chiamate di funzione locali in-process
- semplici CRUD all’interno di un monolite
- logica di validazione
- regole di business deterministiche
- operazioni locali solo CPU
- percorsi di codice dove non esiste un fallback utile
- operazioni di scrittura che non sono idempotenti
- dipendenze già protette da un livello di workflow più forte
Un circuit breaker non sostituisce nemmeno l’igiene di base:
- impostare timeout
- propagare il contesto
- usare correttamente i pool di connessioni
- gestire gli errori esplicitamente
- rendere sicuri i retry
- osservare i tassi di errore
Un circuit breaker cattivo può rendere un sistema più difficile da comprendere. Può nascondere il problema reale, rifiutare il traffico in modo troppo aggressivo o creare comportamenti confusi durante il recupero.
La regola leggermente opiniata è semplice:
Aggiungi circuit breaker ai confini delle dipendenze, non ovunque.
Scegliere una libreria per Circuit Breaker in Go
Puoi implementare un circuit breaker base da solo, ma la maggior parte dei servizi Go in produzione dovrebbero usare una libreria.
La scelta semplice più comune è sony/gobreaker.
Ti offre:
- stati chiuso, aperto e semi-aperto
- soglie di errore configurabili
- timeout dello stato aperto configurabile
- callback per i cambi di stato
- contatori di richieste
- supporto generico nella v2
- una superficie API piccola
Per pipeline di resilienza più grandi, puoi anche guardare librerie che compongono più policy, come retry, timeout, fallback, limitazione di rateo, isolamento bulkhead e circuit breaking. Questo può essere utile quando vuoi un singolo livello di resilienza intorno a un’operazione.
Per molti servizi Go, però, gobreaker è sufficiente.
Confronto tra pacchetti Go per Circuit Breaker
Go non include un circuit breaker integrato nella libreria standard. Nella pratica, di solito scegli tra una piccola libreria per circuit breaker, un framework di resilienza più grande o un pacchetto stile Hystrix più vecchio.
Per la maggior parte dei nuovi servizi Go, la decisione è semplice:
- usa
sony/gobreakerse vuoi un circuit breaker piccolo e focalizzato - usa
failsafe-gose vuoi circuit breaker composti con retry, timeout, fallback, bulkhead, limiti di rateo e altre policy di resilienza - evita di iniziare nuovi progetti con
hystrix-goa meno che tu non abbia già codice legacy che lo usa
| Pacchetto | Ideale per | Punti di forza | Compromessi |
|---|---|---|---|
sony/gobreaker/v2 |
Circuit breaker semplici intorno a client HTTP/RPC | API piccola, supporto generico v2, modello di stato chiaro, facile da avvolgere attorno ai client delle dipendenze | Risolve solo il circuit breaking; retry, timeout e fallback devono essere composti separatamente |
failsafe-go |
Composizione completa di policy di resilienza | Retry, fallback, circuit breaker, timeout, bulkhead, rate limiter, cache, hedge, limiter adattivo e throttler adattivo | Più concetti da imparare; più pesante del necessario se vuoi solo un breaker base |
afex/hystrix-go |
Sistemi legacy stile Hystrix | Concetti Hystrix familiari, esecuzione stile comando, utilizzo storico | Design più vecchio; non la scelta predefinita migliore per nuovi servizi Go |
go-kit/kit/circuitbreaker |
Servizi basati su endpoint Go kit | Si adatta allo stile middleware di Go kit e all’architettura degli endpoint | Utile soprattutto se il tuo servizio usa già Go kit |
cep21/circuit |
Comportamento del circuit breaker simile a Hystrix | Approccio stile Hystrix più ricco di funzionalità | Meno comune come default semplice; potrebbe essere più del necessario per servizi piccoli |
Il mio consiglio predefinito è noioso di proposito: inizia con sony/gobreaker/v2 quando hai bisogno solo di un circuit breaker. Usa failsafe-go quando vuoi esprimere una policy di resilienza completa in un unico posto.
Questa divisione mantiene l’architettura pulita. Un piccolo client di servizio non ha bisogno di un framework di resilienza completo solo per smettere di chiamare una dipendenza in errore. Ma un gateway, un aggregatore, un SDK client API o un livello di integrazione ad alto traffico potrebbero beneficiare di policy composte.
Installare gobreaker
Usa il pacchetto v2 per il nuovo codice:
go get github.com/sony/gobreaker/v2
Poi importalo:
import "github.com/sony/gobreaker/v2"
Un Circuit Breaker base in Go
Ecco un piccolo esempio intorno a una chiamata HTTP.
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/sony/gobreaker/v2"
)
var ErrTemporaryUnavailable = errors.New("dipendenza temporaneamente non disponibile")
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 cambiato da %s a %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("il servizio utenti ha restituito %d", resp.StatusCode)
}
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("utente non trovato")
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("errore client del servizio utenti: %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
}
Questo non è un client di produzione completo, ma mostra la struttura:
- il breaker avvolge la chiamata in uscita
- la richiesta HTTP riceve un contesto
- il client HTTP ha un timeout
- gli errori lato server contano come errori del breaker
- gli errori del circuito aperto sono mappati in un errore dell’applicazione
Configurare le impostazioni di gobreaker
Le impostazioni chiave valgono la pena di essere comprese.
Name
Name identifica il breaker.
Usa un nome stabile e specifico:
payment-api
search-service
llm-gateway
user-service
Evita nomi vaghi come:
http-client
external-call
default
Vorrai questo nome nei log e nelle metriche.
MaxRequests
MaxRequests controlla quante richieste sono consentite mentre il breaker è semi-aperto.
Un numero piccolo è di solito più sicuro. Lo scopo dello stato semi-aperto è testare il recupero, non inviare tutto il traffico immediatamente.
Interval
Interval controlla quando i conteggi interni vengono azzerati mentre il breaker è chiuso.
Se è zero, i conteggi non vengono azzerati automaticamente. Un intervallo non zero dà al breaker una finestra di memoria simile a quella scorrevole, anche se non è la stessa cosa di un’implementazione completa di finestra scorrevole.
Timeout
Timeout controlla quanto tempo il breaker rimane aperto prima di passare allo stato semi-aperto.
Se il timeout è troppo corto, il tuo servizio continuerà a sondare una dipendenza che non si è ripresa. Se è troppo lungo, il recupero sarà ritardato.
Inizia con qualcosa di conservativo, come 10 a 30 secondi, e poi ottimizza dalle metriche di produzione.
ReadyToTrip
ReadyToTrip decide quando il breaker dovrebbe aprirsi.
Una regola semplice sono gli errori consecutivi:
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures >= 5
}
È facile da ragionare, ma potrebbe non essere adatta per servizi ad alto volume.
Un’altra opzione è la proporzione di errori dopo un numero minimo di richieste:
ReadyToTrip: func(counts gobreaker.Counts) bool {
total := counts.Requests
failures := counts.TotalFailures
if total < 20 {
return false
}
return float64(failures)/float64(total) >= 0.5
}
Questo evita di aprire il circuito dopo un campione di dimensioni minuscole.
OnStateChange
OnStateChange è dove dovresti emettere log o metriche.
Al minimo, registra:
- nome del breaker
- vecchio stato
- nuovo stato
- timestamp
Per i sistemi di produzione, esponi lo stato del breaker come metrica. I log sono utili per il debugging, ma le metriche sono migliori per alerting e dashboard.
IsSuccessful
IsSuccessful ti permette di decidere quali errori contano come errori.
Questo è importante.
Non ogni errore dovrebbe aprire il breaker. Ad esempio, un 404 Not Found da un servizio utenti potrebbe essere un risultato valido di business. Un 400 Bad Request potrebbe essere colpa del chiamante, non della dipendenza.
Un 503 Service Unavailable, un timeout, un reset di connessione o un 429 Too Many Requests possono essere segnali reali dello stato di salute della dipendenza.
Attento qui. Contare gli errori sbagliati è uno dei modi più facili per costruire un circuit breaker rumoroso.
Cosa dovrebbe contare come errore?
Qui è dove conta il giudizio ingegneristico.
Di solito conta questi come errori:
- timeout di rete
- connessione rifiutata
- connessione resettata
- HTTP 500
- HTTP 502
- HTTP 503
- HTTP 504
- risposte 429 ripetute
- risposte malformate dalla dipendenza
- deadline del contesto superata durante la chiamata in uscita
Di solito non contare questi come errori della dipendenza:
- errori di validazione
- errori di serializzazione locali
- risposte 404 attese
- fallimenti di autorizzazione lato chiamante
- rifiuti di regole di business
- errori di input dell’utente
Il breaker dovrebbe rappresentare lo stato di salute della dipendenza, non il fallimento generale dell’applicazione.
Circuit Breaker e context.Context
In Go, i circuit breaker non dovrebbero sostituire context.Context.
Un circuit breaker decide se tentare una chiamata. Un contesto controlla quanto tempo quella chiamata può durare e se dovrebbe fermarsi quando il chiamante se ne va.
Una buona chiamata in uscita dovrebbe di solito avere entrambi:
ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
defer cancel()
data, err := client.GetUser(ctx, userID)
Il contesto dovrebbe fluire attraverso la catena delle chiamate:
contesto della richiesta in entrata
-> metodo del servizio
-> metodo del client
-> richiesta HTTP
-> dipendenza
Evita di creare contesti di background distaccati all’interno di codice limitato alla richiesta. Se la richiesta dell’utente è annullata, il lavoro downstream dovrebbe di solito fermarsi anche lui.
La regola calma è:
Il breaker protegge il sistema. Il contesto protegge la richiesta.
Di solito hai bisogno di entrambi.
Circuit Breaker e Retry
I retry e i circuit breaker possono funzionare bene insieme, ma l’ordine conta.
Il default più sicuro è:
timeout per tentativo
retry con backoff e jitter
circuit breaker intorno alla chiamata della dipendenza
Ma non c’è una risposta universale. Pensa a cosa vuoi contare.
Se ogni tentativo di retry passa attraverso il breaker, una richiesta utente può contribuire a più errori. Questo può aprire il breaker più velocemente, il che può essere buono o cattivo.
Se il breaker avvolge l’intera operazione di retry, il breaker vede un singolo successo o errore finale per richiesta utente. Questo è più calmo, ma può nascondere il numero di tentativi falliti.
Per molti servizi applicativi, questa forma è ragionevole:
richiesta utente
-> circuit breaker
-> policy di retry
-> un tentativo HTTP con timeout
Questo significa che il breaker traccia se l’operazione della dipendenza ha funzionato in definitiva per il chiamante.
Per client di livello inferiore, questa forma può anche avere senso:
richiesta utente
-> policy di retry
-> circuit breaker
-> un tentativo HTTP con timeout
Questo significa che il breaker protegge ogni tentativo.
La regola più importante è questa:
Non fare retry ciecamente.
Usa:
- un conteggio massimo di retry piccolo
- backoff esponenziale
- jitter
- timeout per tentativo
- una scadenza totale della richiesta
- idempotenza per le scritture
- metriche per i tentativi di retry
Senza questi, i retry possono trasformare un piccolo guasto in uno più grande. Per un trattamento più approfondito della sicurezza dei retry, vedi Idempotenza nei Sistemi Distribuiti che Funziona Davvero.
Circuit Breaker e Idempotenza
I circuit breaker spesso appaiono accanto ai retry, e i retry sollevano la questione dell’idempotenza.
Per le operazioni di lettura, i retry sono di solito sicuri.
Per le operazioni di scrittura, i retry possono essere pericolosi.
Considera questa chiamata di pagamento:
POST /charge
Se la richiesta va in timeout, il pagamento ha fallito? Forse. Ha avuto successo ma la risposta è stata persa? Anche questo forse.
Se fai retry senza una chiave di idempotenza, potresti addebitare due volte.
Per le operazioni di scrittura, usa uno o più di questi:
- chiavi di idempotenza
- ID richiesta
- ID operazione
- vincoli univoci
- outbox transazionale
- orchestrazione del workflow
- riconciliazione esplicita
Un circuit breaker può impedirti di continuare a chiamare un provider di pagamento in errore, ma non può rendere sicuri i retry non sicuri.
Circuit Breaker e Fallback
Quando il circuito è aperto, il tuo servizio ha bisogno di un piano.
Le possibili strategie di fallback includono:
- restituire dati in cache
- restituire dati obsoleti con un avviso
- omettere una sezione non critica
- accodare il lavoro per più tardi
- passare a un altro provider
- restituire un errore temporaneo
- mostrare funzionalità degradate
- fallire la richiesta rapidamente
Un fallback dovrebbe essere onesto.
Ad esempio, questo è di solito buono:
{
"status": "temporary_unavailable",
"message": "Le raccomandazioni sono temporaneamente non disponibili"
}
Questo è rischioso:
{
"recommendations": []
}
Un elenco vuoto può sembrare un risultato valido. Può nascondere un’interruzione, confondere gli utenti e rendere il debugging più difficile.
I fallback silenziosi sono allettanti. Sono anche pericolosi.
Circuit Breaker e Osservabilità
Un circuit breaker senza osservabilità è per lo più un generatore di sorprese.
Traccia almeno queste metriche:
- stato attuale del breaker
- cambi di stato
- chiamate consentite
- chiamate rifiutate
- successi
- errori
- timeout
- risposte di fallback
- tentativi di retry
- latenza downstream
- codici di stato downstream
Etichette utili includono:
- nome del breaker
- nome della dipendenza
- nome dell’operazione
- classe di stato
- categoria di errore
Evita etichette ad alta cardinalità come ID utente, URL completo, ID richiesta o messaggi di errore grezzi.
Dovresti essere in grado di rispondere a queste domande dalle dashboard:
- Quali circuit breaker sono aperti adesso?
- Quanto spesso si aprono?
- Quale dipendenza ha causato l’apertura?
- Gli utenti stanno vedendo risposte di fallback?
- La latenza è migliorata dopo che il breaker si è aperto?
- Il volume di retry è aumentato prima che il breaker si aprisse?
- La dipendenza si è ripresa?
Se non puoi osservare il breaker, non puoi ottimizzarlo. Per un logging strutturato che si abbina bene con le metriche, vedi Logging Strutturato in Go con slog.
Una forma HTTP Client più amichevole per la produzione
Per i servizi reali, evita di spargere la logica del circuit breaker tra gli handler.
Crea un piccolo pacchetto client intorno alla dipendenza.
Struttura di esempio:
internal/
userservice/
client.go
errors.go
metrics.go
Lo handler non dovrebbe conoscere i dettagli di gobreaker. Dovrebbe dipendere da un metodo client a livello di dominio:
type UserService interface {
GetUser(ctx context.Context, userID string) (*User, error)
}
Poi l’implementazione può contenere:
- creazione della richiesta HTTP
- propagazione del contesto
- esecuzione del breaker
- gestione dei codici di stato
- decodifica della risposta
- metriche
- mappatura degli errori
Questo mantiene la policy di resilienza vicina al confine della dipendenza. Per altro sulla classificazione degli errori ai confini, vedi Architettura di Gestione degli Errori in Go: Confini e Pattern.
Dove i Circuit Breaker si inseriscono nell’Architettura Applicativa
Il pattern del circuit breaker appartiene ai confini di integrazione.
In un’applicazione Go, questo di solito significa:
Mantieni il breaker fuori dalla logica di business quando possibile.
Il livello di business dovrebbe comprendere errori di dominio come:
provider di pagamento non disponibile
raccomandazioni non disponibili
timeout del servizio profilo
Non dovrebbe aver bisogno di comprendere gli stati di gobreaker.
Questa separazione mantiene l’architettura pulita:
- le preoccupazioni di trasporto restano nei client
- la policy di resilienza resta vicino alle dipendenze
- la logica di dominio resta leggibile
- gli handler restano sottili
- i test sono più facili da scrivere
Errori Comuni
Errore 1: Nessun Timeout
Un circuit breaker non ferma magicamente le chiamate lente a meno che le chiamate non restituiscano.
Se l’operazione in uscita può rimanere in attesa per sempre, il breaker potrebbe non vedere un errore abbastanza rapidamente.
Usa sempre i timeout.
Errore 2: Un Breaker Globale per Tutto
Non usare un breaker per tutte le dipendenze.
Un provider di email in errore non dovrebbe aprire il circuito per il tuo provider di pagamento. Un endpoint di ricerca lento non dovrebbe bloccare le chiamate al profilo utente.
Usa breaker separati per operazioni di dipendenza separate quando i loro modi di fallire differiscono.
Errore 3: Contare gli Errori del Chiamante come Errori della Dipendenza
Se il tuo servizio invia input errati e riceve 400 Bad Request, di solito non è un’interruzione downstream.
Non addestrare il breaker sui tuoi bug.
Errore 4: Fare Retry su Scritture Non Idempotenti
I retry non sono gratis. Possono duplicare scritture, pagamenti, messaggi o effetti collaterali.
Rendi le scritture idempotenti prima di fare retry.
Errore 5: Nascondere le Interruzioni dietro i Fallback
I fallback dovrebbero degradarsi in modo elegante, non falsare la realtà.
Se una dipendenza è giù, le tue metriche e i tuoi log dovrebbero rendere questo ovvio.
Errore 6: Ottimizzare senza Dati di Produzione
Le soglie copiate dagli esempi sono solo punti di partenza.
Ottimizza in base a:
- volume delle richieste
- tasso di errore normale
- latenza della dipendenza
- impatto sull’utente
- tempo di recupero
- qualità del fallback
Errore 7: Usare i Circuit Breaker al posto della Gestione della Capacità
Un circuit breaker non è un sostituto per:
- load shedding
- rate limiting
- limiti delle code
- autoscaling
- tuning del database
- limiti del pool di connessioni
- quote upstream
È una parte di una strategia di resilienza.
Default Pratici
Per un tipico servizio Go che chiama una dipendenza HTTP interna, un punto di partenza ragionevole potrebbe essere:
Timeout client HTTP: 2 a 5 secondi
Timeout contesto per richiesta: basato sull'SLA del chiamante
Regola di errore del breaker: 5 errori consecutivi o 50 percento di errori dopo 20 richieste
Timeout aperto: 10 a 30 secondi
Richieste semi-aperte: 1 a 5
Conteggio retry: 1 a 3 tentativi
Backoff retry: esponenziale con jitter
Questi non sono valori universali. Sono punti di partenza relativamente sicuri.
Per le API front-end, mantieni i budget di latenza totali stretti. Per i lavori in background, puoi tollerare attese più lunghe. Per i provider di pagamento, sii molto più attento con i retry e l’idempotenza.
Checklist del Circuit Breaker
Prima di aggiungere un circuit breaker, rispondi a queste domande:
- Quale dipendenza sta proteggendo?
- Quale operazione sta proteggendo?
- Quali errori contano come errore della dipendenza?
- Quali errori dovrebbero essere ignorati dal breaker?
- Quale timeout si applica a ogni chiamata?
- I retry sono consentiti?
- Le scritture sono idempotenti?
- Cosa succede quando il circuito è aperto?
- C’è un fallback?
- Il fallback è visibile nelle metriche?
- Chi riceve l’alert se il circuito continua ad aprirsi?
- Come sarà ottimizzato il breaker dopo il deployment?
Se non puoi rispondere a queste, aggiungere un breaker potrebbe creare più confusione che resilienza.
Testare i Circuit Breaker in Go
Testa il comportamento, non la macchina a stati interna della libreria.
I test utili includono:
- la dipendenza ha successo e la risposta è restituita
- la dipendenza fallisce ripetutamente e il circuito si apre
- il circuito aperto restituisce un errore temporaneo
- gli errori di validazione lato client non attivano il breaker
- il timeout del contesto è rispettato
- la risposta di fallback è restituita quando previsto
- le metriche sono emesse sui cambi di stato
Usa server HTTP fake per test di stile integrazione:
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "unavailable", http.StatusServiceUnavailable)
}))
defer server.Close()
Per i test unitari, nascondi la dipendenza dietro un’interfaccia e inietta un’implementazione fake.
Mantieni i test deterministici. Evita di dormire per durate reali lunghe. Configura timeout brevi del breaker nei test. Per altro sul test del codice Go concorrente con tempo fake e bolle isolate, vedi Testare Codice Go Concorrente con testing/synctest.
Dovresti Costruire il Tuo Circuit Breaker?
Costruire un piccolo circuit breaker è un buon esercizio di apprendimento. Ti aiuta a capire la macchina a stati.
Per il codice di produzione, preferisci una libreria mantenuta a meno che le tue esigenze non siano molto specifiche.
Un breaker di produzione deve gestire:
- concorrenza
- transizioni di stato
- contatori
- probe semi-aperti
- callback
- classificazione degli errori personalizzata
- comportamento race-free
- gestione degli errori prevedibile
Non è impossibile, ma è facile sbagliare in modo sottile.
La libreria noiosa è di solito la scelta migliore.
Conclusione
Il pattern del circuit breaker non è polvere magica di affidabilità.
In Go, funziona meglio quando fa parte di uno stack di resilienza piccolo ed esplicito:
timeout contesto
+ retry con backoff e jitter
+ circuit breaker
+ fallback
+ metriche
Il pattern è più utile ai confini delle dipendenze, specialmente intorno a servizi remoti che possono diventare lenti o parzialmente non disponibili.
Usalo per fermare i guasti a cascata. Usalo per fallire rapidamente quando una dipendenza è chiaramente non sana. Usalo per dare ai sistemi sovraccarichi spazio per recuperare.
Ma non usarlo come scusa per ignorare timeout, idempotenza, osservabilità o architettura pulita.
Un buon circuit breaker rende il fallimento più chiaro e meno costoso. Uno cattivo rende il fallimento solo più misterioso.
Riferimenti
- Microservizi Go per Orchestrazione AI/ML — contesto di orchestrazione più ampio dove i circuit breaker si inseriscono
- Pattern Saga nelle Transazioni Distribuite — pattern di transazioni distribuite che si abbinano ai circuit breaker
- Idempotenza nei Sistemi Distribuiti — sicurezza dei retry e operazioni idempotenti
- Pattern Outbox Transazionale in Go — consegna affidabile degli eventi accanto ai pattern di resilienza
- Architettura di Gestione degli Errori in Go — classificazione degli errori ai confini delle dipendenze
- Testare Codice Go Concorrente con synctest — test del comportamento asincrono con circuit breaker
- Logging Strutturato in Go con slog — osservabilità accanto ai circuit breaker
github.com/sony/gobreaker/v2— pacchetto ufficiale gobreaker v2- Cancellazione e Timeout del Contesto in Go — pattern del contesto che si abbinano ai circuit breaker