Patterns de résilience : circuit breaker, retry, timeout

Par KamangaMar 2, 20267 mins de lecture

Patterns de résilience : circuit breaker, retry, timeout

J'accompagnais un client dans le secteur du retail en ligne (25 développeurs). En six mois, trois incidents P1, à chaque fois pendant des pics de trafic. Le service de recommandations devenait lent, les threads du service produit s'accumulaient en attente, le pool de connexions s'épuisait, et le service produit tombait à son tour. La homepage devenait indisponible.

Le service de recommandations avait une excuse valable : les pics de charge. Le service produit n'en avait pas. Il n'avait simplement aucun mécanisme pour se protéger d'un voisin défaillant.

Deux heures de développement pour ajouter un timeout. Deux jours pour le circuit breaker. Zéro incident P1 lié aux recommandations depuis lors. Les recommandations continuent de tomber lors des pics, mais ça reste isolé.

C'est ce que les patterns de résilience font : ils n'éliminent pas les pannes, ils empêchent qu'une panne locale devienne un effondrement global.


La cascade failure : pourquoi les systèmes distribués s'effondrent en bloc

Sans mécanisme de protection, les appels vers des services externes dans un système distribué ont une propriété dangereuse : si le service cible ralentit, les appelants attendent, leurs ressources se consomment, et ils deviennent eux-mêmes défaillants. Ce phénomène est aggravé par le couplage temporel entre services, qui crée des dépendances implicites d'ordre ou de timing difficiles à détecter jusqu'en production.

Michael Nygard a documenté ce pattern en détail dans "Release It!" (2007), l'ouvrage de référence sur la résilience des systèmes en production. Sa conclusion : la stabilité ne se construit pas par l'optimisme (espérer que les services tiers restent disponibles), mais par la conception défensive (prévoir leur défaillance et la gérer).

Selon Gartner, une heure de downtime coûte en moyenne 300 000€ pour une application business critique. Les 3 patterns décrits ici sont des assurances contre ce coût.


Pattern 1 : Timeout

Le timeout est le premier niveau de protection, et le plus souvent absent. Un appel vers un service externe sans timeout peut attendre indéfiniment, bloquant les ressources de l'appelant.

Principe : tout appel vers un service externe (HTTP, base de données, cache, message broker) doit avoir un timeout explicite défini dans le code. Si le service ne répond pas dans ce délai, l'appel est considéré comme échoué et les ressources sont libérées.

Comment définir la valeur du timeout :

  • Mesurer le p99 (99ème percentile) de la latence du service cible sur les 30 derniers jours
  • Multiplier par 2 à 3 pour le timeout
  • Ne jamais utiliser la valeur par défaut du client HTTP (souvent 30 secondes à 2 minutes, beaucoup trop long)
// Exemple avec OkHttp (Java)
OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(2, TimeUnit.SECONDS)  // temps max pour établir la connexion
    .readTimeout(5, TimeUnit.SECONDS)     // temps max pour recevoir la réponse
    .writeTimeout(5, TimeUnit.SECONDS)    // temps max pour envoyer la requête
    .build();

À quelle couche définir le timeout : aussi près que possible de l'appel externe. Un timeout au niveau de l'API gateway seul ne protège pas les services internes.


Pattern 2 : Retry avec backoff exponentiel

Un service peut échouer de façon transitoire : surcharge momentanée, redémarrage d'instance, pic de latence. Un retry permet de retenter l'opération après un délai, sans inonder le service défaillant de requêtes supplémentaires.

Principe du backoff exponentiel : le délai entre les tentatives augmente exponentiellement. Avec un jitter (variation aléatoire) pour éviter que tous les appelants retentent exactement au même moment, ce qui recréerait la surcharge qu'on cherche à éviter.

import time
import random

def call_with_retry(func, max_retries=3, base_delay=0.1):
    for attempt in range(max_retries):
        try:
            return func()
        except TransientError as e:
            if attempt == max_retries - 1:
                raise  # dernière tentative — propager l'erreur
            delay = base_delay * (2 ** attempt) + random.uniform(0, 0.1)  # jitter
            time.sleep(delay)

Quand ne pas retenter :

  • Erreurs non-transientes (404, 401, 403, validation error) : retenter ne changera rien
  • Opérations non-idempotentes : retenter peut dupliquer l'effet (créer deux commandes au lieu d'une)

Votre système distribué tombe en cascade lors des incidents et vous ne savez pas par où commencer pour le renforcer ?

L'implémentation des patterns de résilience nécessite un audit des points de défaillance critiques et un plan de priorisation. Je l'ai fait pour des équipes dans la finance, l'assurance et le e-commerce. En 30 minutes, on peut identifier les 3 flux les plus à risque dans votre architecture et définir le plan d'implémentation.


Pattern 3 : Circuit Breaker

Le circuit breaker est le pattern le plus important pour prévenir les cascade failures. Son principe est emprunté à l'électricité : quand un circuit est en surcharge, le disjoncteur coupe le courant pour protéger le reste du circuit.

Les 3 états :

Closed (normal) : les appels passent normalement. Le circuit breaker monitore le taux d'échec.

Open : le taux d'échec a dépassé le seuil (par exemple, plus de 50% d'échecs sur les 10 derniers appels). Les appels échouent immédiatement sans atteindre le service défaillant. Cela protège le service défaillant de la surcharge et protège l'appelant d'une accumulation de threads.

