Pattern Circuit Breaker in Go: interrompere le cascate di guasti
Arresta le anomalie a catena nei microservizi Go.
Un circuit breaker impedisce al tuo servizio Go di martellare una dipendenza in avaria, prevenendo failure 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 conta come fallimento, come interagisce con i timeout e i retry, e cosa dovrebbe fare il 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, sovraccariche o parzialmente non disponibili. Usato bene, un circuit breaker riduce i failure a cascata. Usato male, diventa un’altra modalità di fallimento oscura.
Quale problema risolve un Circuit Breaker?
I sistemi distribuiti raramente falliscono in modo pulito.
Una dipendenza potrebbe non essere completamente offline. Potrebbe:
- restituire errori 500
- restituire risposte 429 di rate limit
- accettare connessioni TCP ma non rispondere mai
- rispondere in 30 secondi invece che in 300 millisecondi
- fallire solo per alcune richieste
- essere sovraccarica perché ogni client sta facendo retry contemporaneamente
Il caso peggiore spesso non è un fallimento 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 da una dipendenza che è già malsana, anche il tuo servizio può diventare malsano.
Un circuit breaker previene questo fenomeno fallendo rapidamente dopo che la dipendenza supera una soglia di fallimento.
Invece di fare questo per sempre:
richiesta -> chiama dipendenza -> aspetta -> timeout -> retry -> aspetta -> fallimento
il servizio finisce per fare questo:
richiesta -> circuito aperto -> restituisci fallback o errore immediatamente
Questo fallimento rapido non è sempre piacevole, ma è prevedibile. Il fallimento 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.
Closed (Chiuso)
Il circuito è chiuso durante il normale funzionamento.
Le richieste vengono consentite. Il breaker registra i successi e i fallimenti. Se il numero o il rapporto dei fallimenti supera una soglia, il breaker apre il circuito.
Closed non significa “sicuro per sempre”. Significa “il traffico è attualmente consentito”.
Open (Aperto)
Il circuito è aperto quando la dipendenza è considerata malsana.
Le richieste vengono rifiutate immediatamente. Il servizio dovrebbe restituire un fallback, una risposta memorizzata nella cache, una risposta degradata o un chiaro errore upstream.
Open non risolve il problema della dipendenza. Dà tempo alla dipendenza di riprendersi e protegge il chiamante dallo spreco di risorse.
Half-Open (Parzialmente aperto)
Dopo un periodo di raffreddamento, il breaker entra in uno stato half-open.
Solo un numero limitato di richieste di prova viene consentito. Se hanno successo, il breaker si chiude. Se falliscono, il breaker si riapre.
Half-open è importante perché evita due estremi negativi:
- non provare mai più la dipendenza
- inviare tutto il traffico di nuovo troppo rapidamente
Le transizioni di stato appaiono così:
Circuit Breaker vs Timeout vs Retry
Un errore comune è trattare i circuit breaker, i retry e i timeout come intercambiabili. Sono correlati, ma risolvono problemi diversi.
Timeout
Un timeout limita per quanto tempo un’operazione può essere eseguita.
In Go, questo di solito significa passare un context.Context con una deadline o un timeout nella chiamata in uscita.
Un timeout risponde a questa domanda:
Per quanto tempo sono disposto ad aspettare per questa singola chiamata?
Retry
Un retry ripete un’operazione quando il fallimento potrebbe essere temporaneo.
I retry sono utili per brevi interruzioni di rete, risposte 503 temporanee, reset della connessione e altri fallimenti transitori.
Un retry risponde a questa domanda:
Dovrei provare di nuovo questa chiamata?
Circuit Breaker
Un circuit breaker interrompe le chiamate quando la dipendenza è probabilmente malsana.
Risponde a questa domanda:
Dovrei chiamare questa dipendenza in questo momento?
Rate Limiter
Un rate limiter controlla quanto traffico è consentito nel tempo.
Risponde a questa domanda:
Quanto traffico dovrebbe inviare questo chiamante?
Bulkhead
Un bulkhead 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ù efficaci 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 di servizio.
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ò degradare in modo grace.
Ad esempio:
- restituire dati memorizzati nella cache del prodotto
- saltare un blocco di raccomandazioni
- segnare un provider di pagamento come temporaneamente non disponibile
- accodare il lavoro per dopo, con una dead-letter queue come protezione finale per i messaggi che non hanno successo nemmeno dopo il recupero della dipendenza
- restituire una risposta parziale
- fallire rapidamente con un chiaro errore temporaneo
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 verso di essa?
Se la risposta è no, un circuit breaker potrebbe 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 funzioni locali nello stesso processo
- CRUD semplice 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 uno strato di workflow più robusto
Un circuit breaker inoltre non sostituisce l’igiene di base:
- imposta i timeout
- propagare il context
- usare correttamente i connection pool
- gestire gli errori esplicitamente
- rendere i retry sicuri
- osservare i tassi di fallimento
Un cattivo circuit breaker può rendere un sistema più difficile da ragionare. Può nascondere il problema reale, rifiutare il traffico troppo aggressivamente o creare comportamenti confusi durante il recupero.
La regola leggermente opinosa è semplice:
Aggiungi circuit breaker ai confini delle dipendenze, non ovunque.
Scegliere una libreria Circuit Breaker per Go
Puoi implementare un circuit breaker di base da te, ma la maggior parte dei servizi Go in produzione dovrebbe usare una libreria.
La scelta semplice più comune è sony/gobreaker.
Ti offre:
- stati closed, open e half-open
- soglie di fallimento configurabili
- timeout per lo stato open configurabile
- callback di cambio di stato
- contatori di richieste
- supporto generico in v2
- una superficie API piccola
Per pipeline di resilienza più grandi, potresti anche guardare librerie che compongono più policy, come retry, timeout, fallback, rate limiting, isolamento bulkhead e circuit breaking. Questo può essere utile quando si vuole un singolo strato di resilienza intorno a un’operazione.
Per molti servizi Go, tuttavia, gobreaker è sufficiente.
Confronto dei pacchetti Circuit Breaker Go
Go non include un circuit breaker integrato nella libreria standard. In pratica, di solito si sceglie tra una piccola libreria circuit breaker, un framework di resilienza più grande o un pacchetto più vecchio in stile Hystrix.
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, rate limit e altre policy di resilienza - evita di iniziare nuovi progetti su
hystrix-goa meno che tu non abbia già codice legacy che lo usa
| Package | Migliore 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 i client delle dipendenze | Risolve solo il circuit breaking; retry, timeout e fallback devono essere composti separatamente |
failsafe-go |
Composizione completa delle policy di resilienza | Retry, fallback, circuit breaker, timeout, bulkhead, rate limiter, cache, hedge, adaptive limiter e adaptive throttler policies | Più concetti da imparare; più pesante del necessario se si vuole solo un breaker di base |
afex/hystrix-go |
Sistemi legacy in stile Hystrix | Concetti Hystrix familiari, esecuzione in stile command, utilizzo storico | Design più vecchio; non è la scelta migliore di default per nuovi servizi Go |
go-kit/kit/circuitbreaker |
Servizi basati su endpoint Go kit | Si adatta allo stile middleware e all’architettura endpoint di Go kit | Utile principalmente se il tuo servizio usa già Go kit |
cep21/circuit |
Comportamento circuit breaker simile a Hystrix | Approccio in stile Hystrix più completo | Meno comune come default semplice; potrebbe essere più del necessario per servizi piccoli |
La mia raccomandazione predefinita è volutamente noiosa: inizia con sony/gobreaker/v2 quando ti serve solo un circuit breaker. Passa a failsafe-go quando vuoi esprimere una policy di resilienza completa in un unico punto.
Questa separazione mantiene l’architettura pulita. Un piccolo client di servizio non ha bisogno di un intero framework di resilienza solo per smettere di chiamare una dipendenza in avaria. Ma un gateway, un aggregatore, un SDK client API o uno strato di integrazione ad alto traffico possono beneficiare di policy composte.
Installazione di gobreaker
Usa il pacchetto v2 per il nuovo codice:
go get github.com/sony/gobreaker/v2
Poi importa:
import "github.com/sony/gobreaker/v2"
Un Circuit Breaker di 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 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
}
Questo non è un client di produzione completo, ma mostra la struttura:
- il breaker avvolge la chiamata in uscita
- la richiesta HTTP riceve un context
- il client HTTP ha un timeout
- i fallimenti lato server contano come fallimenti del breaker
- gli errori di circuito aperto vengono mappati in un errore dell’applicazione
Configurazione delle impostazioni di gobreaker
Le impostazioni chiave meritano 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 è half-open.
Un numero piccolo è di solito più sicuro. Lo scopo di half-open è testare il recupero, non inviare tutto il traffico immediatamente.
Interval
Interval controlla quando i conteggi interni vengono resettati mentre il breaker è closed.
Se è zero, i conteggi non vengono resettati automaticamente. Un intervallo non nullo dà al breaker una finestra di memoria approssimativamente rotolante, anche se non è la stessa cosa di un’implementazione completa a sliding window.
Timeout
Timeout controlla per quanto tempo il breaker rimane open prima di passare a half-open.
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-30 secondi, poi regola in base alle metriche di produzione.
ReadyToTrip
ReadyToTrip decide quando il breaker dovrebbe aprirsi.
Una regola semplice sono i fallimenti consecutivi:
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures >= 5
}
Questo è facile da ragionare, ma potrebbe non essere corretto per servizi ad alto volume.
Un’altra opzione è il rapporto di fallimento 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 una dimensione del campione minuscola.
OnStateChange
OnStateChange è dove dovresti emettere log o metriche.
Al minimo, registra:
- nome del breaker
- stato precedente
- nuovo stato
- timestamp
Per i sistemi di produzione, espongono lo stato del breaker come metrica. I log sono utili per il debugging, ma le metriche sono migliori per gli allarmi e i dashboard.
IsSuccessful
IsSuccessful ti permette di decidere quali errori contano come fallimenti.
Questo è importante.
Non ogni errore dovrebbe aprire il breaker. Ad esempio, un 404 Not Found da un servizio utente potrebbe essere un risultato di business valido. Un 400 Bad Request potrebbe essere colpa del chiamante, non della dipendenza.
Un 503 Service Unavailable, timeout, reset della connessione o 429 Too Many Requests potrebbero essere un vero segnale di salute della dipendenza.
Fai attenzione qui. Contare gli errori sbagliati è uno dei modi più semplici per costruire un circuit breaker rumoroso.
Cosa dovrebbe contare come fallimento?
È qui che entra in gioco il giudizio ingegneristico.
Di solito conta questi come fallimenti:
- timeout di rete
- connessione rifiutata
- reset della connessione
- HTTP 500
- HTTP 502
- HTTP 503
- HTTP 504
- risposte 429 ripetute
- risposte non valide dalla dipendenza
- deadline del context superata durante la chiamata in uscita
Di solito NON contare questi come fallimenti della dipendenza:
- errori di validazione
- errori di serializzazione locale
- risposte 404 attese
- fallimenti di autorizzazione lato chiamante
- rifiuti di regole di business
- errori di input dell’utente
Il breaker dovrebbe rappresentare la 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 context controlla per quanto tempo quella chiamata può essere eseguita e se dovrebbe fermarsi quando il chiamante è gone.
Una buona chiamata in uscita di solito dovrebbe avere entrambi:
ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
defer cancel()
data, err := client.GetUser(ctx, userID)
Il context dovrebbe fluire attraverso la catena di chiamate:
context della richiesta in arrivo
-> metodo del servizio
-> metodo del client
-> richiesta HTTP
-> dipendenza
Evita di creare context di background staccati all’interno del codice basato sulla richiesta. Se la richiesta dell’utente viene cancellata, anche il lavoro downstream dovrebbe di solito fermarsi.
La regola calma è:
Il breaker protegge il sistema. Il context 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 esiste una risposta universale. Pensa a cosa vuoi contare.
Se ogni tentativo di retry passa attraverso il breaker, una singola richiesta utente può contribuire con più fallimenti. Questo potrebbe aprire il breaker più rapidamente, il che può essere bene o male.
Se il breaker avvolge l’intera operazione di retry, il breaker vede un successo o fallimento finale per richiesta utente. Questo è più tranquillo, ma potrebbe nascondere il numero di tentativi falliti.
Per molti servizi applicativi, questa forma è ragionevole:
richiesta utente
-> circuit breaker
-> policy 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 i client di livello inferiore, questa forma può anche avere senso:
richiesta utente
-> policy retry
-> circuit breaker
-> un tentativo HTTP con timeout
Questo significa che il breaker protegge ogni tentativo.
La regola più importante è questa:
Non fare retry alla cieca.
Usa:
- un numero massimo di retry piccolo
- backoff esponenziale
- jitter
- timeout per tentativo
- una deadline complessiva della richiesta
- idempotenza per le scritture
- metriche per i tentativi di retry
Senza questi, i retry possono trasformare una piccola interruzione in una più grande. Per un trattamento più approfondito della sicurezza dei retry, vedi Idempotency in Distributed Systems That Actually Works.
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, il retry è di solito sicuro.
Per le operazioni di scrittura, il retry può essere pericoloso.
Considera questa chiamata di pagamento:
POST /charge
Se la richiesta va in timeout, il pagamento è fallito? Forse. È riuscito ma la risposta è stata persa? Anche questo è possibile.
Se fai retry senza una chiave di idempotenza, potresti addebitare due volte.
Per le operazioni di scrittura, usa una o più di queste:
- chiavi di idempotenza
- ID della richiesta
- ID dell’operazione
- vincoli di unicità
- outbox transazionale
- orchestrazione del workflow
- reconciliazione esplicita
Un circuit breaker può impedirti di continuare a chiamare un provider di pagamento in avaria, 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 memorizzati nella cache
- restituire dati obsoleti con un avviso
- omettere una sezione non critica
- accodare il lavoro per dopo
- passare a un altro provider
- restituire un errore temporaneo
- mostrare funzionalità degradate
- fallire rapidamente la richiesta
Un fallback dovrebbe essere onesto.
Ad esempio, questo di solito è buono:
{
"status": "temporary_unavailable",
"message": "Recommendations are temporarily unavailable"
}
Questo è rischioso:
{
"recommendations": []
}
Un elenco vuoto può sembrare un risultato valido. Può nascondere un’interruzione di servizio, confondere gli utenti e rendere il debugging più difficile.
I fallback silenziosi sono tentanti. Sono anche pericolosi.
Circuit Breaker e Osservabilità
Un circuit breaker senza osservabilità è principalmente un generatore di sorprese.
Traccia almeno queste metriche:
- stato attuale del breaker
- cambi di stato
- chiamate consentite
- chiamate rifiutate
- successi
- fallimenti
- 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 dell’errore
Evita etichette ad alta cardinalità come ID utente, URL completo, ID della richiesta o messaggi di errore grezzi.
Dovresti essere in grado di rispondere a queste domande dai dashboard:
- Quali circuit breaker sono aperti in questo momento?
- Quanto spesso si aprono?
- Quale dipendenza ha causato l’apertura?
- Gli utenti stanno vedendo risposte di fallback?
- La latenza è migliorata dopo l’apertura del breaker?
- Il volume dei retry è aumentato prima dell’apertura del breaker?
- La dipendenza si è ripresa?
Se non puoi osservare il breaker, non puoi sintonizzarlo. Per il logging strutturato che si abbina bene con le metriche, vedi Structured Logging in Go with slog.
Una forma più Production-Friendly del Client HTTP
Per i servizi reali, evita di spargere la logica del circuit breaker tra i handler.
Crea un piccolo pacchetto client intorno alla dipendenza.
Struttura di esempio:
internal/
userservice/
client.go
errors.go
metrics.go
L’handler non dovrebbe conoscere i dettagli di gobreaker. Dovrebbe dipendere da un metodo del 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 context
- esecuzione del breaker
- gestione dei codici di stato
- decoding della risposta
- metriche
- mappatura degli errori
Questo mantiene la policy di resilienza vicino al confine della dipendenza. Per altro sulla classificazione degli errori ai confini, vedi Go Error Handling Architecture: Boundaries and Patterns.
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:
Tieni il breaker fuori dalla logica di business quando possibile.
Lo strato di business dovrebbe capire gli errori di dominio come:
payment provider unavailable
recommendations unavailable
profile service timeout
Non dovrebbe dover capire gli stati di gobreaker.
Questa separazione mantiene l’architettura pulita:
- i concern del trasporto restano nei client
- la policy di resilienza rimane vicino alle dipendenze
- la logica di dominio rimane leggibile
- gli handler restano leggeri
- i test sono più facili da scrivere
Questo articolo fa parte dell’App Architecture in Production topic — insieme alle guide su idempotenza, outbox, saga e orchestrazione in Integration Patterns.
Errori comuni
Errore 1: Nessun Timeout
Un circuit breaker non ferma magicamente le chiamate lente a meno che le chiamate non terminino.
Se l’operazione in uscita può appendere per sempre, il breaker potrebbe non vedere un fallimento abbastanza velocemente.
Usa sempre i timeout.
Errore 2: Un Global Breaker per Tutto
Non usare un breaker per tutte le dipendenze.
Un provider di email in avaria 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 fallimento differiscono.
Errore 3: Contare gli Errori del Chiamante come Fallimenti della Dipendenza
Se il tuo servizio invia input errati e riceve 400 Bad Request, questo di solito non è un downstream outage.
Non addestrare il breaker sui tuoi bug.
Errore 4: Retry su Scritture Non Idempotenti
I retry non sono gratuiti. Possono duplicare scritture, pagamenti, messaggi o effetti collaterali.
Rendi le scritture idempotenti prima di fare retry su di esse.
Errore 5: Nascondere le Interruzioni Dietro i Fallback
I fallback dovrebbero degradare grace, non falsificare la realtà.
Se una dipendenza è giù, le tue metriche e i log dovrebbero rendere questo evidente.
Errore 6: Regolazione Senza Dati di Produzione
Le soglie copiate dagli esempi sono solo punti di partenza.
Regola 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 Invece della Gestione della Capacità
Un circuit breaker non è un sostituto per:
- load shedding
- rate limiting
- limiti delle code
- autoscaling
- tuning del database
- limiti dei connection pool
- 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:
HTTP client timeout: 2 to 5 seconds
per-request context timeout: based on caller SLA
breaker failure rule: 5 consecutive failures or 50 percent failure after 20 requests
open timeout: 10 to 30 seconds
half-open requests: 1 to 5
retry count: 1 to 3 attempts
retry backoff: exponential with jitter
Questi non sono valori universali. Sono punti di partenza safe-ish.
Per le API user-facing, mantieni i budget di latenza totali stretti. Per i job di background, potresti 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 fallimento 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 viene allertato se il circuito continua ad aprirsi?
- Come verrà sintonizzato il breaker dopo il deployment?
Se non riesci a 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 viene 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 context viene rispettato
- la risposta di fallback viene restituita quando prevista
- le metriche vengono emesse sui cambi di stato
Usa server HTTP fake per i test di tipo integrazione:
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "unavailable", http.StatusServiceUnavailable)
}))
defer server.Close()
Per i unit test, nascondi la dipendenza dietro un’interfaccia e inietta un’implementazione fake.
Mantieni i test deterministici. Evita di dormire per durate reali lunghe. Configura timeout del breaker brevi nei test. Per altro sul testing del codice Go concurrent con fake time e bubble isolate, vedi Testing Concurrent Go Code with 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 siano molto specifiche.
Un breaker di produzione deve gestire:
- concorrenza
- transizioni di stato
- contatori
- sonde half-open
- callback
- classificazione personalizzata dei fallimenti
- comportamento senza race condition
- gestione degli errori prevedibile
Questo 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 un piccolo stack di resilienza esplicito:
context timeout
+ 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 failure a cascata. Usalo per fallire rapidamente quando una dipendenza è chiaramente malsana. Usalo per dare spazio ai sistemi sovraccarichi di riprendersi.
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
- Go Microservices for AI/ML Orchestration — contesto di orchestrazione più ampio dove si inseriscono i circuit breaker
- Saga Pattern in Distributed Transactions — pattern di transazione distribuita che si abbina con i circuit breaker
- Idempotency in Distributed Systems — sicurezza dei retry e operazioni idempotenti
- Transactional Outbox Pattern in Go — consegna affidabile degli eventi insieme ai pattern di resilienza
- Go Error Handling Architecture — classificazione degli errori ai confini delle dipendenze
- Testing Concurrent Go Code with synctest — testing del comportamento async con i circuit breaker
- Structured Logging in Go with slog — osservabilità accanto ai circuit breaker
github.com/sony/gobreaker/v2— pacchetto ufficiale gobreaker v2- Go Context Cancellation and Timeouts — pattern di context che si abbina con i circuit breaker