Patrons de résilience
La page Monolithe, modulaire, microservices énumère ce que le réseau facture : un appel qui peut expirer alors que le travail a été fait, une disponibilité qui se multiplie le long d’une chaîne, des pannes partielles permanentes. Voici ce qu’on paie avec.
Ces patrons forment le pendant du catalogue du GoF à l’échelle distribuée. Ils ont été catalogués par Michael Nygard dans Release It! (2007), sous le nom de stability patterns, et ils partent d’un renversement de perspective :
Dans un système distribué, la panne partielle n’est pas un incident : c’est l’état normal. La question n’est pas comment l’éviter, mais comment échouer sans propager.
La panne se propage par les ressources, pas par les erreurs
C’est l’idée centrale, et elle est contre-intuitive. Une erreur est inoffensive : elle revient immédiatement, l’appelant la traite, la ressource est libérée. Ce qui tue un système, c’est un appel qui ne revient pas.
C’est la défaillance en cascade, et elle explique deux choses. D’abord pourquoi un service qui ralentit est plus dangereux qu’un service qui tombe : un service tombé renvoie une erreur immédiate, un service lent immobilise. Ensuite pourquoi le délai d’attente est le patron numéro un, avant tous les autres — sans lui, aucun des suivants ne sert à rien.
Les anti-patrons
Nygard les catalogue avant les remèdes, parce qu’on les reconnaît dans son propre code.
| Anti-patron | Ce qui se passe |
|---|---|
| Appel sans délai d’attente | Le défaut d’un client HTTP est souvent « infini ». Une seule dépendance lente suffit à figer l’application |
| Reprise immédiate | Trois tentatives sans attente triplent la charge sur un service déjà en difficulté |
| Tempête de reprises | Tous les clients réessayent à la même seconde, et achèvent le service au moment où il redémarre |
| File non bornée | La file absorbe la surcharge, la latence explose, la mémoire part — l’échec est différé, pas évité |
| Ressource partagée | Un seul pool de connexions pour toutes les dépendances : la plus lente les prive toutes |
| Chaîne synchrone | Cinq appels en série multiplient les indisponibilités et additionnent les latences |
| Client trop confiant | Aucune gestion d’échec côté appelant : la panne du voisin devient la sienne |
Les patrons, dans l’ordre où il faut les appliquer
L’ordre compte plus que la liste : chaque patron suppose les précédents.
| Patron | Empêche | Coût | |
|---|---|---|---|
| 1 | Délai d’attente | L’immobilisation des ressources | Presque nul |
| 2 | Idempotence | Les doubles effets lors des reprises | Une clé et une table |
| 3 | Reprise avec recul et gigue | L’échec sur incident transitoire | Charge supplémentaire à surveiller |
| 4 | Cloisonnement | Qu’une dépendance en prive les autres | Des pools séparés à dimensionner |
| 5 | Disjoncteur | L’acharnement sur un service mort | Réglage délicat, faux positifs |
| 6 | Dégradation gracieuse | La panne totale sur une fonction secondaire | Un mode dégradé à concevoir et tester |
| 7 | Délestage et limitation de débit | L’écroulement sous la surcharge | Des refus assumés |
1. Le délai d’attente
Tout appel distant en a un. Sans exception, et explicitement : les valeurs par défaut des clients HTTP sont fréquemment infinies ou de plusieurs minutes, ce qui revient au même.
La règle moins connue est celle du budget : le délai doit décroître le long de la chaîne. Si la passerelle accorde 3 secondes, le service appelé doit accorder moins à son propre appel, sinon il travaille encore alors que son appelant a déjà abandonné — le travail est perdu et les ressources restent occupées des deux côtés.
2. L’idempotence
Prérequis absolu de la reprise : réessayer une opération non idempotente, c’est choisir de facturer deux fois. Le mécanisme est toujours le même — une clé fournie par l’appelant, et une table des clés déjà traitées.
Le sujet est développé côté publication d’événements dans Événements de domaine ; c’est la même mécanique.
3. La reprise avec recul exponentiel et gigue
Trois conditions, toutes nécessaires :
- Uniquement sur les erreurs reprenables — une expiration, une erreur 503, une connexion refusée. Jamais sur une 400 ou une 404 : réessayer une requête invalide ne la rendra pas valide.
- Un nombre borné de tentatives, deux ou trois. Au-delà, on n’aide plus personne.
- Un recul exponentiel avec gigue : attendre 100 ms, 200 ms, 400 ms — plus un aléa. La gigue est la partie que l’on oublie, et c’est elle qui évite la tempête : sans elle, mille clients qui reculent de la même façon réessayent tous à la même milliseconde.
Warning
La reprise est le patron le plus dangereux du catalogue, parce qu’il amplifie la charge exactement quand le système en a le moins besoin. Trois tentatives sur trois niveaux de la chaîne font vingt-sept appels pour une requête utilisateur. Si un seul patron devait être retiré de cette liste sur un système en difficulté, c’est celui-là.
4. Le cloisonnement
Un pool de ressources par dépendance, au lieu d’un pool global. Le nom vient des compartiments étanches d’un navire : une voie d’eau noie un compartiment, pas le bateau.
Concrètement : dix connexions réservées au service de paiement, dix au service de recommandation. Le service de recommandation devient lent ? Ses dix connexions se remplissent, et le paiement continue de fonctionner. Sans cloisonnement, la recommandation — fonction secondaire — aurait emporté l’encaissement.
5. Le disjoncteur
Trois états, et sa valeur tient à un seul mot : échouer vite.
stateDiagram-v2
[*] --> Ferme
Ferme --> Ouvert : seuil d'échecs atteint
Ouvert --> SemiOuvert : après le délai de repos
SemiOuvert --> Ferme : l'appel d'essai réussit
SemiOuvert --> Ouvert : l'appel d'essai échoue
Ferme : Fermé — les appels passent
Ouvert : Ouvert — échec immédiat, aucun appel
SemiOuvert : Semi-ouvert — un seul appel d'essaiQuand le disjoncteur est ouvert, l’appel échoue immédiatement au lieu d’attendre le délai d’attente. Le gain n’est pas de protéger le service en panne — il l’est accessoirement — mais de libérer l’appelant : ses ressources ne sont plus immobilisées à attendre quelque chose dont on sait qu’il ne répondra pas.
D’où une conséquence à retenir : un disjoncteur sans délai d’attente ne sert à rien, puisqu’il n’y a alors rien à raccourcir.
6. La dégradation gracieuse
Répondre partiellement plutôt que pas du tout : la page produit s’affiche sans le bloc de recommandations, le tarif utilise la dernière grille connue, la recherche retombe sur un tri par date.
Warning
Le repli silencieux est un piège classique. Renvoyer une liste vide, un zéro ou une valeur en cache sans que l’appelant sache qu’il s’agit d’un repli est parfois pire qu’une erreur : « aucune allergie connue » et « je n’ai pas pu vérifier les allergies » ne sont pas la même information. Un repli doit être signalé dans la réponse, et le choix de dégrader ou d’échouer est une décision métier, pas technique.
7. Le délestage et la limitation de débit
Refuser du travail est une fonctionnalité. Au-delà de sa capacité, un système qui accepte tout sert tout le monde mal ; un système qui refuse 20 % des requêtes sert correctement les 80 % restants.
La limitation de débit protège d’un client trop gourmand ; le délestage protège de la charge globale, en rejetant tôt et à bas coût. Les deux supposent d’avoir décidé à l’avance quoi sacrifier — et une file bornée avec contre-pression, plutôt qu’une file infinie qui transforme un problème de débit en problème de mémoire.
En code
Délai d’attente, reprise bornée, recul exponentiel et gigue — le socle minimal.
Trois détails portent tout le bénéfice : le filtre reprenable, sans lequel on
s’acharne sur des requêtes invalides ; la borne sur le nombre de tentatives ; et la
gigue, la ligne qu’on supprime en premier quand on simplifie et qui est
précisément celle qui empêche la tempête de reprises.
Où placer tout cela
Ces patrons appartiennent à l’adaptateur sortant de l’hexagone, jamais au domaine. Un disjoncteur dans une règle métier est une fuite d’infrastructure ; le domaine voit uniquement un port qui réussit ou échoue.
Une partie du travail peut aussi être déportée hors du code applicatif : un maillage de services ou une passerelle gèrent délais, reprises et disjoncteurs pour tous les appels sortants. C’est séduisant et cela a une limite nette — l’infrastructure ne sait pas ce qui est idempotent, ni ce qui est dégradable. Le repli et l’idempotence restent des décisions applicatives.
Côté exploitation, les sondes de vivacité et de disponibilité participent du même sujet, et sont traitées dans Kubernetes — sondes : une instance qui se déclare indisponible pendant qu’elle se rétablit est une forme de délestage.
Ce que ça coûte
Le débogage devient plus difficile. Une reprise masque l’erreur d’origine ; un disjoncteur ouvert produit des échecs dont la cause est ailleurs et plus tôt. Sans traçabilité corrélée, on cherche longtemps.
Les faux positifs. Un disjoncteur mal réglé s’ouvre sur une dépendance lente mais saine, et provoque la panne qu’il devait éviter. Les seuils demandent des mesures réelles, pas des valeurs recopiées.
Des modes dégradés à tester. Un repli jamais exercé ne fonctionne pas le jour où il sert. C’est du code qui ne tourne qu’en cas de panne — donc jamais, sauf si on le provoque.
Du réglage permanent. Délais, seuils, tailles de pool évoluent avec le trafic et les dépendances. Ce n’est pas une configuration qu’on écrit une fois.
Vérifier que ça fonctionne
Un patron de résilience non éprouvé est une hypothèse. Trois pratiques, par ordre d’accessibilité :
- L’injection de fautes en test : un adaptateur qui expire, renvoie 503, ou répond avec trois secondes de retard. C’est le plus rentable, et cela tient dans les tests existants.
- Les journées de panne : provoquer une défaillance en préproduction, en équipe, et observer. On y découvre systématiquement un délai d’attente manquant.
- L’ingénierie du chaos en production, avec un périmètre restreint et un moyen d’arrêt. À réserver aux systèmes qui ont déjà les deux points précédents.
Quand ne pas s’en occuper
Toute cette page est un coût de la distribution. Un monolithe avec une seule base de données n’a besoin ni de disjoncteur, ni de cloisonnement, ni de reprise : il n’y a pas de panne partielle à absorber.
Deux choses restent utiles même dans ce cas, et devraient être en place partout : un délai d’attente sur chaque appel sortant — y compris vers la base et vers le moindre service tiers — et un pool de connexions borné. Le reste attend le premier appel réseau vers un service dont vous n’êtes pas propriétaire.
Références
- Michael Nygard, Release It!, 2ᵉ édition, 2018 — la référence, et l’origine des stability patterns
- https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/ — le meilleur texte court sur le trio délai / reprise / gigue
- https://sre.google/sre-book/handling-overload/ — délestage et surcharge, par les équipes SRE de Google
- https://martinfowler.com/bliki/CircuitBreaker.html
- https://resilience4j.readme.io/ — l’implémentation de référence, utile même hors JVM pour son vocabulaire