Circuit breaker et bulkhead contre la propagation des pannes
- Accueil
- Architecture Cyber
- Circuit breaker et bulkhead contre la propagation des pannes
Circuit breaker et bulkhead contre la propagation des pannes
Section intitulée « Circuit breaker et bulkhead contre la propagation des pannes »Un circuit breaker coupe temporairement les appels vers une dépendance qui échoue, tandis qu’un bulkhead réserve des capacités séparées à des flux distincts. Ces deux patterns ne bloquent pas une attaque par déni de service à la frontière réseau, mais empêchent une pression locale de transformer toute l’application en débris fumants.
Un restaurant garde parfois une salle fermée quand la cuisine est débordée, et sépare les groupes pour qu’une table bruyante ne gâche pas tout le service. Ces choix n’empêchent pas les problèmes d’arriver, mais ils évitent qu’un problème limité fasse tomber toute la soirée.
→ Lire l’explication complète de la marmotte
Circuit breaker : couper avant la saturation
Section intitulée « Circuit breaker : couper avant la saturation »Le circuit breaker surveille les erreurs et les délais d’une dépendance distante, par exemple une API de paiement, un annuaire d’identités ou une base de données. Quand un seuil est dépassé, il passe à l’état ouvert : les nouveaux appels échouent localement, sans ouvrir de connexion ni attendre l’expiration du délai distant.
Trois états suffisent :
- Fermé : les appels passent et leurs résultats alimentent les statistiques.
- Ouvert : les appels sont rejetés immédiatement pendant une durée définie.
- Semi-ouvert : un nombre limité d’appels de test passe ; leurs résultats décident de fermer ou rouvrir le circuit.
La valeur de sécurité est indirecte mais réelle. Une attaque par déni de service peut rendre une dépendance lente sans la rendre totalement indisponible. Sans circuit breaker, chaque requête entrante occupe un thread, une connexion, de la mémoire et parfois un processus de chiffrement Transport Layer Security (TLS). La saturation se propage alors de service en service. Une dépendance défaillante n’a plus besoin d’être compromise pour devenir un amplificateur d’attaque.
Le compromis central est brutal : on gagne la disponibilité des fonctions non dépendantes, on paie par des refus anticipés et des réponses partielles. Il faut accepter ce coût. Attendre trente secondes une réponse de profil utilisateur pour finalement renvoyer une erreur 503 ne rend service à personne, sauf au graphique de consommation mémoire qui cherchait une carrière artistique.
Un circuit breaker doit être placé côté appelant, pas seulement devant le fournisseur. Un proxy ou une passerelle peut limiter le trafic entrant, mais il ne connaît pas toujours la valeur métier d’un appel sortant. Un service de commande peut continuer à afficher un panier sans joindre le moteur de recommandation ; il ne peut pas créer une transaction de paiement sans le prestataire correspondant. Le repli doit refléter cette différence.
Les seuils doivent être calculés par dépendance et par opération. Mélanger GET /catalogue et POST /paiements dans le même circuit produit un interrupteur décoratif. Les deux appels ont des budgets de latence, des conséquences et des politiques de reprise différents.
Une fenêtre glissante de 20 à 100 appels donne généralement une réaction assez rapide sans basculer au premier incident isolé. Le seuil d’erreur doit inclure les expirations de délai et les réponses 5xx, pas les réponses 4xx provoquées par une requête invalide. Un code 401 n’indique pas que l’annuaire est malade ; il indique qu’une identité ne passe pas le contrôle. Confondre les deux déclenche une panne auto-infligée, discipline étonnamment populaire.
Bulkhead : isoler les ressources qui meurent ensemble
Section intitulée « Bulkhead : isoler les ressources qui meurent ensemble »Le bulkhead, ou cloisonnement de ressources, sépare les pools de threads, connexions, files d’attente ou instances selon les dépendances et les niveaux de service. Son objectif est d’empêcher un flux lent de consommer les ressources nécessaires aux autres flux.
Le cloisonnement est plus déterminant qu’un circuit breaker pour contenir une attaque qui produit beaucoup de requêtes coûteuses mais techniquement valides. Une recherche plein texte, une exportation de données ou un calcul de rapport peuvent épuiser les workers disponibles sans générer une seule erreur. Le circuit reste fermé, car la dépendance répond. L’application, elle, ne répond plus.
Un découpage minimal distingue :
- les requêtes interactives authentifiées ;
- les parcours critiques, tels que connexion, paiement ou révocation ;
- les traitements asynchrones ;
- les appels vers chaque dépendance fragile ;
- les opérations d’administration.
Chaque compartiment reçoit une limite explicite : nombre de requêtes simultanées, profondeur de file, temps maximal d’attente et débit maximal. Une file sans limite n’est pas de la résilience ; c’est une fuite mémoire avec une interface utilisateur.
Le choix entre pool de threads et limite de concurrence dépend du modèle d’exécution. Avec des threads bloquants, un pool dédié évite qu’une dépendance distante immobilise tous les workers. Avec une exécution asynchrone, une limite de concurrence et une file bornée évitent l’accumulation de promesses, buffers et sockets. Dans les deux cas, le rejet doit arriver avant la saturation globale.
Le bulkhead doit aussi exister au niveau de l’infrastructure. Des limites de CPU, mémoire et connexions par charge de travail empêchent un conteneur de priver les autres processus. La page sur l’observabilité d’architecture complète ce sujet : sans métriques par pool et par dépendance, le cloisonnement devient une boîte noire qui refuse des requêtes au hasard apparent.
Déni de service : ce que ces patterns contiennent, et ce qu’ils ne contiennent pas
Section intitulée « Déni de service : ce que ces patterns contiennent, et ce qu’ils ne contiennent pas »Le circuit breaker et le bulkhead réduisent le rayon d’explosion d’une attaque applicative. Ils limitent notamment :
- l’épuisement de threads par attente réseau ;
- l’ouverture massive de connexions sortantes ;
- la saturation d’un pool de connexions de base de données ;
- la propagation de réessais automatiques ;
- l’indisponibilité de fonctions indépendantes ;
- les effets de bord sur une dépendance mutualisée.
Ils ne filtrent pas les paquets avant qu’ils n’atteignent l’application. Ils ne remplacent ni filtrage réseau, ni limitation de débit en bordure, ni protection contre les attaques volumétriques. Une connexion ouverte avant le déclenchement du circuit consomme déjà des ressources. Une requête HTTPS coûteuse peut aussi épuiser le frontal avant d’atteindre le code métier.
La défense doit donc être répartie : limitation de débit et taille maximale des requêtes au proxy, quotas par identité après authentification, contrôles de coût métier dans l’application, puis bulkheads et circuit breakers pour les dépendances internes. Les sujets de proxy, de segmentation et d’exposition réseau relèvent aussi de l’infrastructure cyber.
Les réessais constituent le piège le plus fréquent. Un délai d’expiration suivi de trois réessais à chaque niveau multiplie la charge. Trois couches qui réessaient trois fois transforment un appel initial en 27 appels potentiels. Cette arithmétique ne négocie pas, même avec un manifeste agile très convaincu.
Un seul niveau doit piloter les réessais pour une même opération. Les réessais doivent être réservés aux erreurs transitoires, protégés par un budget global, espacés par un délai aléatoire et interdits sur les opérations non idempotentes sans clé d’idempotence. Une clé d’idempotence identifie une demande de façon stable afin qu’une répétition ne crée pas deux paiements, deux comptes ou deux révocations contradictoires.
Configuration Resilience4j pour une dépendance de paiement
Section intitulée « Configuration Resilience4j pour une dépendance de paiement »Resilience4j est une bibliothèque Java qui fournit notamment des implémentations de circuit breaker et de bulkhead. L’exemple suivant cible Spring Boot avec le module resilience4j-spring-boot3, une application dont les appels sortants ont déjà des délais de connexion et de lecture définis.
resilience4j: circuitbreaker: instances: paiement: slidingWindowType: COUNT_BASED # Calcul sur les 50 derniers appels slidingWindowSize: 50 minimumNumberOfCalls: 20 # Éviter une ouverture sur trois erreurs isolées failureRateThreshold: 50 # Ouvrir à partir de 50 % d'échecs slowCallDurationThreshold: 2s # Un appel au-delà de 2 s est considéré lent slowCallRateThreshold: 50 waitDurationInOpenState: 30s # Rejet local pendant 30 secondes permittedNumberOfCallsInHalfOpenState: 5 recordExceptions: - java.net.SocketTimeoutException - java.net.ConnectException
thread-pool-bulkhead: instances: paiement: coreThreadPoolSize: 8 # Pool réservé au prestataire de paiement maxThreadPoolSize: 8 queueCapacity: 16 # File bornée, rejet au-delà keepAliveDuration: 20msCette configuration n’est valable que si le volume simultané légitime du paiement tient dans huit workers et seize requêtes en attente. Sinon, elle coupe des ventes valides. Si ce volume exige 500 workers, le problème n’est probablement pas un paramètre à gonfler : il faut revoir le protocole, la latence du fournisseur ou le découpage du traitement.
Le délai applicatif doit être inférieur au délai du proxy appelant, lui-même inférieur au délai global exposé au client. Sans cette hiérarchie, le composant amont abandonne avant que le circuit breaker ait produit son résultat. Les délais contradictoires sont une spécialité de l’infrastructure moderne, juste derrière les secrets dans des variables d’environnement en clair.
Exploitation : métriques, repli et reprise
Section intitulée « Exploitation : métriques, repli et reprise »Surveiller au minimum l’état du circuit, le taux de rejet, les appels lents, la profondeur de file, le taux de saturation des pools et les erreurs par dépendance. Ces métriques doivent être étiquetées par opération et par destination, sans placer d’identifiant personnel ou de jeton d’accès dans les labels. Une cardinalité non bornée transforme l’observabilité en attaque par déni de service comptable.
Définir le comportement de repli avant le déploiement. Les replis acceptables incluent une réponse en cache avec âge maximal déclaré, une fonctionnalité dégradée, une mise en file asynchrone ou un refus explicite. Inventer une valeur par défaut pour une autorisation, un montant, une politique d’accès ou un statut de révocation est interdit. En sécurité, un repli permissif convertit une panne en contournement.
Les états des circuits sont généralement locaux à chaque instance. Cette propriété évite une dépendance supplémentaire, mais rend la bascule non uniforme : certaines instances peuvent rejeter tandis que d’autres continuent d’appeler. Partager l’état via un magasin central n’est justifié que si l’incohérence crée un risque métier démontrable. Ajouter Redis pour synchroniser chaque seuil de panne est une excellente façon de créer une nouvelle dépendance fragile, avec bonus de complexité.
Tester régulièrement les chemins de dégradation : indisponibilité totale, latence accrue, réponses partielles, saturation de file et retour progressif du fournisseur. Les tests doivent vérifier que les opérations critiques restent isolées, que les alarmes se déclenchent et que le retour à l’état fermé ne provoque pas une ruée simultanée. Une reprise sans limitation graduelle peut relancer la saturation au moment même où le service revient.
Les paramètres de résilience sont du code de production. Ils passent par revue, versionnement, validation en intégration continue et déploiement contrôlé. La page sur Git et l’intégration continue couvre les fondations nécessaires pour traiter ces règles comme des artefacts vérifiables plutôt que comme des incantations de console.
🐝 Ce qui arrive si on s’en passe
Section intitulée « 🐝 Ce qui arrive si on s’en passe »Sans circuit breaker ni bulkhead, une dépendance qui échoue ou ralentit contamine toute l’application au lieu de rester confinée. En l’absence de coupure temporaire des appels vers un service défaillant, les requêtes s’accumulent, saturent les ressources partagées et propagent la panne de proche en proche. Sans capacités réservées par flux, un unique flux sous pression épuise les connexions ou les threads communs et entraîne les autres dans sa chute. La conséquence est une panne locale qui se transforme en indisponibilité générale, et une simple surcharge, accidentelle ou provoquée, qui suffit à faire tomber l’ensemble faute de cloisonnement. Ces patterns n’arrêtent pas une attaque à la frontière réseau, mais leur absence retire la capacité d’absorber une défaillance sans que tout le service parte en débris, laissant la disponibilité dépendre du maillon le plus fragile.
🦫 Explication de la marmotte 🍫
Section intitulée « 🦫 Explication de la marmotte 🍫 »Une application appelle souvent d’autres services pour terminer une action : vérifier une identité, calculer un prix, enregistrer un paiement ou lire une donnée. Si un service appelé devient lent ou tombe en panne, chaque demande peut attendre longtemps. Les demandes s’accumulent alors, utilisent de la mémoire, des connexions et des capacités de calcul. Une panne limitée peut rendre toute l’application indisponible.
Le circuit breaker surveille les résultats des appels vers un service précis. Quand trop d’appels échouent ou prennent trop de temps, il arrête temporairement d’envoyer de nouvelles demandes vers ce service. L’application répond alors tout de suite par une erreur prévue, une donnée enregistrée auparavant ou une fonction réduite. Après un délai, elle laisse passer quelques demandes de test. Si elles réussissent, les appels normaux reprennent. Si elles échouent, le blocage continue.
Le bulkhead sépare les ressources disponibles. Par exemple, les demandes de paiement peuvent avoir leur propre groupe de travailleurs et leur propre file d’attente. Une fonction lente, comme un export de données, ne peut alors pas prendre toutes les ressources nécessaires à la connexion ou au paiement.
Ces protections ne stoppent pas seules un grand afflux de trafic venant d’Internet. Il faut aussi limiter les demandes à l’entrée, imposer des délais, éviter les répétitions excessives et surveiller les refus. Une mauvaise configuration peut aussi bloquer des demandes valides : les seuils et les limites doivent être testés avec les charges attendues.
À lire aussi
- Principes et architecture de l'événementielConcevoir une architecture événementielle fiable : contrats, livraison, cohérence, sécurité, exploitation et documentation.
- Sécuriser une API gateway comme point de contrôleArchitecture de sécurité d'une API gateway comme point de contrôle centralisé, avec ses limites, ses attaques et ses conditions d'exploitation.
- Monolithe ou microservices : choisir sans se mentirComparaison technique des architectures monolithique et microservices, avec le coût réel du découpage, ses effets sur la sécurité, l'observabilité et l'exploitation.
- Défendre un DDoS volumétrique et applicatifDifférencier, détecter et contenir les attaques par déni de service distribué volumétriques et applicatives avec une défense en couches.
Pages voisines
La citent
- Monolithe ou microservices : choisir sans se mentir
- Défendre un DDoS volumétrique et applicatif
- Dimensionner un cluster HSM à forte charge transactionnelle
- Gérer une crise cloud multi-tenant
- Python pour l'automatisation défensive, usages et garde-fous
Circuit breaker et bulkhead contre la propagation des pannes
Elle cite
