System Design

Concevoir la résilience dans les microservices

Les systèmes distribués modernes sont des toiles complexes de services interconnectés. Lorsqu'un composant tombe en panne, cette défaillance peut se propager et entraîner la chute de l'ensemble du système. La mise en œuvre de modèles de résilience ne consiste pas seulement à gérer les erreurs ; il s'agit de concevoir des systèmes capables de résister au chaos. Ce guide couvre trois piliers essentiels : les disjoncteurs, les nouvelles tentatives intelligentes et la dégradation gracieuse.

Comprendre le modèle de disjoncteur

Le modèle de disjoncteur s'inspire de l'ingénierie électrique. Lorsqu'un service échoue de manière répétée, le disjoncteur « déclenche » et cesse d'envoyer des requêtes à ce service pendant une période définie. Cela empêche l'appelant d'être submergé par des dépassements de délai et donne au service aval le temps de se rétablir.

Il existe trois états dans un disjoncteur :

  • Fermé : Fonctionnement normal. Les requêtes passent. Les échecs sont comptés.
  • Ouvert : Le circuit a déclenché. Toutes les requêtes échouent rapidement sans appeler le service aval.
  • À demi ouvert : Après un dépassement de délai, le circuit autorise un nombre limité de requêtes pour tester si le service s'est rétabli.

Voici une implémentation conceptuelle en Python utilisant la bibliothèque tenacity pour les nouvelles tentatives et une machine à états simple pour le disjoncteur :


import time
from enum import Enum

class CircuitState(Enum):
    CLOSED = "closed"
    OPEN = "open"
    HALF_OPEN = "half_open"

class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=30):
        self.failure_count = 0
        self.state = CircuitState.CLOSED
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.last_failure_time = None

    def call(self, func, *args, **kwargs):
        if self.state == CircuitState.OPEN:
            if time.time() - self.last_failure_time > self.recovery_timeout:
                self.state = CircuitState.HALF_OPEN
            else:
                raise Exception("Circuit is Open")

        try:
            result = func(*args, **kwargs)
            if self.state == CircuitState.HALF_OPEN:
                self.state = CircuitState.CLOSED
                self.failure_count = 0
            return result
        except Exception as e:
            self.record_failure()
            raise e

    def record_failure(self):
        self.failure_count += 1
        self.last_failure_time = time.time()
        if self.failure_count >= self.failure_threshold:
            self.state = CircuitState.OPEN

Nouvelles tentatives stratégiques avec retour exponentiel

Les nouvelles tentatives sont essentielles pour les erreurs transitoires, telles que les interruptions réseau ou les surcharges temporaires. Cependant, réessayer immédiatement peut aggraver le problème en envoyant plus de trafic à un service en difficulté. Utilisez toujours un retour exponentiel avec de l'aléa (jitter).

L'aléa (jitter) randomise le délai pour éviter un « troupeau de tonnerre » de nouvelles tentatives frappant le service exactement au même moment.


import random

def retry_with_backoff(func, max_retries=3, base_delay=1):
    for attempt in range(max_retries):
        try:
            return func()
        except Exception as e:
            if attempt == max_retries - 1:
                raise e
            delay = (2 ** attempt) + random.uniform(0, 1)
            time.sleep(delay)

Dégradation gracieuse : des solutions de repli qui comptent

Lorsqu'un service principal est indisponible, le système doit se dégrader gracieusement plutôt que d'échouer complètement. Cela implique de définir un comportement de repli qui offre une expérience réduite mais fonctionnelle.

Les exemples de dégradation gracieuse incluent :

  • Retourner des données en cache avec un indicateur d'obsolescence.
  • Fournir un contenu par défaut ou statique.
  • Désactiver les fonctionnalités non critiques tout en maintenant l'intégrité des fonctionnalités principales.

Par exemple, si un moteur de recommandation tombe en panne, un site de commerce électronique pourrait revenir à l'affichage d'« Articles populaires » depuis un cache local au lieu d'afficher une page d'erreur.

Conclusion

La résilience est une discipline de conception, et non une après-pensée. En combinant des disjoncteurs pour stopper les défaillances en cascade, des nouvelles tentatives intelligentes pour gérer les problèmes transitoires et une dégradation gracieuse pour maintenir la confiance des utilisateurs, vous construisez des systèmes qui ne sont pas seulement robustes, mais résilients. Commencez par instrumenter vos services, identifiez vos chemins critiques et mettez en œuvre ces modèles là où ils comptent le plus.

Share: