Guide de fiabilité

Résilience des dépendances : délais d'attente, tentatives, contre-pression et solutions de repli

Un appel à une dépendance fait partie du domaine de défaillance de votre service. Limitez le temps, le travail et les tentatives qu'il peut consommer avant qu'un incident amont ne devienne votre panne.

Publié le
Mis à jour le
Temps de lecture
20 min de lecture
Sur cette page

La plupart des défaillances de production ne sont pas le crash d'un unique processus. Une base de données ralentit, la résolution DNS se bloque, une API tierce renvoie des erreurs ou une région perd de la capacité. Si les appelants attendent indéfiniment, réessaient sans limites et montent en charge vers la même dépendance contrainte, une défaillance partielle devient une panne en cascade.

La résilience des dépendances est l'ensemble des décisions explicites qui empêchent cette cascade. Elle comprend les délais d'attente, l'annulation, la politique de tentative, les limites de concurrence, les circuit breakers, les files, les solutions de repli et la télémétrie. Ces contrôles relèvent du comportement applicatif, pas d'un paramètre sidecar ou Kubernetes activable aveuglément. Ils comptent d'autant plus à l'échelle où tourne une couche d'orchestration edge managée : de nombreuses origines, de nombreux fournisseurs, de nombreuses régions, où une politique mal calibrée dans un seul service devient discrètement l'incident de tout le monde. Une plateforme qui surveille Performance, Sécurité et Visibilité sur cette surface, comme MYO chez Optimi, ne peut rapporter la vérité que si les services qu'elle observe échouent eux-mêmes de façon bornée et honnête.

Les patterns fondamentaux de la résilience des dépendances

Six patterns reviennent dans toute stratégie de résilience des dépendances, et ils se combinent plutôt que de se substituer. Les délais d'attente et échéances bornent la durée autorisée d'un appel. Les tentatives avec budget décident si et à quelle fréquence un appel idempotent en échec est répété. Les circuit breakers cessent d'appeler une dépendance déjà défaillante. Les bulkheads isolent le pool de connexions, de threads ou la file d'une dépendance afin que sa défaillance ne prive pas de ressources les appelants d'une dépendance sans rapport. La contre-pression (backpressure) et l'écrêtage de charge décident de ce qui se passe une fois une limite atteinte. Les solutions de repli décident de ce que l'appelant annonce à son propre utilisateur lorsque la dépendance reste inaccessible à temps.

Un septième pattern, le hedging, est plus étroit : pour un appel idempotent en lecture où la latence de queue compte plus que le volume brut, un client déclenche une seconde requête vers une autre réplique dès que la première dépasse sa latence attendue au 95ᵉ centile, puis retient la réponse la plus rapide et annule l'autre. Il échange une petite charge dupliquée plafonnée contre une forte réduction de la latence p99 ; il est dangereux pour tout ce qui écrit de l'état.

Commencer par une carte des dépendances et un budget de latence

Pour chaque chemin de requête, cartographiez chaque dépendance synchrone et asynchrone : DNS, périphérie ou ingress, appels de service, base de données, cache, file, fournisseur d'identité, système de paiement et API externe. Enregistrez la responsabilité, le protocole, la latence normale et de queue, le comportement d'erreur, le quota, la limite de connexions, la criticité des données et l'option de repli.

Définissez ensuite une échéance de bout en bout depuis le point d'entrée exposé aux utilisateurs. Répartissez-la entre le travail nécessaire. Un budget utilisateur de 1 000 ms ne peut pas contenir trois délais d'attente client par défaut de 500 ms en série. Réservez du temps pour la mise en file, la sérialisation et une réponse ; n'allouez pas tout le budget aux appels aval. Chez Acme Shop, un site e-commerce, le checkout-api dispose ainsi de 900 ms au total : 120 ms pour le travail applicatif et la réponse, 50 ms pour une file bornée, et le solde de 730 ms pour l'appel de paiement, dont 550 ms pour la première tentative d'autorisation.

