🤖 Computer Science
Exponential Backoff et Jitter : Un pattern essentiel pour stabiliser vos interactions avec les services tiers
date
Jan 20, 2024
slug
exponential-backoff-jitter-eviter-surcharge-services-retries
author
status
Public
tags
Optimisation
I/O Bound
Network
summary
Lorsque l’on bâtit des services critiques comme des plateformes de paiement, l’envoi d’e-mails ou encore des API de notifications, une question essentielle se pose immédiatement : comment gérer les échecs ?
type
Post
category
🤖 Computer Science
updatedAt
Dec 7, 2024 11:10 PM
Lorsque l’on bâtit des services critiques comme des plateformes de paiement, l’envoi d’e-mails ou encore des API de notifications, une question essentielle se pose immédiatement : comment gérer les échecs ? Les services externes ne sont pas toujours fiables, et l’infrastructure elle-même peut rencontrer des ralentissements. Alors, comment garantir que tout continue à fonctionner sans que des pannes ponctuelles ne perturbent l’expérience utilisateur ?
Dès le départ, nous avons décidé de concevoir une stratégie pour gérer les appels externes et les I/O sensibles. Cela incluait non seulement des mécanismes de retry, mais aussi des optimisations avancées comme l’Exponential Backoff et le Jitter.
Pourquoi anticiper est crucial
Dans le développement d'applications modernes, la gestion des appels vers des ressources externes est cruciale. Qu'il s'agisse d'APIs tierces, de bases de données distantes ou de services cloud, ces interactions sont intrinsèquement sujettes à des échecs temporaires : latence réseau, indisponibilité momentanée du service, ou erreurs transitoires. C'est pourquoi la stratégie de retry est devenue un pattern fondamental de la résilience applicative.
Les frameworks et bibliothèques populaires comme Axios, Fetch API, ou les SDKs intègrent nativement des mécanismes de retry, tentant plusieurs fois d'accéder à la ressource avant d'abandonner définitivement avec un timeout.
Cette approche permet d'absorber les perturbations ponctuelles et d'améliorer significativement la fiabilité des applications, transformant ce qui aurait pu être une erreur utilisateur en une simple latence transparente.
Les services externes comme les passerelles de paiement ou les API e-mail peuvent rencontrer des surcharges, des latences ou même des indisponibilités temporaires. Sans une gestion proactive, un simple délai peut vite dégénérer :
- Les systèmes saturent en multipliant les tentatives de connexion. 🚫
- Les utilisateurs subissent des temps d’attente prolongés ou des erreurs répétées. 🚫
- L’effet boule de neige peut affecter d’autres parties de l’application. 🚫
Plutôt que de réagir après coup, nous pouvons choisir de considérer ces échecs comme une donnée normale du système. La question n’est pas de savoir si des erreurs vont survenir, mais comment y répondre efficacement.
Imaginez une situation où votre application, face à une erreur, réessaie immédiatement et continuellement d'accéder à un service déjà surchargé. C'est comme si, trouvant un ascenseur en panne, tout le monde appuyait frénétiquement sur le bouton d'appel en même temps ! Cette approche de "retry simple" peut en réalité aggraver le problème.
Lorsqu'un service est temporairement indisponible, des centaines ou des milliers de clients qui retentent immédiatement leurs requêtes créent un effet d'avalanche, submergeant davantage le service et prolongeant sa période de récupération.
👉🏼 Un simple mécanisme de retry n’est donc pas suffisant…
C'est là qu'interviennent l'Exponential Backoff et le Jitter : le Backoff fait patienter de plus en plus longtemps entre chaque tentative (comme attendre 2 secondes, puis 4, puis 8...), tandis que le Jitter ajoute un délai aléatoire pour éviter que toutes les requêtes n'arrivent exactement au même moment.
C'est comme si, au lieu de tous appuyer sur le bouton d'ascenseur en même temps, les gens attendaient des intervalles différents, donnant au système le temps de respirer et de se rétablir.
Exponential Backoff et Jitter
Nous savons donc qu’un simple mécanisme de retry ne suffirait pas. Réessayer trop rapidement ou trop souvent peut facilement aggraver la situation. Pour éviter cela, nous devons implémenté deux concepts dès la conception de nos systèmes :
- Exponential Backoff : Espacer intelligemment les tentatives
Plutôt que de réessayer immédiatement après un échec, nous augmentons progressivement le délai entre chaque tentative. Par exemple, après un premier échec, nous attendons 1 seconde, puis 2 secondes, puis 4 secondes, et ainsi de suite. Cela laisse le temps au service externe de se stabiliser et évite de surcharger inutilement notre infrastructure.
- Jitter : Casser la synchronisation des tentatives
Sans une variation aléatoire (le Jitter), toutes les tentatives suivent le même schéma, ce qui peut créer des "collisions". En ajoutant un délai aléatoire dans l’intervalle de Backoff, chaque tentative est légèrement décalée, réduisant ainsi les risques de surcharge.
Nous intégrons ces mécanismes à plusieurs endroits clés de nos systèmes :
- Appels aux passerelles de paiement : Lorsqu’un paiement ne passe pas du premier coup, une nouvelle tentative est faite avec un Backoff exponentiel, tout en surveillant les délais maximum pour éviter des boucles infinies.
- Envoi d’e-mails via des API tierces : Les services d’envoi d’e-mails peuvent devenir temporairement indisponibles en cas de surcharge. Avec le Backoff et le Jitter, nous réduisons la charge et garantissons que les e-mails finissent par être envoyés.
- Requêtes vers nos bases de données : Même localement, des problèmes de contention ou de latence peuvent survenir. Le même principe s’applique pour gérer les connexions et éviter d’aggraver une situation déjà critique.
Pour illustrer concrètement ce mécanisme de retry, voici une implémentation simplifiée (code en python mais facilement comprehensible 😉 ):
# Retry simple (problématique) retry_interval = 1 # Toujours 1 seconde entre chaque essai # Exemple : 1s, 1s, 1s, 1s, 1s # Problème : Tous les clients réessaient au même moment! # Exponential Backoff base_delay = 1 # Délai de base en secondes max_delay = 32 # Délai maximum en secondes retry_count = 0 # Nombre de tentatives retry_delay = min(base_delay * (2 ** retry_count), max_delay) # Pour 5 tentatives : # Tentative 1 : 1s # Tentative 2 : 2s # Tentative 3 : 4s # Tentative 4 : 8s # Tentative 5 : 16s # Exponential Backoff avec Jitter (Full Jitter) max_delay = base_delay * (2 ** retry_count) actual_delay = random.uniform(0, max_delay) # Exemple pour retry_count = 3 (max_delay = 8s) : # Client A : 2.1s # Client B : 5.8s # Client C : 3.4s # Les requêtes sont naturellement espacées ! # Décorateur Python pour implémenter un retry avec backoff et jitter def retry_with_backoff(retries=5, base_delay=1): def decorator(func): def wrapper(*args, **kwargs): for n in range(retries): try: return func(*args, **kwargs) except Exception as e: if n == retries - 1: # Dernière tentative raise e # Calcul du délai avec backoff et jitter max_delay = base_delay * (2 ** n) sleep_time = random.uniform(0, max_delay) time.sleep(sleep_time) return None return wrapper return decorator # Utilisation @retry_with_backoff(retries=5, base_delay=1) def call_external_api(): # Votre code d'appel API ici pass
De nombreux frameworks et bibliothèques intègrent nativement des mécanismes de retry avec backoff. Avant d'implémenter votre propre solution, explorez les fonctionnalités existantes de vos outils ou optez pour des bibliothèques spécialisées et éprouvées telles que :
👉🏼 PHP
backoff
stechstudio • Updated Dec 3, 2024
👉🏼JAVA
resilience4j
resilience4j • Updated Dec 6, 2024
👉🏼PYTHON
backoff
litl • Updated Dec 6, 2024
Leçons tirées de cette approche
En intégrant ces mécanismes dès le départ, plusieurs avantages se sont révélés :
- Une infrastructure plus résiliente. Les échecs ponctuels n’affectent plus la disponibilité globale. Les utilisateurs ne remarquent presque jamais les perturbations. ✅
- Des systèmes plus stables sous forte charge. En évitant les boucles rapides ou les synchronisations massives, nous réduisons considérablement les risques de surcharge. ✅
- Des coûts maîtrisés. Limiter les retries inutiles permet de réduire la consommation de ressources, ce qui est particulièrement important pour des appels coûteux comme ceux vers des API tierces. ✅
Cependant, cela demande aussi de bien ajuster les paramètres (délai initial, maximum de retries, etc.) et de surveiller en continu les métriques pour s’assurer que le système réagit comme prévu.
En comprenant que les échecs font partie du système et en les traitant comme tels, vous pourrez bâtir une infrastructure robuste dès le premier jour.
Et vous, comment gérez-vous les échecs dans vos propres systèmes ?