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étrique | Sans Circuit Breaker | Avec Circuit Breaker |
|---|---|---|
| Temps de détection | Minutes (quand les utilisateurs le signalent) | Secondes (changement d’état du circuit) |
| Services affectés | Tous ceux qui dépendent du service en panne | Uniquement le service en panne |
| Récupération | Manuelle (redémarrer les services, vider les files) | Automatique (le semi-ouvert détecte la récupération) |
| Expérience utilisateur | Timeouts longs, erreurs 500 | Réponse rapide avec fallback ou erreur claire |
| Ressources consommées | Threads et connexions épuisés | Ressources 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ètre | Description | Exemple | Considération |
|---|---|---|---|
| Failure threshold | Pourcentage d’échecs pour ouvrir | 50 % | Trop bas = faux positifs, trop haut = réaction lente |
| Request volume | Minimum de requêtes avant d’évaluer | 10 requêtes | Évite d’ouvrir le circuit pour 1 échec isolé |
| Sleep window | Durée en état ouvert | 30 secondes | Doit laisser au service le temps de se rétablir |
| Success threshold | Succès nécessaires pour fermer le circuit | 3 consécutifs | Plus élevé = plus de confiance dans la récupération |
| Timeout | Temps d’attente maximal | 5 secondes | Doit ê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égie | Description | Exemple |
|---|---|---|
| Valeur par défaut | Renvoyer des données statiques | Afficher un catalogue mis en cache |
| Cache | Dernière réponse réussie mise en cache | Prix du produit d’il y a 5 min |
| Service alternatif | Rediriger vers un service de secours | Utiliser un service de paiement secondaire |
| Dégradation gracieuse | Fonctionnalité réduite | Afficher « prix non disponible » |
| Fail fast | Erreur immédiate avec message clair | HTTP 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égorie | Options |
|---|---|
| Bibliothèques | Resilience4j (Java), Polly (.NET), Hystrix (Java, legacy), opossum (Node.js) |
| Service Mesh | Istio, Linkerd, Envoy (circuit breaking au niveau de l’infrastructure) |
| Cloud | AWS App Mesh, Azure Traffic Manager |
| Surveillance | Prometheus + Grafana, Datadog, New Relic |
| Patrons complémentaires | Retry 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
| Erreur | Conséquence | Solution |
|---|---|---|
| Seuil trop bas | Le circuit s’ouvre pour des échecs isolés | Augmenter le request volume minimum |
| Sleep window trop courte | Le circuit oscille entre ouvert et fermé | Laisser plus de temps de récupération |
| Aucun fallback défini | Erreur brute à l’utilisateur quand le circuit s’ouvre | Concevoir des fallbacks pour chaque dépendance |
| Un circuit breaker global | Un service lent affecte tout le monde | Un circuit breaker par dépendance |
| Ne pas surveiller les transitions | Vous ne détectez pas les problèmes à temps | Alertes sur les changements d’état |
| Ignorer l’état semi-ouvert | Trafic excessif pendant le test | Limiter 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.