Circuit Breaker

Comment protéger vos services contre les pannes en cascade grâce au patron Circuit Breaker, avec ses états, ses fallbacks et sa récupération automatique.

Quel problème il résout

Dans une architecture de microservices, les services dépendent les uns des autres. Lorsqu’un service downstream devient lent ou cesse de répondre, les services qui l’appellent accumulent des connexions ouvertes, épuisent leurs thread pools et finissent eux aussi par tomber en panne. Cet effet domino est connu sous le nom de panne en cascade.

Exemple du problème

Utilisateur → API Gateway → Service A → Service B (lent/hors service)
                                         ⏳ timeout 30s
                                         ⏳ timeout 30s
                                         ⏳ timeout 30s

              Le Service A épuise ses threads
              en attendant la réponse de B

              L'API Gateway épuise ses threads
              en attendant la réponse de A

              Tout le système cesse de répondre

Sans protection, un seul service lent peut mettre à terre toute la plateforme. Le Circuit Breaker agit comme un disjoncteur automatique qui coupe la communication avec un service défaillant, empêchant le problème de se propager.

Ce patron a été popularisé par Michael Nygard dans son livre Release It! et tire son nom des disjoncteurs électriques qui coupent le circuit lorsqu’ils détectent une surcharge.

Impact réel des pannes en cascade

MétriqueSans Circuit BreakerAvec Circuit Breaker
Temps de détectionMinutes (quand les utilisateurs le signalent)Secondes (changement d’état du circuit)
Services affectésTous ceux qui dépendent du service en panneUniquement le service en panne
RécupérationManuelle (redémarrer les services, vider les files)Automatique (le semi-ouvert détecte la récupération)
Expérience utilisateurTimeouts longs, erreurs 500Réponse rapide avec fallback ou erreur claire
Ressources consomméesThreads et connexions épuisésRessources libérées immédiatement

Comment ça fonctionne

Le Circuit Breaker est un proxy placé entre un service et ses dépendances. Il surveille les appels et, en fonction du taux d’échec, décide d’autoriser ou de bloquer les requêtes.

Les trois états

                    Échecs > seuil
    ┌──────────┐ ──────────────────► ┌──────────┐
    │  FERMÉ   │                     │  OUVERT  │
    │ (normal) │ ◄──────────────── │ (coupé)  │
    └──────────┘   Test réussi       └────┬─────┘
         ▲                                │
         │      Le timeout expire         │
         │                                ▼
         │                          ┌───────────┐
         └────── Test réussi ───────│ SEMI-OUVERT│
                                    └───────────┘
                  Test échoue ─────► OUVERT

État Fermé (Closed)

C’est l’état normal. Toutes les requêtes passent vers le service downstream. Le circuit breaker surveille les résultats :

  • Il compte les échecs récents (erreurs, timeouts)
  • Si le taux d’échec dépasse un seuil (ex. : 50 % d’échecs sur les 10 derniers appels), il passe à Ouvert

État Ouvert (Open)

Le circuit est coupé. Les requêtes n’atteignent pas le service downstream. À la place, le circuit breaker répond immédiatement par une erreur ou exécute un fallback.

  • Il évite d’envoyer du trafic vers un service dont on sait qu’il est défaillant
  • Il libère les ressources (threads, connexions) qui seraient gaspillées à attendre des timeouts
  • Après une période d’attente (ex. : 30 secondes), il passe à Semi-Ouvert

État Semi-Ouvert (Half-Open)

État de test. Le circuit breaker autorise le passage d’un nombre limité de requêtes pour vérifier si le service s’est rétabli :

  • Si les requêtes de test réussissent → retour à Fermé
  • Si les requêtes de test échouent → retour à Ouvert

Paramètres de configuration

ParamètreDescriptionExempleConsidération
Failure thresholdPourcentage d’échecs pour ouvrir50 %Trop bas = faux positifs, trop haut = réaction lente
Request volumeMinimum de requêtes avant d’évaluer10 requêtesÉvite d’ouvrir le circuit pour 1 échec isolé
Sleep windowDurée en état ouvert30 secondesDoit laisser au service le temps de se rétablir
Success thresholdSuccès nécessaires pour fermer le circuit3 consécutifsPlus élevé = plus de confiance dans la récupération
TimeoutTemps d’attente maximal5 secondesDoit être inférieur au timeout du client

Stratégies de fallback

Lorsque le circuit est ouvert, au lieu de renvoyer une erreur brute, vous pouvez mettre en place des fallbacks :

