Aller au contenu

Circuit breaker et bulkhead

Deux patterns complementaires pour contenir les pannes — l'un coupe, l'autre isole.


Circuit breaker

Le circuit breaker protégé un service appelant quand un service appelé devient defaillant. Sans lui, chaque requête attendrait un timeout complet avant d'échouer — epuisant les threads, les connexions, et propageant la panne en cascade vers tous les appelants.

Le pattern emprunte son nom aux disjoncteurs électriques : quand la charge est trop forte, on coupe le circuit pour protéger l'installation. On le retablit ensuite progressivement.

stateDiagram-v2
    [*] --> Closed

    Closed --> Open : seuil d'erreurs atteint
    Open --> HalfOpen : timer expire
    HalfOpen --> Closed : sondes reussies
    HalfOpen --> Open : sonde echouee

    Closed : Closed\nRequetes passent normalement
    Open : Open\nRequetes bloquees — fallback immediat
    HalfOpen : Half-Open\nQuelques requetes de sonde

États et transitions

Closed — état normal. Les requêtes passent. Le circuit compte les succès et les échecs dans une fenêtre glissante.

Open — le seuil d'échec a été dépassé. Les requêtes sont bloquees immédiatement, sans appeler la dépendance. Le fallback est retourné. Cet état persiste pendant la durée du timeout configuré.

Half-Open — après expiration du timer, quelques requêtes de sonde sont laissees passer pour tester si la dépendance est retablie. Si elles reussissent, le circuit se referme. Si elles échouent, il reste ouvert et le timer recommence.

Parametres clés

Parametre Exemple Rôle
Error threshold 50% sur 10 requêtes Seuil d'ouverture du circuit
Minimum requests 10 requêtes Taille de la fenêtre de mesure
Timeout (open) 30s Durée en état Open avant de tenter Half-Open
Half-open probes 3 requêtes Nombre de sondes reussies pour refermer

Stratégies de fallback

Quand le circuit est ouvert, le service doit répondre sans appeler la dépendance defaillante. Trois stratégies :

  • Cache — retourner la dernière réponse connue si elle est acceptable (produits populaires, solde mis en cache)
  • Valeur par défaut — retourner une liste vide, un score neutre, un message générique qui préservé l'expérience de base
  • Message d'erreur explicite — informer l'utilisateur proprement avec un message utile plutôt que de le faire attendre indéfiniment

Exemple concret : le service de recommandation tombe. Après 10 requêtes dont 60% ont échoué, le circuit s'ouvre. En fallback, l'application affiche les produits les plus populaires de la semaine, issus d'un cache Redis. L'utilisateur voit quelque chose d'utile. Après 30 secondes, le circuit passe en Half-Open. Une requête de sonde est envoyee. Si le service répond, le circuit se referme et les recommandations personnalisees reviennent.


Library-based vs sidecar-based

Le circuit breaker peut être implémenté de deux manières fondamentalement différentes, chacune avec ses forces et ses contraintes.

Implémentation par bibliotheque

Le circuit breaker est intégré dans le code applicatif via une bibliotheque. Le développeur configure les seuils, les fallbacks et la logique de transition directement dans le service.

Bibliotheque Langage Statut Notes
Resilience4j Java Actif Successeur de Hystrix, léger, modulaire
Polly .NET Actif Intégration native avec HttpClientFactory
opossum Node.js Actif Simple, bien maintenu
gobreaker Go Actif Implémentation Sony, minimaliste
Hystrix Java Deprecie Pionnier Netflix, plus maintenu

Avantages : contrôle fin sur les fallbacks, accès au contexte métier (le fallback peut retourner des données métier pertinentes), pas de composant d'infrastructure supplémentaire.

Inconvénients : chaque service doit implémenter le pattern, risque d'inconsistance entre équipes, pas de vision globale de l'état des circuits.

Implémentation par sidecar (service mesh)

Le circuit breaker est géré par un proxy sidecar (Envoy, Linkerd proxy) qui intercepte tout le trafic réseau du service. La configuration est declarative et centralisee.

# Istio DestinationRule — circuit breaker via Envoy
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: catalogue-circuit-breaker
spec:
  host: catalogue-service
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        h2UpgradePolicy: DEFAULT
        http1MaxPendingRequests: 50
        http2MaxRequests: 100
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 30s
      baseEjectionTime: 30s
      maxEjectionPercent: 50

Avantages : uniforme sur tous les services (polyglotte), pas de code applicatif, configuration centralisee, métriques automatiques sur l'état des circuits.

Inconvénients : fallback limite (le sidecar ne connait pas le contexte métier), complexité opérationnelle du mesh, latence ajoutee par le proxy.

Choix pragmatique

Critère Library Sidecar
Fallback métier riche Oui Limite
Polyglotte Non (par langage) Oui
Cohérence entre équipes Difficile Automatique
Complexité opérationnelle Faible Élevée (mesh)
Visibilité centralisee A construire Native

En pratique, les deux approches coexistent souvent. Le service mesh fournit une protection de base (outlier détection, ejection) et le code applicatif ajoute des fallbacks métier là où c'est nécessaire.


Monitoring de l'état des circuits

Un circuit breaker invisible est un circuit breaker dangereux. Si personne ne sait qu'un circuit est ouvert, le fallback peut durer des heures sans que l'équipe n'en soit informee.

Métriques a exposer

