Le motif Circuit Breaker en Go : stopper les pannes en cascade
Stoppez les défaillances en cascade dans les microservices Go.
Un disjoncteur (circuit breaker) empêche votre service Go de harceler une dépendance défaillante, évitant ainsi les défaillances en cascade qui consomment goroutines, sockets et mémoire jusqu’à l’effondrement total du système.
La partie difficile n’est pas la machine d’états. C’est de décider où placer le disjoncteur, ce qui constitue une défaillance, comment il interagit avec les délais d’expiration (timeouts) et les tentatives de reprise (retries), et ce que votre service doit faire lorsque le circuit est ouvert.

En Go, le motif du disjoncteur est particulièrement utile pour les appels sortants : API HTTP, passerelles de paiement, services de recherche, fournisseurs d’emails, passerelles LLM, microservices internes et autres dépendances qui peuvent devenir lentes, surchargées ou partiellement indisponibles. Bien utilisé, un disjoncteur réduit les défaillances en cascade. Mal utilisé, il devient un autre mode de défaillance obscur.
Quel problème résout un disjoncteur ?
Les systèmes distribués échouent rarement proprement.
Une dépendance n’est peut-être pas complètement en panne. Elle peut :
- retourner des erreurs 500
- retourner des réponses de limitation de taux (429)
- accepter les connexions TCP mais ne jamais répondre
- répondre en 30 secondes au lieu de 300 millisecondes
- échouer uniquement pour certaines requêtes
- être surchargée parce que tous les clients réessaient simultanément
Le pire cas n’est souvent pas une erreur critique. C’est une dépendance lente.
Les appels lents consomment des goroutines, des sockets, des connexions de base de données, de la mémoire et de la capacité de traitement. Si votre service continue d’attendre une dépendance déjà malsaine, votre service peut devenir malsain à son tour.
Un disjoncteur empêche cela en échouant rapidement après que la dépendance franchisse un seuil de défaillance.
Au lieu de faire cela indéfiniment :
requête -> appeler la dépendance -> attendre -> timeout -> retry -> attendre -> échec
le service finit par faire ceci :
requête -> circuit ouvert -> retourner une solution de repli ou une erreur immédiatement
Cet échec rapide n’est pas toujours agréable, mais il est prévisible. Une défaillance prévisible est plus facile à gérer qu’un effondrement lent.
Les trois états du disjoncteur
La plupart des disjoncteurs utilisent trois états.
Fermé (Closed)
Le circuit est fermé pendant le fonctionnement normal.
Les requêtes sont autorisées à passer. Le disjoncteur enregistre les succès et les échecs. Si le nombre ou le ratio d’échecs franchit un seuil, le disjoncteur s’ouvre.
Fermé ne signifie pas « sûr pour toujours ». Cela signifie « le trafic est actuellement autorisé ».
Ouvert (Open)
Le circuit est ouvert lorsque la dépendance est considérée comme malsaine.
Les requêtes sont rejetées immédiatement. Le service doit retourner une solution de repli, une réponse mise en cache, une réponse dégradée ou une erreur amont claire.
Ouvert ne répare pas la dépendance. Il donne à la dépendance le temps de récupérer et protège l’appelant de perdre des ressources.
Demi-ouvert (Half-Open)
Après une période de refroidissement, le disjoncteur entre dans un état demi-ouvert.
Seul un nombre limité de requêtes d’essai est autorisé à passer. Si elles réussissent, le disjoncteur se ferme. Si elles échouent, le disjoncteur s’ouvre à nouveau.
L’état demi-ouvert est important car il évite deux extrêmes néfastes :
- ne jamais réessayer la dépendance
- renvoyer tout le trafic trop rapidement
Les transitions d’état ressemblent à ceci :
Disjoncteur vs Délai d’expiration vs Tentative de reprise
Une erreur courante consiste à traiter les disjoncteurs, les tentatives de reprise et les délais d’expiration comme interchangeables. Ils sont liés, mais ils résolvent des problèmes différents.
Délai d’expiration (Timeout)
Un délai d’expiration limite la durée pendant laquelle une opération peut s’exécuter.
En Go, cela signifie généralement passer un context.Context avec une date limite ou un timeout dans l’appel sortant.
Un délai d’expiration répond à cette question :
Combien de temps suis-je prêt à attendre pour cet appel unique ?
Tentative de reprise (Retry)
Une tentative de reprise répète une opération lorsque l’échec peut être temporaire.
Les tentatives de reprise sont utiles pour les petits glitchs réseau, les réponses 503 temporaires, les réinitialisations de connexion et autres échefs transitoires.
Une tentative de reprise répond à cette question :
Devrais-je réessayer cet appel ?
Disjoncteur (Circuit Breaker)
Un disjoncteur arrête les appels lorsque la dépendance est probablement malsaine.
Il répond à cette question :
Devrais-je appeler cette dépendance du tout en ce moment ?
Limiteur de débit (Rate Limiter)
Un limiteur de débit contrôle la quantité de trafic autorisée sur une période donnée.
Il répond à cette question :
Combien de trafic cet appelant doit-il envoyer ?
Cloisonnement (Bulkhead)
Le cloisonnement isole les ressources pour qu’une dépendance ne puisse pas tout consommer.
Il répond à cette question :
Dans quelle mesure cette dépendance peut-elle endommager mon service ?
Ces motifs sont plus efficaces lorsqu’ils sont utilisés ensemble. Un disjoncteur sans délais d’expiration est faible. Les tentatives de reprise sans variation (jitter) peuvent créer des tempêtes de retries. Une solution de repli sans métriques peut masquer une panne.
Quand utiliser un disjoncteur en Go
Utilisez un disjoncteur lorsque votre service appelle une dépendance qui peut échouer indépendamment de votre service.
De bons candidats incluent :
- API HTTP externes
- processeurs de paiement
- fournisseurs d’email et SMS
- services de recherche
- services de recommandation
- passerelles d’inférence LLM
- points de terminaison de microservices internes
- API SaaS tierces
- services de lecture lents ou surchargés
Les disjoncteurs sont particulièrement utiles lorsque l’appelant peut se dégrader gracieusement.
Par exemple :
- retourner des données produit mises en cache
- ignorer un bloc de recommandation
- marquer un fournisseur de paiement comme temporairement indisponible
- mettre au file d’attente le travail pour plus tard, avec une file aux messages morts (dead-letter queue) comme filet de sécurité pour les messages qui n’aboutissent jamais, même après la récupération de la dépendance
- retourner une réponse partielle
- échouer rapidement avec une erreur temporaire claire
La question importante n’est pas « cet appel peut-il échouer ? » Tout peut échouer. La meilleure question est :
Si cette dépendance échoue, devrions-nous continuer à lui envoyer tout le trafic ?
Si la réponse est non, un disjoncteur peut aider.
Quand ne pas utiliser un disjoncteur
N’ajoutez pas un disjoncteur à chaque fonction juste parce que le motif semble responsable.
Un disjoncteur n’est généralement pas utile pour :
- les appels de fonctions locaux dans le même processus
- les opérations CRUD simples dans un monolithe
- la logique de validation
- les règles métier déterministes
- les opérations locales uniquement CPU
- les chemins de code où aucune solution de repli utile n’existe
- les opérations d’écriture qui ne sont pas idempotentes
- les dépendances déjà protégées par une couche de workflow plus robuste
Un disjoncteur ne remplace pas non plus l’hygiène de base :
- définir des délais d’expiration
- propager le contexte
- utiliser correctement les pools de connexions
- gérer explicitement les erreurs
- rendre les tentatives de reprise sûres
- observer les taux d’échec
Un mauvais disjoncteur peut rendre un système plus difficile à comprendre. Il peut masquer le vrai problème, rejeter le trafic trop agressivement ou créer un comportement confus pendant la récupération.
La règle légèrement opinionnée est simple :
Ajoutez des disjoncteurs aux limites de dépendance, pas partout.
Choisir une bibliothèque de disjoncteur Go
Vous pouvez implémenter un disjoncteur basique vous-même, mais la plupart des services Go de production devraient utiliser une bibliothèque.
Le choix simple le plus courant est sony/gobreaker.
Il vous offre :
- les états fermé, ouvert et demi-ouvert
- des seuils de défaillance configurables
- un délai d’expiration de l’état ouvert configurable
- des rappels de changement d’état
- des compteurs de requêtes
- un support générique en v2
- une surface d’API petite
Pour des pipelines de résilience plus complexes, vous pouvez également regarder des bibliothèques qui composent plusieurs politiques, telles que la reprise, le délai d’expiration, la solution de repli, la limitation de débit, l’isolement par cloisonnement et la rupture de circuit. Cela peut être utile lorsque vous souhaitez une seule couche de résilience autour d’une opération.
Pour de nombreux services Go, cependant, gobreaker suffit.
Comparaison des paquets de disjoncteurs Go
Go n’inclut pas de disjoncteur intégré dans la bibliothèque standard. En pratique, vous choisissez généralement entre une petite bibliothèque de disjoncteur, un cadre de résilience plus volumineux ou un ancien paquet de style Hystrix.
Pour la plupart des nouveaux services Go, la décision est simple :
- utilisez
sony/gobreakersi vous voulez un petit disjoncteur focalisé - utilisez
failsafe-gosi vous voulez composer des disjoncteurs avec des tentatives de reprise, des délais d’expiration, des solutions de repli, des cloisons, des limites de débit et d’autres politiques de résilience - évitez de commencer de nouveaux projets sur
hystrix-gosauf si vous avez déjà du code legacy l’utilisant
| Package | Meilleur pour | Points forts | Compromis |
|---|---|---|---|
sony/gobreaker/v2 |
Petits disjoncteurs autour des clients HTTP/RPC | API petite, support générique v2, modèle d’état clair, facile à envelopper les clients de dépendance | Ne résout que la rupture de circuit ; les tentatives de reprise, délais d’expiration et solutions de repli doivent être composés séparément |
failsafe-go |
Composition complète des politiques de résilience | Retry, fallback, circuit breaker, timeout, bulkhead, rate limiter, cache, hedge, adaptive limiter, et adaptive throttler | Plus de concepts à apprendre ; plus lourd que nécessaire si vous voulez seulement un disjoncteur basique |
afex/hystrix-go |
Systèmes legacy de style Hystrix | Concepts Hystrix familiers, exécution de style commande, usage historique | Conception plus ancienne ; pas le meilleur choix par défaut pour les nouveaux services Go |
go-kit/kit/circuitbreaker |
Services basés sur les points de terminaison Go kit | S’intègre au style middleware Go kit et à l’architecture endpoint | Principalement utile si votre service utilise déjà Go kit |
cep21/circuit |
Comportement de disjoncteur de style Hystrix | Approche de style Hystrix plus complète | Moins courant comme choix simple par défaut ; peut être plus que nécessaire pour les petits services |
Ma recommandation par défaut est ennuyeuse intentionnellement : commencez avec sony/gobreaker/v2 lorsque vous n’avez besoin que d’un disjoncteur. Tournez-vous vers failsafe-go lorsque vous souhaitez exprimer une politique de résilience complète en un seul endroit.
Cette séparation garde l’architecture propre. Un petit client de service n’a pas besoin d’un cadre de résilience complet juste pour arrêter d’appeler une dépendance défaillante. Mais une passerelle, un agrégateur, un SDK client API ou une couche d’intégration à fort trafic peuvent bénéficier de politiques composées.
Installation de gobreaker
Utilisez le paquet v2 pour le nouveau code :
go get github.com/sony/gobreaker/v2
Puis importez-le :
import "github.com/sony/gobreaker/v2"
Un disjoncteur basique en Go
Voici un petit exemple autour d’un appel HTTP.
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/sony/gobreaker/v2"
)
var ErrTemporaryUnavailable = errors.New("dépendance temporairement indisponible")
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
}
Ce n’est pas un client de production complet, mais il montre la structure :
- le disjoncteur enveloppe l’appel sortant
- la requête HTTP reçoit un contexte
- le client HTTP a un délai d’expiration
- les erreurs côté serveur comptent comme des échecs du disjoncteur
- les erreurs de circuit ouvert sont mappées dans une erreur application
Configuration des paramètres gobreaker
Les paramètres clés valent la peine d’être compris.
Name
Name identifie le disjoncteur.
Utilisez un nom stable et spécifique :
payment-api
search-service
llm-gateway
user-service
Évitez les noms vagues tels que :
http-client
external-call
default
Vous voudrez ce nom dans les journaux et les métriques.
MaxRequests
MaxRequests contrôle combien de requêtes sont autorisées lorsque le disjoncteur est demi-ouvert.
Un petit nombre est généralement plus sûr. L’objectif du demi-ouvert est de tester la récupération, pas d’envoyer tout le trafic immédiatement.
Interval
Interval contrôle quand les compteurs internes sont effacés lorsque le disjoncteur est fermé.
S’il est nul, les compteurs ne sont pas effacés automatiquement. Un intervalle non nul donne au disjoncteur une fenêtre de mémoire type “rolling”, bien que ce ne soit pas la même chose qu’une implémentation complète de fenêtre glissante.
Timeout
Timeout contrôle combien de temps le disjoncteur reste ouvert avant de passer en demi-ouvert.
Si le délai d’expiration est trop court, votre service continuera de sonder une dépendance qui ne s’est pas rétablie. S’il est trop long, la récupération sera retardée.
Commencez par quelque chose de conservateur, tel que 10 à 30 secondes, puis ajustez à partir des métriques de production.
ReadyToTrip
ReadyToTrip décide quand le disjoncteur doit s’ouvrir.
Une règle simple est les échecs consécutifs :
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures >= 5
}
C’est facile à comprendre, mais cela peut ne pas convenre aux services à fort volume.
Une autre option est le ratio d’échec après un nombre minimum de requêtes :
ReadyToTrip: func(counts gobreaker.Counts) bool {
total := counts.Requests
failures := counts.TotalFailures
if total < 20 {
return false
}
return float64(failures)/float64(total) >= 0.5
}
Cela évite d’ouvrir le circuit après un échantillon minuscule.
OnStateChange
OnStateChange est l’endroit où vous devriez émettre des journaux ou des métriques.
Au minimum, enregistrez :
- le nom du disjoncteur
- l’ancien état
- le nouvel état
- l’horodatage
Pour les systèmes de production, exposez l’état du disjoncteur comme une métrique. Les journaux sont utiles pour le débogage, mais les métriques sont meilleures pour l’alerte et les tableaux de bord.
IsSuccessful
IsSuccessful vous permet de décider quelles erreurs comptent comme des échecs.
C’est important.
Toutes les erreurs ne devraient pas ouvrir le disjoncteur. Par exemple, un 404 Not Found d’un service utilisateur peut être un résultat métier valide. Un 400 Bad Request peut être la faute de l’appelant, pas celle de la dépendance.
Un 503 Service Unavailable, un timeout, une réinitialisation de connexion ou un 429 Too Many Requests peuvent être un signal réel de santé de la dépendance.
Soyez prudent ici. Compter les mauvaises erreurs est l’un des moyens les plus simples de créer un disjoncteur bruyant.
Qu’est-ce qui doit compter comme une défaillance ?
C’est ici que le jugement technique compte.
Comptez généralement ces erreurs comme des échecs :
- timeouts réseau
- connexion refusée
- réinitialisation de connexion
- HTTP 500
- HTTP 502
- HTTP 503
- HTTP 504
- réponses 429 répétées
- réponses malformées de la dépendance
- dépassement du délai d’expiration du contexte pendant l’appel sortant
Ne comptez généralement pas ces erreurs comme des échecs de dépendance :
- erreurs de validation
- erreurs de sérialisation locales
- réponses 404 attendues
- échecs d’autorisation côté appelant
- rejets de règles métier
- erreurs de saisie utilisateur
Le disjoncteur doit représenter la santé de la dépendance, pas une erreur générale de l’application.
Disjoncteurs et context.Context
En Go, les disjoncteurs ne doivent pas remplacer context.Context.
Un disjoncteur décide s’il faut tenter un appel. Un contexte contrôle combien de temps cet appel peut durer et s’il doit s’arrêter lorsque l’appelant a disparu.
Un bon appel sortant devrait généralement avoir les deux :
ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
defer cancel()
data, err := client.GetUser(ctx, userID)
Le contexte doit traverser la chaîne d’appels :
contexte de requête entrante
-> méthode du service
-> méthode du client
-> requête HTTP
-> dépendance
Évitez de créer des contextes de fond détachés dans le code lié à la requête. Si la requête utilisateur est annulée, le travail en aval devrait généralement s’arrêter également.
La règle calme est :
Le disjoncteur protège le système. Le contexte protège la requête.
Vous avez normalement besoin des deux.
Disjoncteurs et tentatives de reprise (Retries)
Les tentatives de reprise et les disjoncteurs peuvent bien fonctionner ensemble, mais l’ordre importe.
Le choix par défaut le plus sûr est :
timeout par tentative
retry avec backoff et jitter
disjoncteur autour de l'appel à la dépendance
Mais il n’y a pas de réponse universelle. Pensez à ce que vous voulez compter.
Si chaque tentative de retry passe par le disjoncteur, une seule requête utilisateur peut contribuer à plusieurs échecs. Cela peut ouvrir le disjoncteur plus rapidement, ce qui peut être bien ou mal.
Si le disjoncteur enveloppe toute l’opération de retry, le disjoncteur voit un succès final ou un échec par requête utilisateur. C’est plus calme, mais cela peut masquer le nombre d’essais échoués.
Pour de nombreux services applicatifs, cette forme est raisonnable :
requête utilisateur
-> disjoncteur
-> politique de retry
-> une tentative HTTP avec timeout
Cela signifie que le disjoncteur suit si l’opération de dépendance a finalement fonctionné pour l’appelant.
Pour les clients de niveau inférieur, cette forme peut également avoir du sens :
requête utilisateur
-> politique de retry
-> disjoncteur
-> une tentative HTTP avec timeout
Cela signifie que le disjoncteur protège chaque tentative.
La règle la plus importante est celle-ci :
Ne faites pas de retry aveuglément.
Utilisez :
- un nombre maximum de retries petit
- un backoff exponentiel
- du jitter
- des timeouts par tentative
- une date limite globale pour la requête
- l’idempotence pour les écritures
- des métriques pour les tentatives de retry
Sans cela, les tentatives de reprise peuvent transformer une petite panne en une panne plus importante. Pour un traitement plus approfondi de la sécurité des retries, consultez Idempotence dans les Systèmes Distribués qui Fonctionnent Réellement.
Disjoncteurs et Idempotence
Les disjoncteurs apparaissent souvent à côté des tentatives de reprise, et les retries soulèvent la question de l’idempotence.
Pour les opérations de lecture, le retry est généralement sûr.
Pour les opérations d’écriture, le retry peut être dangereux.
Considérez cet appel de paiement :
POST /charge
Si la requête expire, le paiement a-t-il échoué ? Peut-être. A-t-il réussi mais que la réponse a été perdue ? Peut-être aussi.
Si vous réessayez sans clé d’idempotence, vous pourriez facturer deux fois.
Pour les opérations d’écriture, utilisez une ou plusieurs de ces méthodes :
- clés d’idempotence
- IDs de requête
- IDs d’opération
- contraintes d’unicité
- boîte de sortie transactionnelle (transactional outbox)
- orchestration de workflow
- réconciliation explicite
Un disjoncteur peut vous empêcher de continuer à appeler un fournisseur de paiement défaillant, mais il ne peut pas rendre sûres les tentatives de reprise non sûres.
Disjoncteurs et Solutions de repli (Fallbacks)
Lorsque le circuit est ouvert, votre service a besoin d’un plan.
Les stratégies de repli possibles incluent :
- retourner des données mises en cache
- retourner des données périmées avec un avertissement
- omettre une section non critique
- mettre au file d’attente le travail pour plus tard
- basculer vers un autre fournisseur
- retourner une erreur temporaire
- afficher une fonctionnalité dégradée
- échouer rapidement la requête
Une solution de repli doit être honnête.
Par exemple, ceci est généralement bon :
{
"status": "temporary_unavailable",
"message": "Les recommandations sont temporairement indisponibles"
}
Ceci est risqué :
{
"recommendations": []
}
Une liste vide peut sembler être un résultat valide. Cela peut masquer une panne, confondre les utilisateurs et rendre le débogage plus difficile.
Les solutions de repli silencieuses sont tentantes. Elles sont également dangereuses.
Disjoncteurs et Observabilité
Un disjoncteur sans observabilité est surtout un générateur de surprises.
Suivez au moins ces métriques :
- l’état actuel du disjoncteur
- les changements d’état
- les appels autorisés
- les appels rejetés
- les succès
- les échecs
- les timeouts
- les réponses de repli
- les tentatives de retry
- la latence aval
- les codes de statut aval
Les libellés utiles incluent :
- le nom du disjoncteur
- le nom de la dépendance
- le nom de l’opération
- la classe de statut
- la catégorie d’erreur
Évitez les libellés à haute cardinalité tels que l’ID utilisateur, l’URL complète, l’ID de requête ou les messages d’erreur bruts.
Vous devriez pouvoir répondre à ces questions depuis les tableaux de bord :
- Quels disjoncteurs sont ouverts en ce moment ?
- À quelle fréquence s’ouvrent-ils ?
- Quelle dépendance a provoqué l’ouverture ?
- Les utilisateurs voient-ils des réponses de repli ?
- La latence s’est-elle améliorée après l’ouverture du disjoncteur ?
- Le volume de retry a-t-il augmenté avant l’ouverture du disjoncteur ?
- La dépendance s’est-elle rétablie ?
Si vous ne pouvez pas observer le disjoncteur, vous ne pouvez pas le régler. Pour la journalisation structurée qui s’associe bien aux métriques, consultez Journalisation structurée en Go avec slog.
Une forme de client HTTP plus adaptée à la production
Pour les services réels, évitez de disperser la logique du disjoncteur dans les handlers.
Créez un petit paquet client autour de la dépendance.
Structure d’exemple :
internal/
userservice/
client.go
errors.go
metrics.go
Le handler ne devrait pas connaître les détails de gobreaker. Il devrait dépendre d’une méthode client de niveau domaine :
type UserService interface {
GetUser(ctx context.Context, userID string) (*User, error)
}
Ensuite, l’implémentation peut contenir :
- la création de la requête HTTP
- la propagation du contexte
- l’exécution du disjoncteur
- la gestion des codes de statut
- le décodage de la réponse
- les métriques
- le mappage des erreurs
Cela garde la politique de résilience près de la limite de dépendance. Pour plus de détails sur la classification des erreurs aux limites, consultez Architecture de gestion des erreurs Go : Limites et motifs.
Où les disjoncteurs s’intègrent dans l’architecture applicative
Le motif du disjoncteur appartient aux frontières d’intégration.
Dans une application Go, cela signifie généralement :
Gardez le disjoncteur hors de la logique métier lorsque cela est possible.
La couche métier devrait comprendre les erreurs de domaine telles que :
fournisseur de paiement indisponible
recommandations indisponibles
timeout du service de profil
Elle n’a pas besoin de comprendre les états de gobreaker.
Cette séparation garde l’architecture propre :
- les préoccupations de transport restent dans les clients
- la politique de résilience reste près des dépendances
- la logique métier reste lisible
- les handlers restent fins
- les tests sont plus faciles à écrire
Cet article fait partie du thème App Architecture in Production — aux côtés des guides d’idempotence, outbox, saga et orchestration dans les Intégrations Patterns.
Erreurs courantes
Erreur 1 : Pas de délai d’expiration (Timeout)
Un disjoncteur n’arrête pas magiquement les appels lents à moins que les appels ne retournent.
Si l’opération sortante peut se bloquer indéfiniment, le disjoncteur peut ne pas voir un échec assez rapidement.
Toujours utiliser des timeouts.
Erreur 2 : Un seul disjoncteur global pour tout
N’utilisez pas un seul disjoncteur pour toutes les dépendances.
Un fournisseur d’email défaillant ne devrait pas ouvrir le circuit pour votre fournisseur de paiement. Un point de terminaison de recherche lent ne devrait pas bloquer les appels de profil utilisateur.
Utilisez des disjoncteurs séparés pour des opérations de dépendance distinctes lorsque leurs modes d’échec diffèrent.
Erreur 3 : Compter les erreurs de l’appelant comme des échecs de dépendance
Si votre service envoie une mauvaise entrée et reçoit un 400 Bad Request, il s’agit généralement d’une panne en aval.
N’entraînez pas le disjoncteur sur vos propres bugs.
Erreur 4 : Réessayer des écritures non idempotentes
Les retries ne sont pas gratuits. Ils peuvent dupliquer les écritures, les paiements, les messages ou les effets de bord.
Rendez les écritures idempotentes avant de les réessayer.
Erreur 5 : Masquer les pannes derrière des solutions de repli
Les solutions de repli doivent se dégrader gracieusement, pas falsifier la réalité.
Si une dépendance est en panne, vos métriques et journaux devraient rendre cela évident.
Erreur 6 : Réglage sans données de production
Les seuils copiés à partir d’exemples ne sont que des points de départ.
Réglez en fonction de :
- le volume de requêtes
- le taux d’erreur normal
- la latence de la dépendance
- l’impact utilisateur
- le temps de récupération
- la qualité de la solution de repli
Erreur 7 : Utiliser des disjoncteurs à la place de la gestion de la capacité
Un disjoncteur n’est pas un substitut pour :
- le déchargement de charge (load shedding)
- la limitation de débit
- les limites de file d’attente
- la mise à l’échelle automatique (autoscaling)
- le réglage de la base de données
- les limites de pool de connexions
- les quotas amont
C’est une partie d’une stratégie de résilience.
Valeurs par défaut pratiques
Pour un service Go typique appelant une dépendance HTTP interne, un point de départ raisonnable pourrait être :
Timeout du client HTTP : 2 à 5 secondes
Timeout du contexte par requête : basé sur l'SLA de l'appelant
Règle d'échec du disjoncteur : 5 échecs consécutifs ou 50 % d'échec après 20 requêtes
Timeout ouvert : 10 à 30 secondes
Requêtes demi-ouvertes : 1 à 5
Nombre de retries : 1 à 3 tentatives
Backoff du retry : exponentiel avec jitter
Ce ne sont pas des valeurs universelles. Ce sont des points de départ sûrs.
Pour les API destinées aux utilisateurs, gardez les budgets de latence totaux serrés. Pour les jobs de fond, vous pouvez tolérer des attentes plus longues. Pour les fournisseurs de paiement, soyez beaucoup plus prudent avec les retries et l’idempotence.
Liste de contrôle du disjoncteur
Avant d’ajouter un disjoncteur, répondez à ces questions :
- Quelle dépendance est protégée ?
- Quelle opération est protégée ?
- Quelles erreurs comptent comme un échec de dépendance ?
- Quelles erreurs doivent être ignorées par le disjoncteur ?
- Quel timeout s’applique à chaque appel ?
- Les retries sont-ils autorisés ?
- Les écritures sont-elles idempotentes ?
- Que se passe-t-il lorsque le circuit est ouvert ?
- Y a-t-il une solution de repli ?
- La solution de repli est-elle visible dans les métriques ?
- Qui est alerté si le circuit s’ouvre continuellement ?
- Comment le disjoncteur sera-t-il réglé après le déploiement ?
Si vous ne pouvez pas répondre à ces questions, l’ajout d’un disjoncteur peut créer plus de confusion que de résilience.
Tester les disjoncteurs en Go
Testez le comportement, pas la machine d’états interne de la bibliothèque.
Les tests utiles incluent :
- la dépendance réussit et la réponse est retournée
- la dépendance échoue à plusieurs reprises et le circuit s’ouvre
- le circuit ouvert retourne une erreur temporaire
- les erreurs de validation côté client ne déclenchent pas le disjoncteur
- le timeout du contexte est respecté
- la réponse de repli est retournée quand attendu
- les métriques sont émises lors des changements d’état
Utilisez des serveurs HTTP fictifs pour les tests de type intégration :
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "unavailable", http.StatusServiceUnavailable)
}))
defer server.Close()
Pour les tests unitaires, masquez la dépendance derrière une interface et injectez une implémentation fictive.
Gardez les tests déterministes. Évitez de dormir pendant de longues durées réelles. Configurez des timeouts de disjoncteur courts dans les tests. Pour plus de détails sur le test du code Go concurrent avec du temps fictif et des bulles isolées, consultez Test du code Go concurrent avec testing/synctest.
Devriez-vous construire votre propre disjoncteur ?
Construire un petit disjoncteur est un bon exercice d’apprentissage. Cela vous aide à comprendre la machine d’états.
Pour le code de production, préférez une bibliothèque maintenue sauf si vos besoins sont très spécifiques.
Un disjoncteur de production doit gérer :
- la concurrence
- les transitions d’état
- les compteurs
- les sondes demi-ouvertes
- les rappels
- la classification personnalisée des échecs
- un comportement sans course (race-free)
- une gestion des erreurs prévisible
Cela n’est pas impossible, mais il est facile de se tromper subtilement.
La bibliothèque ennuyeuse est généralement le meilleur choix.
Conclusion
Le motif du disjoncteur n’est pas de la poussière de fiabilité magique.
En Go, il fonctionne mieux lorsqu’il fait partie d’une pile de résilience petite et explicite :
timeout de contexte
+ retry avec backoff et jitter
+ disjoncteur
+ solution de repli
+ métriques
Le motif est le plus utile aux limites de dépendance, en particulier autour des services distants qui peuvent devenir lents ou partiellement indisponibles.
Utilisez-le pour arrêter les défaillances en cascade. Utilisez-le pour échouer rapidement lorsqu’une dépendance est clairement malsaine. Utilisez-le pour donner de la marge aux systèmes surchargés pour se rétablir.
Mais ne l’utilisez pas comme prétexte pour ignorer les timeouts, l’idempotence, l’observabilité ou une architecture propre.
Un bon disjoncteur rend l’échec plus clair et moins coûteux. Un mauvais rend juste l’échec plus mystérieux.
Références
- Go Microservices for AI/ML Orchestration — contexte d’orchestration plus large où s’insèrent les disjoncteurs
- Saga Pattern in Distributed Transactions — motifs de transactions distribuées qui s’associent aux disjoncteurs
- Idempotency in Distributed Systems — sécurité des retries et opérations idempotentes
- Transactional Outbox Pattern in Go — livraison d’événements fiables aux côtés des motifs de résilience
- Go Error Handling Architecture — classification des erreurs aux limites de dépendance
- Testing Concurrent Go Code with synctest — test du comportement asynchrone avec des disjoncteurs
- Structured Logging in Go with slog — observabilité aux côtés des disjoncteurs
github.com/sony/gobreaker/v2— paquet officiel gobreaker v2- Go Context Cancellation and Timeouts — motifs de contexte qui s’associent aux disjoncteurs