Un prestataire de paiement se met à répondre en dix secondes au lieu de cinquante millisecondes. Votre service de checkout continue de l'appeler, chaque requête bloque un worker pendant l'attente, et en une minute le checkout n'a plus aucun worker libre, y compris pour des requêtes qui n'avaient pas besoin du prestataire de paiement. Puis les services qui appellent le checkout commencent eux aussi à tomber en timeout. Une seule dépendance lente est devenue une panne qui touche plusieurs équipes.
Un circuit breaker arrête cette chaîne en refusant, pendant un temps, d'appeler une dépendance qui échoue manifestement, et en répondant immédiatement à la place. Cet article couvre les trois états avec un diagramme d'états, les seuils qui décident quand le breaker se déclenche, sa combinaison avec les timeouts, les retries et les bulkheads, ce qui fait une bonne solution de repli, et où placer le breaker : dans votre code ou dans un service mesh.
Pourquoi les pannes se propagent en cascade
Le calcul est simple et implacable. Un service doté de 200 threads de travail qui appelle une dépendance répondant en 50 ms peut traiter environ 4 000 requêtes par seconde sur ce chemin. Si la dépendance ralentit jusqu'à un timeout de 10 secondes, les mêmes 200 threads traitent 20 requêtes par seconde. Rien n'est cassé dans votre service ; sa capacité a simplement été consommée par l'attente.
Les retries aggravent les choses. Si trois couches de services réessaient chacune trois fois, une requête utilisateur peut se transformer en 27 appels à la dépendance en difficulté tout en bas, précisément au moment où elle peut le moins se le permettre.
Michael Nygard a décrit le circuit breaker en 2007 dans son livre sur la conception de logiciels prêts pour la production, et la description de Martin Fowler en a fait un élément standard de la conception de services : encapsuler les appels à une dépendance, compter les échecs et, lorsqu'ils dépassent un seuil, cesser d'appeler pendant un moment.
Les trois états
| État | Ce qui arrive aux appels | Passe à |
|---|---|---|
| Fermé (Closed) | Les appels passent ; les résultats sont enregistrés dans une fenêtre glissante | Ouvert, quand le taux d'échecs ou d'appels lents dépasse le seuil |
| Ouvert (Open) | Les appels échouent immédiatement sans toucher la dépendance ; la solution de repli répond | Semi-ouvert, après un temps d'attente |
| Semi-ouvert (Half-open) | Un petit nombre d'appels d'essai passent ; les autres échouent toujours immédiatement | Fermé si les essais réussissent, ouvert s'ils échouent |
L'état ouvert remplit deux fonctions à la fois. Il protège votre service, car échouer vite libère les workers instantanément. Il protège aussi la dépendance, qui obtient une pause dans le trafic pendant qu'elle se rétablit, au lieu d'une pile croissante de retries.
L'état semi-ouvert existe parce que le breaker ne peut pas savoir si la dépendance est de nouveau en bonne santé sans essayer. Laisser passer une poignée d'appels, et non tout le trafic, teste le rétablissement sans renverser la dépendance au moment même où elle revient.
Ce qui compte comme un échec
Décidez-le délibérément, car un mauvais choix ouvre les breakers pour de mauvaises raisons :
- À compter : les timeouts, les erreurs de connexion, les réponses 5xx et les appels plus lents que votre seuil d'appel lent.
- À ne pas compter : les réponses 4xx comme 400 ou 404. Elles décrivent une mauvaise requête, pas une dépendance défaillante, et un seul client qui se comporte mal ne devrait pas couper tout le monde.
- À décider au cas par cas : 429 Too Many Requests. Cela signifie que la dépendance se protège, ce qui est souvent une bonne raison de lever le pied.
Les appels lents comptent autant que les erreurs. Une dépendance qui répond avec succès à chaque requête en huit secondes est, pour une API interactive, une dépendance défaillante.
Choisir les seuils
Resilience4j, une bibliothèque de breaker très répandue sur la JVM, est livrée avec des valeurs par défaut prudentes qu'il vaut la peine de connaître, car beaucoup d'équipes ne les changent jamais :
| Paramètre | Défaut | Signification |
|---|---|---|
failureRateThreshold |
50 % | Ouvrir quand la moitié des appels enregistrés ont échoué |
slidingWindowType / slidingWindowSize |
Par nombre, 100 | Juger sur les 100 derniers appels |
minimumNumberOfCalls |
100 | Ne pas juger avant que 100 appels soient enregistrés |
slowCallDurationThreshold |
60 000 ms | Les appels plus lents comptent comme lents |
slowCallRateThreshold |
100 % | N'ouvrir que si tous les appels sont lents |
waitDurationInOpenState |
60 000 ms | Rester ouvert une minute |
permittedNumberOfCallsInHalfOpenState |
10 | Dix appels d'essai décident du rétablissement |
Pour une API interactive, les valeurs par défaut des appels lents reviennent à les désactiver : personne n'attend 60 secondes une réponse. Des points de départ raisonnables sont un seuil d'appel lent proche de votre objectif de latence, un taux d'appels lents de 50 % à 80 % et une attente à l'état ouvert de 10 à 30 secondes. Gardez minimumNumberOfCalls significatif : sur un point de terminaison peu sollicité, deux échecs sur trois appels sont du bruit, pas une panne. Les fenêtres temporelles se comportent mieux que les fenêtres par nombre quand le trafic varie beaucoup au cours de la journée.
Timeouts, retries et bulkheads
Un circuit breaker est l'un de quatre outils qui ne fonctionnent bien qu'ensemble :
- Les timeouts d'abord. Sans timeout, un appel lent n'échoue jamais, donc le breaker ne voit rien à compter. Fixez chaque timeout en dessous de celui de l'appelant, et propagez les échéances le long de la chaîne d'appels pour que personne ne continue de travailler sur une requête déjà abandonnée.
- Les retries ont besoin de limites. Ne réessayez que les opérations idempotentes, utilisez un backoff exponentiel avec jitter comme le décrit l'AWS Architecture Blog, réessayez à une seule couche plutôt qu'à toutes, et plafonnez les retries à une part du trafic. Les budgets de retry d'Envoy valent par défaut 20 % des requêtes actives quand vous les activez.
- Ne réessayez jamais contre un breaker ouvert. Resilience4j applique par défaut le retry à l'extérieur du breaker, si bien que chaque tentative le traverse ; configurez les retries pour qu'ils s'arrêtent sur l'erreur « call not permitted » au lieu de marteler un breaker qui a déjà tranché.
- Les bulkheads isolent. Donnez à chaque dépendance son propre pool de connexions ou sa propre limite de concurrence, pour qu'un prestataire de paiement lent ne puisse consommer que son pool, et non tous les workers du service.
Des solutions de repli qui ont du sens
Quand le breaker est ouvert, il faut bien que quelque chose réponde. La bonne solution de repli dépend de l'usage des données :
| Fonctionnalité | Bon repli | Mauvais repli |
|---|---|---|
| Recommandations | Des articles populaires, ou masquer la section | Réessayer le service de recommandations en boucle |
| Avatar utilisateur | Une image de remplacement | Bloquer la page jusqu'à ce que le service d'images réponde |
| Estimation de livraison | Une estimation en cache avec une mention | Une date de livraison inventée |
| Prix ou paiement | Une erreur claire et un moyen de réessayer | Un prix deviné ou un débit sauté en silence |
Une solution de repli doit être peu coûteuse et ne pas dépendre de ce qui vient d'échouer. Si la réponse honnête est « cela ne fonctionne pas pour l'instant », échouez vite avec une erreur claire ; c'est toujours bien mieux qu'un timeout.
Où placer le breaker
| Bibliothèque dans le processus | Service mesh ou proxy | |
|---|---|---|
| Exemples | Resilience4j (Java), Polly (.NET), gobreaker (Go), opossum (Node.js) | Circuit breaking et outlier detection d'Envoy |
| Voit | Exceptions, échecs métier, solutions de repli | Connexions, requêtes, codes de réponse |
| Solutions de repli | Oui, dans votre code | Non ; l'appelant reçoit une erreur |
| Cohérence entre équipes | Chaque équipe configure le sien | Une seule politique, appliquée partout |
Le modèle d'Envoy mérite d'être compris, car il diffère du modèle des manuels. Ses circuit breakers sont des limites de concurrence par cluster amont : par défaut 1 024 connexions, 1 024 requêtes en attente, 1 024 requêtes actives et 3 retries simultanés. La détection d'échecs par hôte est une fonctionnalité distincte, l'outlier detection, qui éjecte par défaut un hôte après cinq réponses 5xx consécutives pendant 30 secondes, plus longtemps en cas d'éjections répétées, et n'éjecte jamais plus de 10 % d'un cluster. Le diagramme d'architecture service mesh montre où se trouvent ces proxys.
Beaucoup d'équipes utilisent les deux : le mesh pour des limites cohérentes et l'éjection des mauvais hôtes, et une bibliothèque là où l'application a besoin d'une vraie solution de repli. Hystrix, de Netflix, qui a popularisé le pattern, est en mode maintenance depuis 2018 ; Resilience4j est son remplaçant habituel sur la JVM.
Observer et tester
Un breaker qui s'ouvre en silence transforme une panne en mystère. Émettez chaque changement d'état sous forme d'événement de log et de métrique, alertez quand un breaker reste ouvert, et affichez les états des breakers sur le même tableau de bord que la latence de la dépendance. Ensuite, testez : injectez de la latence et des erreurs dans la dépendance en préproduction et regardez le breaker s'ouvrir, la solution de repli répondre et le breaker se refermer. Le diagramme d'architecture d'observabilité couvre le volet métriques et traçage.
Le diagramme du circuit breaker
Contenu du diagramme
- CLOSED
- OPEN
- HALF-OPEN
- Checkout service (caller)
- Payment provider
- Fallback: clear error, retry later
- failure rate ≥ 50% of the last 100 calls
- after a 60 s wait
- a trial call fails
- trial calls succeed
- charge(), 2 s timeout, when closed or half-open
- when open: no call, fail fast
Comment le lire :
- Trois états. Fermé est l'état normal, ouvert échoue vite, semi-ouvert teste le rétablissement. Le point marque l'état initial.
- Les transitions portent leurs conditions : un taux d'échec de 50 % sur les 100 derniers appels ouvre le breaker, une attente de 60 secondes le fait passer en semi-ouvert, et le résultat des appels d'essai le referme ou le rouvre.
- Le chemin d'appel en dessous montre l'appelant, le prestataire de paiement derrière le breaker et la solution de repli qui répond tant que le breaker est ouvert.
- Le bloc Markdown liste les paramètres avec leurs valeurs par défaut.
Dessiner clairement les patterns de résilience
- Dessinez le breaker comme une machine à états. Les descriptions en prose brouillent la différence entre ouvert et semi-ouvert ; un diagramme la rend évidente.
- Écrivez les chiffres sur les transitions. « S'ouvre en cas d'échecs » invite au débat ; « s'ouvre à 50 % des 100 derniers appels » invite à la relecture.
- Montrez le chemin de repli, y compris ce que voit l'utilisateur.
- Placez timeouts et retries sur le diagramme du chemin d'appel, pour que les relecteurs puissent multiplier les retries d'une couche à l'autre.
Pour en savoir plus sur la notation des machines à états, voyez l'exemple de diagramme d'états. Le diagramme d'architecture microservices montre où se placent les breakers entre les services.
Le construire dans Archboard
- Connectez-vous avec Google ou GitHub et créez un diagramme.
- Ajoutez trois nœuds État du pack UML : Closed, Open et Half-open. Colorez-les en vert, rouge et ambre.
- Ajoutez une petite forme Ellipse du pack Base comme état initial et reliez-la à Closed.
- Dessinez les transitions comme des arêtes étiquetées : le seuil d'échec, le temps d'attente, le succès et l'échec des essais. Ajoutez à côté de chaque état un Texte du pack Base qui indique ce qui arrive aux appels.
- Sous la machine à états, ajoutez une Boîte du pack Base pour l'appelant, un Système externe du pack Modèle C4 pour le prestataire de paiement et une autre Boîte pour la solution de repli, avec une arête en pointillé pour le chemin de l'état ouvert.
- Ajoutez un bloc Markdown du pack Base avec les paramètres et leurs valeurs par défaut.
L'exemple utilise 8 nœuds de diagramme et reste dans les limites de l'offre Free.
FAQ
Quels sont les trois états d'un circuit breaker ? Fermé, où les appels passent et les échecs sont comptés ; ouvert, où les appels échouent immédiatement pendant un temps d'attente ; et semi-ouvert, où quelques appels d'essai décident de refermer le breaker ou de le rouvrir.
Quelle est la différence entre un circuit breaker et un retry ? Un retry répète un appel échoué dans l'espoir qu'il réussisse. Un circuit breaker arrête complètement les appels quand les échecs sont fréquents. Utilisés ensemble, les retries doivent être limités et s'arrêter quand le breaker est ouvert.
Les erreurs 4xx doivent-elles ouvrir un circuit breaker ? En général non. Un 400 ou un 404 signifie que la requête était erronée, pas que la dépendance a échoué. Comptez plutôt les timeouts, les erreurs de connexion, les réponses 5xx et les appels lents.
Ai-je besoin d'un circuit breaker si j'utilise un service mesh ? Un mesh vous donne des limites de concurrence et l'éjection des mauvais hôtes sans modifier le code. Il vous faut quand même un breaker dans le processus là où vous voulez une vraie solution de repli, comme une valeur en cache ou une fonctionnalité dégradée.
Quel est un bon temps d'attente pour l'état ouvert ? Assez long pour qu'une dépendance en difficulté se rétablisse, assez court pour que les utilisateurs ne subissent pas une longue panne une fois qu'elle est rétablie. Dix à trente secondes est un point de départ courant pour les API interactives.