← Retour au blog
Résilience

Pattern Circuit Breaker : un bouclier contre l'effet domino

Le Circuit Breaker, disjoncteur d'une architecture résiliente : machine à états CLOSED/OPEN/HALF-OPEN pour stopper la dégradation en cascade entre services.

📅 ✍️ Antoine Coulon
resiliencecircuit-breakerdistributed-systemsfault-tolerancenodejs

Quand un service externe défaille, le vrai risque n’est pas sa panne, c’est qu’il entraîne tout votre système dans sa chute. Un appel qui ne répond plus, ce sont des connexions qui s’accumulent, des threads bloqués, un event loop saturé, et de proche en proche une application entière qui s’effondre. Le Circuit Breaker est le pattern qui empêche cet effet domino.

Ce troisième volet de la série sur la résilience prolonge naturellement les deux précédents : si vos retries (épisode 1) et vos timeouts (épisode 2) déclenchent en boucle sans jamais aboutir, c’est le signe que le service d’en face est probablement down. Et qu’il est temps d’arrêter de frapper à une porte fermée.

Le principe : un disjoncteur pour vos services

Vous connaissez le disjoncteur électrique : quand un appareil défaille, il coupe le circuit pour protéger l’installation. Le Circuit Breaker applique exactement cette idée au logiciel. Un appareil défectueux sur le réseau électrique, c’est l’équivalent d’un service tiers défaillant dans votre architecture distribuée : si on ne l’isole pas, il met tout le reste en danger.

Concrètement, le Circuit Breaker s’intercale comme un proxy entre votre système et le service externe. Il surveille les appels qui le traversent et décide, selon leur résultat, de les laisser passer ou de les couper immédiatement. C’est ni plus ni moins le disjoncteur d’une architecture résiliente : quand un service chauffe, on coupe avant que tout parte en fumée.

Une machine à états à trois positions

Le cœur du pattern est une machine à états. À tout instant, le breaker se trouve dans l’un de ces trois états :

ÉtatComportement
CLOSEDTout fonctionne normalement : les requêtes passent vers le service.
OPENLe service est considéré down : les requêtes sont bloquées immédiatement, sans même tenter l’appel.
HALF-OPENÉtat de transition : on laisse passer quelques appels, de façon contrôlée et limitée, pour tester si le service est de retour.

Ce mécanisme apporte trois bénéfices directs :

Des transitions automatiques

Tout l’intérêt du pattern est que ces changements d’état sont gérés pour vous, en fonction de seuils que vous définissez :

En pratique avec Node.js

Inutile de réimplémenter cette machine à états vous-même : le pattern est très largement supporté par des bibliothèques matures. L’exemple ci-dessous utilise cockatiel pour protéger un appel base de données. On configure un breaker qui s’ouvre après 5 échecs consécutifs et reste ouvert 10 secondes avant de tenter une reprise.

/**
 * Le pattern Circuit Breaker permet de protéger notre application
 * contre l'effet domino suite à une panne d'un service externe.
 */
import {
  ConsecutiveBreaker,
  ExponentialBackoff,
  retry,
  handleAll,
  circuitBreaker,
  wrap,
} from "cockatiel";

/**
 * Ici on définit un Circuit Breaker qui arrête d'appeler la fonction
 * exécutée pendant 10 secondes si elle échoue 5 fois consécutivement.
 * Il existe d'autres types de Breakers, qui permettent d'affiner
 * et d'ajuster la stratégie de résilience.
 */
const breaker = circuitBreaker(handleAll, {
  halfOpenAfter: 10 * 1000,
  breaker: new ConsecutiveBreaker(5),
});

const dbCallWithBreakerAndTimeout = () =>
  breaker.execute(() =>
    knex.select().from("books").timeout(10_000, { cancel: true })
  );

dbCallWithBreakerAndTimeout()
  .then(() => {
    // [...]
  })
  .catch(() => {
    // [...]
  });

/**
 * On peut même combiner le Circuit Breaker avec un pattern Retry + Timeout
 * de manière à rendre l'appel initial plus résilient avant de déclencher
 * le comportement du Circuit Breaker.
 */
const retryPolicy = retry(handleAll, {
  maxAttempts: 3,
  backoff: new ExponentialBackoff(),
});

const breakerWithRetry = wrap(retryPolicy, breaker);

const fullyResilientDbCall = () =>
  breakerWithRetry.execute(() =>
    knex.select().from("books").timeout(10_000, { cancel: true })
  );

L’intérêt du dernier bloc mérite d’être souligné : le Circuit Breaker ne s’oppose pas aux patterns vus précédemment, il les complète. En enveloppant le breaker avec une politique de retry et un timeout (wrap), on rend chaque appel plus résilient avant d’envisager d’ouvrir le circuit. Le breaker ne s’ouvre alors que lorsque, malgré les tentatives, le service reste durablement indisponible : exactement le signal qu’on veut détecter.

Un pattern disponible partout

Bonne nouvelle : ce pattern est tellement répandu qu’il existe une implémentation éprouvée dans pratiquement tous les écosystèmes, ce qui le rend facile à adopter :

Conclusion

Le Circuit Breaker ne répare pas un service en panne, ce n’est pas son rôle. Il fait quelque chose de plus précieux : il empêche cette panne de devenir la vôtre. En isolant un dépendant défaillant, il protège vos ressources, soulage le service en difficulté et orchestre une reprise progressive une fois la tempête passée.

Combiné aux retries et aux timeouts des épisodes précédents, il forme le socle d’une stratégie de résilience cohérente. Prenez les devants : mieux vaut couper un circuit volontairement que de regarder tout le système partir en fumée.