Microservices Défensifs : Circuit Breakers, Bulkheads et Fallbacks en TypeScript
Empêcher les pannes en cascade dans les architectures distribuées grâce aux disjoncteurs applicatifs.
1. Pannes en cascade et saturation des ressources
Le mode de panne le plus dangereux d’un système distribué n’est pas une dépendance qui tombe proprement — cela échoue vite et se gère facilement — c’est une dépendance qui devient lente. Quand un service en aval se met à répondre en 20 secondes au lieu de 200 millisecondes, chaque appelant qui garde une connexion ouverte en attendant s’accumule : dans un service Node.js, cela signifie un nombre croissant de promesses en vol et de sockets ouverts ; dans un runtime à un thread par requête, cela signifie un épuisement littéral du pool de threads. Dans les deux cas, la capacité même du service appelant à traiter n’importe quelle requête — y compris celles qui n’ont rien à voir avec la dépendance lente — se dégrade au même rythme.
C’est ainsi qu’une simple requête base de données lente dans un microservice devient une panne totale trois sauts en amont : le service C appelle une base de données en difficulté, le service B appelle C et sa file de requêtes s’accumule en attendant C, le service A appelle B et voilà que les health checks d’A eux-mêmes commencent à échouer parce que sa boucle d’événements ou son pool de threads est saturé de requêtes bloquées derrière B. Aucune des autres dépendances d’A n’est en mauvaise santé, mais A lui-même devient indisponible, et la panne a désormais franchi une frontière qui n’avait rien à voir avec l’incident d’origine.
Les timeouts seuls ne résolvent pas totalement ce problème. Un timeout borne le temps d’attente d’une requête donnée, mais si les requêtes continuent d’arriver plus vite que les timeouts n’expirent, vous accumulez quand même un nombre non borné de requêtes en vol contre la dépendance en difficulté pendant cette fenêtre. Ce qui arrête réellement l’hémorragie, c’est de refuser d’envoyer de nouvelles requêtes à une dépendance manifestement défaillante, immédiatement et à faible coût, plutôt que de redécouvrir sa défaillance à chaque appel individuel.
2. Fonctionnement du Circuit Breaker
Un circuit breaker enveloppe les appels à une dépendance dans une machine à états à trois états. CLOSED est l’état normal : les requêtes passent, et le breaker suit une fenêtre glissante de succès et d’échecs. OPEN est l’état déclenché : les requêtes sont rejetées immédiatement, sans même tenter l’appel, dès que le taux d’échec dans la fenêtre glissante dépasse un seuil configuré. HALF_OPEN est l’état de sonde de récupération : après une période de refroidissement, le breaker laisse passer un petit nombre de requêtes d’essai pour tester si la dépendance s’est rétablie, sans engager tout le trafic tout de suite.
La logique de transition compte plus que les états eux-mêmes. Le passage de CLOSED à OPEN se déclenche quand le taux d’échec (pas le compte brut, pour rester significatif sous trafic variable) dépasse un seuil — 50% d’échecs sur une fenêtre glissante des 20 dernières requêtes est un point de départ courant — avec un volume minimal de requêtes exigé pour qu’une brève rafale de 2 échecs sur 2 requêtes totales ne déclenche pas le breaker sur une donnée statistiquement insignifiante. Le passage d’OPEN à HALF_OPEN est purement temporel : après un refroidissement fixe (souvent 30-60 secondes), le breaker autorise la reprise du sondage. Le passage de HALF_OPEN à CLOSED exige une série de succès consécutifs sur les requêtes de sonde ; HALF_OPEN vers OPEN se produit dès un seul échec de sonde, renvoyant le breaker vers un rejet total plutôt que de risquer une inondation contre une dépendance qui ne s’est en réalité pas rétablie.