Propagez les échéances et l'annulation à travers les appels de service. Lorsque le client est parti ou que l'échéance amont expire, poursuivre un travail coûteux consomme une capacité qui ne peut pas améliorer la réponse. Utilisez des horloges monotones pour les calculs de temps écoulé et distinguez, lorsque la bibliothèque cliente le permet, les délais d'attente de connexion, de handshake TLS, de premier octet, d'inactivité et de requête globale.

Ne réessayer que lorsque c'est sûr et abordable

Les tentatives peuvent améliorer la récupération après une défaillance transitoire, mais chaque tentative ajoute de la charge à une dépendance déjà potentiellement surchargée. Ne réessayez que les opérations idempotentes ou protégées par une clé d'idempotence. Ne réessayez jamais un paiement, une publication de message ou une mutation simplement parce que le client n'a pas reçu de réponse ; déterminez d'abord si le serveur a pu l'achever.

Utilisez un petit nombre de tentatives, un backoff exponentiel, du jitter et un temps écoulé maximal inférieur à l'échéance restante de l'appelant. Respectez Retry-After et ne réessayez jamais une simple erreur client : un code 400 ou 422 échouera à l'identique à la deuxième tentative. Établissez un budget de tentatives afin qu'un amont défaillant ne puisse pas multiplier indéfiniment votre volume de requêtes ; les recommandations SRE de Google plafonnent typiquement les essais par requête (trois avant que l'échec ne remonte à l'appelant) et, séparément, la proportion de requêtes réessayées par client, en suspendant les nouvelles tentatives dès que ce ratio franchit un seuil bas comme dix pour cent.

Évitez de réessayer à plusieurs couches. Si la périphérie, la gateway, le service mesh, le SDK et l'application réessaient chacun trois fois, le multiplicateur se compose sur les cinq couches : une requête initiale peut devenir des centaines d'appels de dépendance, pas la poignée que chaque équipe pensait avoir plafonnée isolément. Attribuez la responsabilité des tentatives à une seule couche par opération et observez-les séparément des requêtes utilisateur ; les conventions sémantiques HTTP d'OpenTelemetry définissent l'attribut http.request.resend_count pour cela. Réservez les tentatives au cas de l'échec et le hedging (voir plus haut) au cas de la latence : les combiner sans budget partagé peut compter la charge en double pendant un incident.

Un délai d'attente est un contrôle de capacité

Une attente non bornée monopolise un worker, une connexion, de la mémoire et souvent un emplacement de requête. Une échéance raisonnable protège le budget de latence et conserve de la capacité pour les requêtes qui peuvent encore réussir. Réglez-la à partir de la latence de queue mesurée et des exigences produit, pas d'une valeur par défaut copiée.

Limiter la concurrence et appliquer la contre-pression (backpressure)

Chaque client de dépendance a besoin d'un nombre maximal d'appels simultanés et d'une file bornée. Une fois la limite atteinte, choisissez une réponse délibérée : mettre le travail en file avec une échéance, renvoyer un résultat mis en cache, écarter le trafic de faible priorité ou échouer rapidement avec une erreur réessayable. Une file interne non bornée transforme un bref ralentissement amont en pression mémoire et en forte latence de queue. C'est cette contre-pression, appliquée délibérément plutôt que subie, qui distingue une dépendance qui se dégrade proprement d'une dépendance qui entraîne tout le service avec elle.

Isolez cette limite par dépendance, pas seulement par service : c'est le pattern du bulkhead (cloison étanche). Donnez au client de paiement son propre pool de connexions et sa propre file bornée, séparés de ceux de l'API de recommandations ou du cache d'inventaire. Sans cette séparation, une dépendance lente épuise un pool partagé et prive les autres dépendances de ressources — un compartiment inondé qui fait sombrer un navire que des cloisons étanches auraient maintenu à flot. Chez Acme Shop, dont la flotte de paiement culmine à 300 requêtes par seconde en vente flash, la loi de Little sur l'échéance de 550 ms donne un besoin de l'ordre de 300 × 0,55 s ≈ 165 appels simultanés à l'échelle de la flotte ; le pool est plafonné à 220, avec de la marge, et séparé de celui des tarifs de livraison.

