Le pattern Circuit Breaker constitue l'un des mécanismes de résilience les plus importants dans les architectures microservices. Inspiré des disjoncteurs électriques, il protège les services des défaillances en cascade.
Mais un circuit breaker est efficace uniquement s'il est correctement configuré et surveillé.
Qu'est-ce que le Pattern Circuit Breaker ?
Le Circuit Breaker encapsule les appels vers des services externes et surveille leurs échecs. Il fonctionne selon trois états distincts.
Les trois états
Échecs < seuil
┌───────────────────────┐
│ │
▼ Échec │
┌─────────┐ ──────────► ┌─────────┐
│ CLOSED │ │ OPEN │
│ (Normal)│ ◄────────── │ (Fail) │
└─────────┘ Timeout └─────────┘
│ │
│ │ Timeout
│ ▼
│ ┌───────────┐
│ │HALF-OPEN │
│ Succès test │ (Test) │
└─────────────────┴───────────┘
État Closed (Normal)
Le fonctionnement normal :
# Pseudo-code circuit breaker
class CircuitBreaker:
def __init__(self, failure_threshold=5, timeout=30):
self.state = "CLOSED"
self.failure_count = 0
self.failure_threshold = failure_threshold
self.timeout = timeout
def call(self, func):
if self.state == "CLOSED":
try:
result = func()
self.failure_count = 0 # Reset on success
return result
except Exception as e:
self.failure_count += 1
if self.failure_count >= self.failure_threshold:
self.state = "OPEN"
self.opened_at = time.now()
raise e
État Open (Protection)
Le circuit refuse les requêtes :
def call(self, func):
if self.state == "OPEN":
if time.now() - self.opened_at > self.timeout:
self.state = "HALF-OPEN"
else:
raise CircuitOpenError("Circuit is open")
État Half-Open (Test)
Quelques requêtes de test passent :
def call(self, func):
if self.state == "HALF-OPEN":
try:
result = func()
self.state = "CLOSED" # Recovery !
self.failure_count = 0
return result
except Exception:
self.state = "OPEN" # Still failing
self.opened_at = time.now()
raise
Bénéfices du pattern
Le circuit breaker offre plusieurs avantages :
- Fail-fast : Pas d'attente sur un service défaillant
- Protection des ressources : Évite l'accumulation de connexions
- Temps de récupération : Le service cible n'est pas submergé
Pourquoi Monitorer les Circuit Breakers
Le monitoring transforme les circuit breakers en outil d'observabilité proactive.
Détection des dépendances dégradées
Un circuit qui s'ouvre signale un problème :
# Exemple d'événement
event:
type: circuit_breaker_opened
circuit: payment-service
timestamp: 2025-12-30T10:30:00Z
failure_count: 5
last_error: "Connection timeout"
Cette information est souvent disponible avant les alertes directes sur le service cible.
Validation des seuils
Le monitoring révèle si les seuils sont appropriés :
| Situation | Symptôme | Action |
|---|---|---|
| Trop sensible | Ouvertures fréquentes | Augmenter failure_threshold |
| Trop laxiste | Jamais d'ouverture | Réduire failure_threshold |
| Timeout court | Rejets inutiles | Augmenter timeout |
| Timeout long | Lenteur de récupération | Réduire timeout |
Impact utilisateur quantifié
Mesurez l'impact des circuits ouverts :
# Métriques d'impact
impact_metrics = {
"circuit": "payment-service",
"open_duration_seconds": 120,
"requests_rejected": 1500,
"estimated_revenue_lost": 15000, # Calcul business
"users_affected": 500
}
Identification des dépendances fragiles
Un circuit qui s'ouvre fréquemment signale une dépendance instable :
# Top 5 des circuits les plus problématiques
topk(5,
increase(circuit_breaker_state_changes{to="open"}[7d])
)
Comment Monitorer les Circuit Breakers
Métriques d'état
Exposez l'état actuel de chaque circuit :
from prometheus_client import Gauge, Counter
# État actuel (0=closed, 1=half-open, 2=open)
circuit_state = Gauge(
'circuit_breaker_state',
'Current state of circuit breaker',
['circuit_name']
)
# Compteurs de transitions
state_transitions = Counter(
'circuit_breaker_state_transitions_total',
'Circuit breaker state transitions',
['circuit_name', 'from_state', 'to_state']
)
# Exemple d'usage
circuit_state.labels(circuit_name='payment-service').set(2) # OPEN
state_transitions.labels(
circuit_name='payment-service',
from_state='closed',
to_state='open'
).inc()
Métriques de requêtes
Distinguez les types de réponses :
# Compteur par type de résultat
circuit_calls = Counter(
'circuit_breaker_calls_total',
'Circuit breaker calls',
['circuit_name', 'result'] # success, failure, rejected
)
# Latence des appels réussis
circuit_latency = Histogram(
'circuit_breaker_call_duration_seconds',
'Call duration through circuit breaker',
['circuit_name'],
buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
)
Intégration Resilience4j
Resilience4j expose des métriques Micrometer :
// Configuration avec métriques
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.slidingWindowSize(10)
.build();
CircuitBreaker circuitBreaker = CircuitBreaker.of("payment", config);
// Enregistrement des métriques
TaggedCircuitBreakerMetrics
.ofCircuitBreakerRegistry(circuitBreakerRegistry)
.bindTo(meterRegistry);
Métriques exposées automatiquement :
# État du circuit
resilience4j_circuitbreaker_state{name="payment"}
# Taux d'échec
resilience4j_circuitbreaker_failure_rate{name="payment"}
# Appels par type
resilience4j_circuitbreaker_calls_total{name="payment", kind="successful"}
resilience4j_circuitbreaker_calls_total{name="payment", kind="failed"}
resilience4j_circuitbreaker_calls_total{name="payment", kind="not_permitted"}
Dashboard de monitoring
Créez un dashboard dédié :
# Grafana dashboard panels
panels:
- title: "Circuit States Overview"
type: stat
query: circuit_breaker_state
- title: "Open Circuits"
type: table
query: circuit_breaker_state == 2
- title: "State Transitions (24h)"
type: timeseries
query: increase(circuit_breaker_state_transitions_total[1h])
- title: "Rejection Rate"
type: gauge
query: |
rate(circuit_breaker_calls_total{result="rejected"}[5m])
/ rate(circuit_breaker_calls_total[5m])
- title: "Top Problematic Circuits"
type: table
query: topk(10, increase(circuit_breaker_state_transitions_total{to="open"}[24h]))
Alertes intelligentes
Configurez des alertes graduées :
groups:
- name: circuit_breaker_alerts
rules:
# Alerte informative : ouverture de circuit
- alert: CircuitBreakerOpened
expr: circuit_breaker_state == 2
for: 0m
labels:
severity: info
annotations:
summary: "Circuit {{ $labels.circuit_name }} is open"
# Alerte warning : circuit ouvert > 5 minutes
- alert: CircuitBreakerOpenProlonged
expr: circuit_breaker_state == 2
for: 5m
labels:
severity: warning
annotations:
summary: "Circuit {{ $labels.circuit_name }} open for 5+ minutes"
# Alerte critique : circuit critique ouvert
- alert: CriticalCircuitOpen
expr: circuit_breaker_state{circuit_name=~"payment.*|order.*"} == 2
for: 1m
labels:
severity: critical
annotations:
summary: "Critical circuit {{ $labels.circuit_name }} is open"
# Alerte sur pattern : ouvertures fréquentes
- alert: CircuitBreakerFlapping
expr: increase(circuit_breaker_state_transitions_total{to="open"}[1h]) > 5
labels:
severity: warning
annotations:
summary: "Circuit {{ $labels.circuit_name }} opened 5+ times in 1 hour"
Bonnes Pratiques de Monitoring Circuit Breaker
Nommage descriptif
Nommez clairement vos circuits :
# Bon : descriptif
circuit_breaker(name="payment-service-charge")
circuit_breaker(name="inventory-service-reserve")
# Mauvais : générique
circuit_breaker(name="cb-1")
circuit_breaker(name="external-call")
order-to-payment-charge est immédiatement compréhensible.SLO pour les circuits
Définissez des objectifs mesurables :
slos:
- circuit: payment-service
max_open_time_per_month: "0.1%" # ~43 minutes
max_openings_per_day: 2
- circuit: inventory-service
max_open_time_per_month: "0.5%" # ~3.6 heures
max_openings_per_day: 5
Corrélation avec incidents
Reliez les événements de circuit aux incidents :
def on_circuit_open(circuit_name, error):
# Log structuré
logger.warning("Circuit opened", extra={
"circuit": circuit_name,
"error": str(error),
"downstream_service": extract_service(circuit_name)
})
# Création d'incident automatique si circuit critique
if is_critical_circuit(circuit_name):
incident_manager.create_incident(
title=f"Circuit {circuit_name} opened",
severity="high",
tags=["circuit-breaker", circuit_name]
)
Test avec chaos engineering
Testez régulièrement le comportement :
# Test avec injection de panne
def test_circuit_breaker_opens_on_failures():
circuit = CircuitBreaker(failure_threshold=3)
# Simuler des échecs
with mock_service_failure():
for _ in range(3):
with pytest.raises(ServiceError):
circuit.call(external_service.call)
# Vérifier que le circuit s'est ouvert
assert circuit.state == "OPEN"
# Vérifier que les appels sont rejetés
with pytest.raises(CircuitOpenError):
circuit.call(external_service.call)
Documentation des seuils
Documentez chaque configuration :
# circuit-breaker-config.yaml
circuits:
payment-service:
failure_threshold: 5
timeout_seconds: 30
rationale: |
- 5 échecs consécutifs car le service de paiement est critique
- Timeout de 30s car les pics de charge durent généralement < 20s
- Revu le 2025-12-01 après incident INC-456
inventory-service:
failure_threshold: 10
timeout_seconds: 60
rationale: |
- 10 échecs car le service a des latences variables normales
- Timeout de 60s car la récupération est lente
- Revu le 2025-11-15
Conclusion
Le monitoring des circuit breakers transforme ce pattern de résilience en outil d'observabilité proactive.
Les bénéfices du monitoring :
- Détection précoce des problèmes de dépendances
- Validation et optimisation des seuils
- Quantification de l'impact utilisateur
- Guide pour l'amélioration de la résilience
Cette observabilité guide l'optimisation continue de votre architecture.