StratégieDescriptionExemple
Valeur par défautRenvoyer des données statiquesAfficher un catalogue mis en cache
CacheDernière réponse réussie mise en cachePrix du produit d’il y a 5 min
Service alternatifRediriger vers un service de secoursUtiliser un service de paiement secondaire
Dégradation gracieuseFonctionnalité réduiteAfficher « prix non disponible »
Fail fastErreur immédiate avec message clairHTTP 503 avec header retry-after

Exemple de flux complet

État : FERMÉ
  Appel 1 → Service B → ✅ OK
  Appel 2 → Service B → ✅ OK
  Appel 3 → Service B → ❌ Timeout
  Appel 4 → Service B → ❌ Erreur 500
  Appel 5 → Service B → ❌ Timeout
  → Taux d'échec : 60 % > seuil 50 %
  → Passage à OUVERT ⚡

État : OUVERT
  Appel 6 → ⚡ Fallback immédiat (n'atteint pas B)
  Appel 7 → ⚡ Fallback immédiat
  ... (30 secondes d'attente)
  → Passage à SEMI-OUVERT

État : SEMI-OUVERT
  Appel 8 → Service B → ✅ OK (test)
  Appel 9 → Service B → ✅ OK (test)
  Appel 10 → Service B → ✅ OK (test)
  → 3 succès consécutifs
  → Passage à FERMÉ ✅

Exemple pratique : Circuit Breaker dans un service de paiement

Imaginez un service de commandes qui appelle un prestataire de paiement externe :

// Pseudocode de configuration du Circuit Breaker
circuitBreaker = new CircuitBreaker({
  name: "payment-provider",
  failureThreshold: 50,        // 50% d'échecs
  requestVolume: 10,           // minimum 10 requests pour évaluer
  sleepWindow: 30000,          // 30 secondes en état ouvert
  successThreshold: 3,         // 3 succès pour fermer
  timeout: 5000,               // 5 secondes de timeout par appel
  fallback: () => ({
    status: "PENDING",
    message: "Paiement en file d'attente, il sera traité dès que le service se rétablira"
  })
});

// Utilisation
async function procesarPago(pedidoId, monto) {
  try {
    resultado = await circuitBreaker.execute(() =>
      paymentProvider.charge(pedidoId, monto)
    );
    return resultado;
  } catch (error) {
    if (error instanceof CircuitOpenError) {
      // Le circuit est ouvert, le fallback a été exécuté
      encolarPagoParaReintento(pedidoId, monto);
      return error.fallbackResult;
    }
    throw error;
  }
}

Surveillance du Circuit Breaker

Les changements d’état du circuit sont des signaux précieux pour l’équipe d’exploitation :

// Métriques à exposer
circuit_breaker_state{name="payment-provider"} = "closed" | "open" | "half_open"
circuit_breaker_calls_total{name="payment-provider", result="success"}
circuit_breaker_calls_total{name="payment-provider", result="failure"}
circuit_breaker_calls_total{name="payment-provider", result="rejected"}
circuit_breaker_state_transitions_total{name="payment-provider", from="closed", to="open"}

// Alertes recommandées
ALERT CircuitBreakerOpen
  IF circuit_breaker_state == "open"
  FOR 1m
  LABELS { severity = "warning" }
  ANNOTATIONS { summary = "Circuit breaker ouvert pour payment-provider" }

Avantages

  • Prévient les pannes en cascade : un service en panne n’entraîne pas le reste du système
  • Fail fast : les requêtes échouent immédiatement au lieu d’attendre de longs timeouts
  • Libération des ressources : aucun thread ni connexion n’est gaspillé sur des appels voués à l’échec
  • Récupération automatique : l’état semi-ouvert détecte le moment où le service se rétablit
  • Expérience utilisateur améliorée : les fallbacks offrent des réponses dégradées plutôt que des erreurs
  • Observabilité : les changements d’état sont des signaux clairs pour les alertes et la surveillance

Compromis / Inconvénients

  • Configuration délicate : des seuils mal réglés peuvent ouvrir le circuit trop tôt ou trop tard
  • Complexité des fallbacks : concevoir des fallbacks utiles pour chaque dépendance demande des efforts
  • Données périmées : les fallbacks basés sur le cache peuvent renvoyer des informations obsolètes
  • Tests complexes : simuler les trois états et les transitions nécessite une infrastructure
  • Latence à la récupération : la sleep window introduit un délai entre la récupération réelle et sa détection
  • Ne résout pas la cause racine : le circuit breaker protège le système, mais le service downstream doit toujours être réparé

Quand l’utiliser

  • Appels à des services externes ou des microservices susceptibles de tomber en panne ou de devenir lents
  • Systèmes où une panne dans une dépendance ne doit pas affecter la disponibilité globale
  • Lorsque vous devez protéger des ressources partagées (thread pools, connection pools)
  • APIs qui appellent des services tiers aux SLA variables
  • Systèmes à forte exigence de disponibilité où la dégradation gracieuse est préférable
  • Lorsque vous disposez déjà de métriques et d’alertes pour surveiller l’état des circuits

Quand l’éviter

  • Appels à des ressources locales (base de données locale, fichiers) où le surcoût ne se justifie pas
  • Opérations qui doivent impérativement aboutir (sans possibilité de fallback)
  • Systèmes simples avec peu de dépendances externes
  • Lorsque la latence supplémentaire du proxy est inacceptable
  • Appels asynchrones où vous disposez déjà de files avec réessais et dead letter queues
  • Prototypes ou MVP où la résilience n’est pas une priorité

Technologies et implémentations courantes

CatégorieOptions
BibliothèquesResilience4j (Java), Polly (.NET), Hystrix (Java, legacy), opossum (Node.js)
Service MeshIstio, Linkerd, Envoy (circuit breaking au niveau de l’infrastructure)
CloudAWS App Mesh, Azure Traffic Manager
SurveillancePrometheus + Grafana, Datadog, New Relic
Patrons complémentairesRetry avec backoff, Bulkhead, Timeout, Rate Limiting

Relation avec d’autres patrons

Le Circuit Breaker est rarement utilisé seul. Il fonctionne mieux en combinaison avec d’autres patrons de résilience :

┌─────────────────────────────────────────┐
│         Chaîne de résilience            │
│                                         │
│  Requête                                │
│     │                                   │
│     ▼                                   │
│  ┌──────────┐                           │
│  │ Timeout  │  Limite le temps d'attente│
│  └────┬─────┘                           │
│       ▼                                 │
│  ┌──────────┐                           │
│  │  Retry   │  Réessaie avec backoff    │
│  └────┬─────┘                           │
│       ▼                                 │
│  ┌──────────┐                           │
│  │ Circuit  │  Coupe s'il y a beaucoup  │
│  │ Breaker  │  d'échecs consécutifs     │
│  └────┬─────┘                           │
│       ▼                                 │
│  ┌──────────┐                           │
│  │ Bulkhead │  Isole les ressources par │
│  │          │  dépendance               │
│  └────┬─────┘                           │
│       ▼                                 │
│  Service downstream                     │
└─────────────────────────────────────────┘
  • Timeout Pattern : définit combien de temps attendre avant de considérer un appel comme échoué. Le timeout alimente le circuit breaker en données d’échec
  • Retry Pattern : réessaie les appels échoués avec un backoff exponentiel. Les réessais ont lieu avant que le circuit ne s’ouvre ; une fois ouvert, on ne réessaie plus
  • Bulkhead : isole les ressources (threads, connexions) par dépendance afin qu’un service lent ne consomme pas toutes les ressources du système
  • Saga Pattern : lorsqu’une étape de la saga échoue et que le circuit breaker est ouvert, la saga déclenche des compensations au lieu de réessayer indéfiniment
  • API Gateway : le gateway peut implémenter des circuit breakers pour chaque service backend, centralisant ainsi la protection
  • Idempotency Pattern : lorsque le circuit se ferme et que les opérations sont réessayées, l’idempotence garantit que les effets ne sont pas dupliqués

Erreurs courantes

ErreurConséquenceSolution
Seuil trop basLe circuit s’ouvre pour des échecs isolésAugmenter le request volume minimum
Sleep window trop courteLe circuit oscille entre ouvert et ferméLaisser plus de temps de récupération
Aucun fallback définiErreur brute à l’utilisateur quand le circuit s’ouvreConcevoir des fallbacks pour chaque dépendance
Un circuit breaker globalUn service lent affecte tout le mondeUn circuit breaker par dépendance
Ne pas surveiller les transitionsVous ne détectez pas les problèmes à tempsAlertes sur les changements d’état
Ignorer l’état semi-ouvertTrafic excessif pendant le testLimiter les requêtes de test

Prochaines étapes

Le Circuit Breaker est une pièce fondamentale de la résilience dans les microservices. Pour compléter votre compréhension des patrons de résilience, explorez le Retry Pattern pour gérer les échecs transitoires avec backoff, le Timeout Pattern pour limiter les attentes, et le Bulkhead Pattern pour isoler les ressources par dépendance.