Dimensionnez les pools de connexions pour la dépendance et la flotte, pas pour un seul conteneur : des pools par pod non divisés par le nombre de réplicas peuvent submerger une base de données ou une gateway NAT avant qu'un seul pod ne paraisse en mauvaise santé. Suivez les connexions actives et inactives, le temps d'attente d'acquisition, le travail rejeté et le nombre de réplicas. Un autoscaler horizontal qui réagit au CPU ou à une profondeur de file élevée peut ajouter de la pression s'il n'est pas coordonné avec la capacité de la dépendance.

Utilisez des files asynchrones pour le travail qui n'a pas besoin de se terminer dans la réponse utilisateur. Définissez l'âge maximal, la politique de tentative et de dead-letter, la concurrence des consommateurs et le traitement idempotent. Les files fournissent un tampon, pas une capacité infinie : un âge croissant est un signal d'impact utilisateur et exige une décision d'écrêtage de charge ou de récupération.

Circuit breakers et retries : une solution de repli commune et honnête

Les circuit breakers et les retries résolvent deux moitiés du même problème : une tentative suppose que le prochain essai pourrait réussir, tandis qu'un breaker suppose qu'il échouera et arrête d'essayer pendant un moment. Un circuit breaker s'ouvre lorsque la défaillance ou la latence franchit un seuil défini, empêchant d'autres appels pendant que la dépendance récupère. Il doit avoir un intervalle ouvert borné, un test semi-ouvert qui n'admet qu'un petit nombre de requêtes d'essai, et une condition de fermeture fondée sur leur succès plutôt que sur une simple minuterie. Délimitez les breakers par dépendance et par opération ; une API de recommandations lente ne doit pas bloquer le paiement. Gardez le breaker et la politique de tentative d'une même dépendance dans le même chemin de code : une boucle de tentatives qui continue après l'ouverture du breaker en annule l'intérêt.

Le comportement de repli doit préserver l'exactitude. De bonnes solutions de repli consistent à servir un catalogue mis en cache connu comme sûr, accepter une requête dans une file durable ou masquer un widget non essentiel. De mauvaises solutions de repli consistent à servir des données d'autorisation obsolètes, supprimer silencieusement une action financière ou renvoyer un succès pour un travail jamais enregistré.

Pour le travail qui modifie l'état, utilisez des patterns qui rendent la récupération explicite : clés d'idempotence, enregistrements transactional outbox, sagas avec actions compensatoires et jobs de réconciliation. Un circuit breaker n'améliore la disponibilité que lorsque le produit peut fournir une réponse véridique pendant l'indisponibilité de la dépendance.

Concevoir pour les réalités cloud-native et multi-région

Kubernetes redémarre les conteneurs défaillants, mais ne sait pas si réessayer une API externe est sûr ni si une base de données a atteint sa limite de connexions. Ne vérifiez jamais une dépendance aval depuis une sonde de liveness : une défaillance partagée deviendrait un redémarrage massif de tous les réplicas concernés, l'inverse de l'isolement qu'un breaker ou un bulkhead doit apporter. Une sonde de readiness peut refléter une dépendance partagée, mais seulement si son délai d'attente dépasse le pire temps de réponse connu de cette dépendance ; sinon, une dépendance lente mais en voie de récupération retire tous les réplicas de la rotation d'un coup et s'enfonce plus profondément dans le ralentissement qu'elle a causé.

Déployez des réplicas à travers les domaines de défaillance lorsque le service l'exige, et testez le comportement lorsqu'une zone, un résolveur DNS, un émetteur d'identifiants ou une dépendance régionale échoue. Le routage multi-région ajoute ses propres préoccupations : retard de réplication, propriété des écritures, résidence des données, qualité des contrôles de santé et invalidation du cache. Envoyer une requête vers une région saine n'est pas utile si cette région ne peut pas servir les données requises en sécurité.