Half-Open : après un délai (30 secondes par exemple), le circuit breaker laisse passer quelques appels de test. Si ils réussissent, retour à Closed. Sinon, retour à Open.

// Exemple avec Resilience4j (Java)
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)
    .waitDurationInOpenState(Duration.ofSeconds(30))
    .slidingWindowSize(10)
    .build();

CircuitBreaker circuitBreaker = CircuitBreaker.of("paymentService", config);

Supplier<Payment> decoratedCall = CircuitBreaker
    .decorateSupplier(circuitBreaker, () -> paymentService.processPayment(request));
// Exemple avec opossum (Node.js)
const CircuitBreaker = require('opossum');

const breaker = new CircuitBreaker(paymentService.processPayment, {
    timeout: 3000,
    errorThresholdPercentage: 50,
    resetTimeout: 30000
});

breaker.fallback(() => ({ status: 'payment_service_unavailable' }));

Les compléments : bulkhead et fallback

Bulkhead : isoler les ressources par service externe. Chaque service externe a son propre pool de threads ou de connexions : une dégradation d'un service ne consomme pas toutes les ressources de l'appelant.

Fallback : définir un comportement de repli quand le circuit est ouvert. Pas "retourner une erreur 500", mais retourner un résultat dégradé acceptable.

  • Service de recommandations indisponible → retourner des recommandations statiques ou populaires
  • Service de notation indisponible → afficher la note moyenne sans recalcul temps-réel
  • Service de paiement indisponible → mettre la commande en queue pour traitement différé

Votre système doit fonctionner de façon dégradée quand un service non-critique est indisponible. Il ne doit pas être entièrement indisponible.


Ordre d'implémentation

  1. Timeout d'abord (1 journée) : ajouter des timeouts explicites sur tous les appels vers des services externes. C'est la correction la plus rapide et la plus impactante.
  2. Retry sur les erreurs transientes (2 à 3 jours) : identifier les erreurs transientes dans votre système et ajouter un retry avec backoff exponentiel. S'assurer que les opérations retriées sont idempotentes.
  3. Circuit breaker sur les services critiques (1 semaine) : identifier les 5 à 10 dépendances externes les plus critiques et implémenter le circuit breaker avec monitoring.

En moins de 2 semaines, votre système est significativement plus résilient.


FAQ sur les patterns de résilience

1. Les patterns de résilience s'appliquent-ils aux monolithes ou seulement aux microservices ?

Aux deux. Tout système qui appelle des services externes (base de données, cache, API tierce, service d'email) bénéficie des timeouts, retries, et circuit breakers. Dans un monolithe, les patterns s'appliquent sur les appels vers les dépendances externes. Dans les microservices avec une base de données par service, ils s'appliquent aussi aux appels inter-services, et sont même indispensables pour gérer la cohérence éventuelle entre services. J'ai implémenté ces patterns dans des monolithes bancaires aussi bien que dans des architectures microservices de e-commerce.

2. Quel outil choisir pour implémenter les patterns de résilience ?

Par écosystème : Java/Kotlin → Resilience4j (successeur de Hystrix, maintenu activement). Node.js → opossum. Python → tenacity pour le retry. Go → gobreaker. Spring Boot → intégration native de Resilience4j via Spring Cloud Circuit Breaker. À l'infrastructure : Istio et Envoy permettent d'implémenter ces patterns au niveau du service mesh, sans modification du code applicatif, utile pour les équipes qui gèrent de nombreux services.

3. Comment monitorer l'état des circuit breakers en production ?

Les circuit breakers ouverts sont des alertes : ils signalent qu'un service est en difficulté. Exposer l'état des circuit breakers dans votre système de monitoring (Prometheus + Grafana, Datadog, New Relic). Configurer une alerte quand un circuit breaker passe à l'état Open : c'est un signal d'action, pas seulement d'information. Un circuit breaker qui reste ouvert en permanence indique un service en difficulté structurelle. Ces métriques de disponibilité font partie des indicateurs clés à suivre pour les équipes engineering : elles objectivent l'état de santé de l'architecture et rendent visibles les progrès réalisés.

4. Comment tester les patterns de résilience sans attendre un incident en production ?

Le chaos engineering est la méthode de référence. Outils : Chaos Monkey (Netflix), Gremlin, ou des scripts simples qui introduisent des latences artificielles en staging. Pour commencer sans outil spécifique : injecter un sleep(5000) dans le service cible en environnement de test et vérifier que les timeouts et circuit breakers fonctionnent comme attendu. Ce test prend 30 minutes et devrait être dans votre suite de tests de non-régression.


Ressource gratuite : Engineering Maturity Self-Assessment

L'Engineering Maturity Self-Assessment couvre le domaine Résilience & Architecture : évaluez en 10 minutes votre niveau sur les patterns de résilience, la robustesse de vos services, et la gestion des pannes. Obtenez un score et un plan d'action personnalisé.


Ecris par Kamanga

Expert IT avec 25 ans d'expérience en développement logiciel, diplômé EPITECH et MBA. Spécialisé en software craftsmanship, gestion du changement, stratégie, direction des systèmes d'information, coaching et certifié en agilité.