Chaque circuit breaker doit émettre des métriques :

  • État courant (gauge) — 0 = closed, 1 = half-open, 2 = open
  • Nombre de transitions (counter) — ouvertures et fermetures par fenêtre de temps
  • Taux de rejet (counter) — requêtes bloquees par le circuit ouvert
  • Durée en état open (histogram) — combien de temps le circuit reste ouvert

Alerting

Deux alertes minimales :

  • Circuit ouvert depuis plus de X minutes — un circuit ouvert longtemps signale un problème persistant qui nécessité une intervention
  • Oscillation rapide (flapping) — le circuit s'ouvre et se ferme plusieurs fois par minute, signe d'une dépendance instable en limite de seuil
graph LR
    CB["Circuit Breaker"] -->|metriques| Prom["Prometheus"]
    Prom -->|regles| AM["Alertmanager"]
    AM -->|notification| On["On-call"]
    Prom -->|visualisation| Graf["Grafana Dashboard"]

    style CB fill:#264653,color:#fff
    style Prom fill:#2a9d8f,color:#fff

Bulkhead

Le pattern bulkhead (cloison etanche) isole les ressources par service dependant pour limiter la propagation des pannes. Le nom vient de l'architecture navale : des compartiments etanches empêchent qu'une seule voie d'eau coule tout le bateau.

graph TD
    App[Application]

    App --> P1[Pool A — Paiement\n10 threads / 20 connexions]
    App --> P2[Pool B — Catalogue\n10 threads / 20 connexions]
    App --> P3[Pool C — Recommandation\n5 threads / 10 connexions]

    P1 --> SvcA[Service Paiement]
    P2 --> SvcB[Service Catalogue]
    P3 --> SvcC[Service Recommandation\nEN PANNE]

    style P3 fill:#c44,color:#fff
    style SvcC fill:#c44,color:#fff

Principe

Un service lent ou bloque ne doit pas épuiser toutes les ressources de l'application et faire tomber les autres. Sans bulkhead, si le service de recommandation répond en 30 secondes, ses requêtes occupent des threads en attente. En quelques minutes, le pool global de threads est sature. Plus aucune requête — y compris les paiements critiques — ne peut être traitee. Le service de recommandation a contamine tout le système.

Le bulkhead garantit que chaque service dependant ne peut consommer qu'un sous-ensemble défini des ressources. Si le pool du service de recommandation est plein, seules les requêtes de recommandation sont bloquees. Les paiements continuent à fonctionner normalement.

Types d'isolation

Thread pool isolation — chaque dépendance a son propre pool de threads. Modèle popularise par Hystrix (Netflix). Le thread appelant retourné immédiatement si le pool est plein (rejet plutôt que blocage). Coûteux en mémoire mais très efficace.

Semaphore isolation — limite le nombre de requêtes concurrentes par dépendance via un compteur. Plus léger qu'un pool de threads, mais le thread appelant reste bloque si la limite est atteinte. Adapté aux appels rapides.

Connection pool isolation — chaque dépendance a son quota de connexions dans un pool partage mais borne. Utile pour les connexions base de données ou les clients HTTP.

Process isolation — chaque dépendance est appelée via un sidecar ou un processus dédié. Plus lourd, mais isole aussi les pannes mémoire et CPU. Modèle utilisé dans les service meshes (Envoy proxy).

Dimensionner les pools

Le dimensionnement des bulkheads est un exercice d'équilibre :

  • Trop petit — les requêtes sont rejetees en conditions normales, degradant le service sans raison
  • Trop grand — la protection est illusoire car le pool ne sera jamais plein avant que les degats ne se propagent

Méthode pragmatique :

  1. Mesurer le throughput normal de chaque dépendance (requêtes/seconde)
  2. Mesurer la latence p99 de chaque dépendance
  3. Calculer la concurrence maximale : throughput * latence_p99
  4. Ajouter une marge de 50% pour les pics
  5. Fixer le pool a cette valeur et monitorer les rejets

Bulkhead et circuit breaker : deux couches complementaires

Les deux patterns protègent contre la propagation des pannes mais a des niveaux différents :

Situation Bulkhead Circuit breaker
Service lent (pas encore en erreur) Limite les ressources consommees Pas encore déclenché
Service en erreur franche Limite les ressources gaspillees Coupe les appels, active fallback
Retour à la normale Pool se vide naturellement Half-open puis closed

Un service lent peut saturer un pool (le bulkhead agit) sans atteindre le seuil d'erreur (le circuit breaker n'a pas encore agi). Déployer les deux ensemble pour une protection complète.

graph TD
    Req["Requete entrante"]
    BH{"Bulkhead\nPool disponible ?"}
    CB{"Circuit breaker\nCircuit ferme ?"}
    Call["Appel dependance"]
    FB["Fallback"]

    Req --> BH
    BH -->|oui| CB
    BH -->|non — pool plein| FB
    CB -->|ferme| Call
    CB -->|ouvert| FB

    style FB fill:#e76f51,color:#fff
    style Call fill:#2a9d8f,color:#fff

La sequence est : le bulkhead agit en premier (le pool a-t-il de la place ?), puis le circuit breaker décidé (le circuit est-il ferme ?). Si les deux laissent passer, la requête atteint la dépendance. Si l'un des deux bloque, le fallback est retourné immédiatement.


Chapitre suivant : Retry et backoff — retenter intelligemment sans amplifier les pannes.