En périphérie, définissez les délais de connexion et de réponse de l'origine, les règles de tentative et les critères de failover en fonction des sémantiques applicatives. Limitez les tentatives automatiques aux méthodes sûres ou aux requêtes explicitement idempotentes. Les réponses pouvant être mises en cache peuvent être servies plus près des utilisateurs pendant une dégradation de l'origine, tandis que les chemins personnalisés et de mutation nécessitent une réponse dégradée honnête ou une file contrôlée. Surveillez la latence par région et par route, pas seulement les moyennes globales.

La cohérence multi-fournisseurs est aussi un contrôle de résilience

Lorsqu'un chemin de requête traverse plusieurs fournisseurs edge et infrastructure — un résolveur DNS, un CDN, un WAF, un cloud d'origine — le délai d'attente et le comportement de failover par défaut de chacun sont raisonnables isolément, mais non coordonnés entre eux. Orchestrer ces fournisseurs vers une politique unique et observer le résultat via une seule couche de visibilité comme MYO referme l'écart entre « chaque fournisseur est en bonne santé » et « la requête s'est réellement terminée correctement », sans demander à aucun des fournisseurs, tous best-of-breed, de changer sa façon de travailler.

Instrumenter toute la chaîne d'appel

Utilisez OpenTelemetry pour créer des spans pour les appels de dépendance et propager le contexte de trace à travers les frontières HTTP, gRPC et de messagerie. Enregistrez le nom de l'opération, le système cible, le statut, la durée, le nombre de tentatives, la raison du délai d'attente et des attributs de route sûrs. Ne placez pas de tokens d'accès, de valeurs de requête complètes ni de données personnelles dans les attributs des spans.

Combinez les traces avec des métriques : taux de requêtes des dépendances, disponibilité, durée p50/p95/p99, nombre de délais d'attente, nombre de tentatives, état du circuit, âge de file, temps d'attente du pool et saturation. Corrélez-les avec les déploiements, les changements de configuration et les événements de routage en périphérie. L'objectif est de déterminer si les utilisateurs attendent le code applicatif, une dépendance partagée ou le chemin réseau.

Exercer la défaillance avant qu'elle ne vous exerce

Exécutez des tests contrôlés dans un environnement sûr. Ajoutez de la latence, renvoyez des réponses 429 et 503, blackholez une route, épuisez un pool de connexions, retardez un consommateur de file et faites tourner un identifiant de dépendance. Confirmez que les échéances bornent la requête, que les tentatives n'augmentent pas fortement, que les solutions de repli sont véridiques, que les alertes se déclenchent sur l'impact utilisateur et que la récupération ne nécessite pas de réparation manuelle du travail dupliqué. Chez Acme Shop, cela revient à retarder le stub de paiement de 700 ms et à lui faire renvoyer du 503 sur une petite cohorte, puis à vérifier que le checkout se termine ou signale une attente avant 900 ms, que la concurrence de paiement reste plafonnée et qu'aucune autorisation dupliquée n'apparaît pour une même clé d'idempotence.

Quelques symptômes reviennent souvent : une latence p99 qui dépasse le budget trahit une échéance absente ou mal propagée ; un débit qui explose côté fournisseur pendant une vague de 503 signale des tentatives dupliquées à plusieurs couches ; un succès affiché sans paiement enregistré révèle une solution de repli malhonnête ; des workers tous bloqués sur le paiement pointent vers un bulkhead partagé ; et des pods qui redémarrent en cascade pendant un ralentissement fournisseur trahissent une sonde de liveness qui interroge la dépendance au lieu d'une sonde de readiness correctement calibrée.

Documentez la décision pour chaque dépendance critique : responsable, SLO, délai d'attente, règle de tentative, limite de concurrence, solution de repli, risque de perte de données et chemin d'escalade. Réexaminez-la à chaque modification de la dépendance, du profil de trafic ou de l'architecture régionale.

Références faisant autorité

Transformez la résilience des dépendances en politique cohérente à l'échelle de la flotte

L'orchestration Performance, Sécurité et Visibilité d'Optimi — observée de bout en bout via MYO — aide à garder une politique de délais d'attente, de tentatives, de circuit breakers et de contre-pression (backpressure) cohérente sur tous les fournisseurs du chemin, pas seulement à l'intérieur d'un service.

Discuter d'une livraison résiliente