Gestion des erreurs dans les systèmes distribués : quand réessayer, réconcilier ou planter

Gestion des erreurs dans les systèmes distribués : quand réessayer, réconcilier ou planter

Table des matières

Lorsque j’ai commencé à travailler avec les microservices, la gestion des erreurs me semblait confuse. Si un service rencontrait une erreur inattendue, ma première réaction était de lever une exception non interceptée ou de laisser le programme entrer en panique. Je pensais que le service devait simplement s’arrêter et quitter.

Cette approche posait problème. Mes programmes plantaient et redémarraient sans cesse à cause de légers problèmes réseau. J’ai fini par apprendre à garder les systèmes en vie grâce aux réessais, mais un nouveau dilemme est apparu : quand un service doit-il réessayer une opération, quand doit-il abandonner et planter, et quand doit-il ralentir et attendre pour se réconcilier ?

Par essais et erreurs, j’ai trouvé une façon fiable de structurer les microservices pour qu’ils fonctionnent de manière prévisible pendant des mois sans redémarrages inutiles.

La règle d’or : erreurs transitoires vs erreurs permanentes

La décision de réessayer ou de planter repose sur une règle simple : réessayez les erreurs transitoires, et faites planter le programme en cas d’erreurs permanentes.

Une erreur transitoire est un problème temporaire qui peut se résoudre lui-même avec le temps. Les glitches réseau, les pannes temporaires de services externes ou les verrous temporaires de base de données sont tous des erreurs transitoires. Si vous attendez un instant et réessayez, l’opération peut réussir.

Une erreur permanente ne se corrigera jamais d’elle-même. Si votre application utilise de mauvais identifiants de base de données, ou si un fichier de configuration requis est absent dans un environnement statique, réessayer cent fois ne résoudra pas le problème. Vous devez arrêter l’application.

Faire échouer vite au démarrage

Pour appliquer cette règle efficacement, structurez votre microservice en deux phases distinctes : le démarrage et l’exécution.

Poussez toutes les vérifications difficiles et permanentes au tout début de l’exécution du programme. Au démarrage de votre application, elle doit lire les fichiers de configuration, valider les variables et vérifier les détails essentiels de l’environnement.

Si quelque chose se passe mal pendant cette phase de démarrage, n’essayez pas de récupérer. Faites immédiatement planter l’application. Si un fichier de configuration statique est manquant, échouez vite et laissez votre orchestrateur (comme Kubernetes) gérer le redémarrage. Il n’y a aucun intérêt à démarrer une application cassée qui ne changera pas.

Utiliser les réessais pendant l’exécution

Une fois que votre service a terminé ses vérifications de démarrage et commence à servir le trafic, changez de stratégie. Traitez la plupart des nouvelles erreurs externes comme transitoires.

Si votre service envoie une requête réseau à une API externe et ne reçoit aucune réponse, ne faites pas planter le service. Le réseau peut avoir perdu le paquet, ou le service externe peut être temporairement surchargé. Dans ces scénarios d’exécution, vous devez réessayer la requête.

Pour gérer ces problèmes d’exécution efficacement, vous avez besoin d’un mécanisme de réessai solide. J’ai écrit une explication détaillée sur comment implémenter les timeouts, les retries et le backoff.

Le contexte compte : environnements statiques vs dynamiques

Parfois, la frontière entre une erreur permanente et une erreur transitoire dépend entièrement de l’architecture de votre système.

Prenons l’exemple d’un fichier de configuration manquant. Dans un déploiement statique où la configuration ne change jamais après le démarrage d’un conteneur, un fichier manquant est une erreur permanente. Vous devez faire planter l’application.

Cependant, dans un environnement dynamique où la configuration est mise à jour à l’exécution, un fichier manquant peut simplement signifier que la mise à jour n’est pas encore arrivée. Dans ce scénario, l’erreur est transitoire. Vous devez journaliser un avertissement informatif pour l’administrateur, attendre, puis réessayer la lecture du fichier.

Par exemple, imaginez un déploiement Kubernetes où un ConfigMap est mis à jour indépendamment du pod de votre application. Si votre service tente un hot-reload exactement au moment où le ConfigMap est en cours de synchronisation, le fichier peut être temporairement verrouillé ou incomplet. Faire planter le pod ici provoque une indisponibilité inutile. Intercepter l’erreur, appliquer un court backoff de 5 secondes, puis réessayer permet à la synchronisation de se terminer, gardant le service en vie sans intervention humaine.

La même logique s’applique aux services externes. Si vous attendez d’un service en aval qu’il soit complètement provisionné avant le démarrage de votre application, une connexion manquante est une erreur permanente — faites planter au démarrage. Si vous supposez une cohérence éventuelle où les services démarrent à des vitesses différentes, traitez le service manquant comme une erreur transitoire et réessayez la connexion jusqu’à ce qu’elle réussisse.

Quand arrêter les réessais : le modèle de réconciliation

Quand une connexion réseau tombe, votre application ne peut pas regarder dans l’avenir. Elle ne sait pas si la panne durera deux secondes ou deux heures. Naturellement, elle suppose que l’erreur est transitoire, commence à réessayer, et (avec un peu de chance) utilise le jitter pour randomiser les intervalles de requête.

Mais que se passe-t-il pendant une panne prolongée, comme une panne de table de routage pendant 30 minutes ?

Au bout de quelques minutes, vos boucles de réessai atteindront leur cap de backoff maximal (généralement 30 ou 60 secondes). Une fois ce cap atteint, votre code cesse d’augmenter son délai. Il reste simplement là, à marteler l’endpoint cassé chaque minute.

Si vous avez des dizaines d’instances qui font cela, elles créent un énorme mur de trafic bruyant. Au moment où un ingénieur répare le réseau, ce « thundering herd » peut instantanément faire planter le système en aval en cours de récupération.

Les erreurs transitoires peuvent devenir structurelles. Vous ne pouvez pas toujours boucler indéfiniment.

Après un nombre défini de tentatives ratées, déclenchez un circuit breaker. Arrêtez la boucle de réessai automatisée, revenez à votre dernier état connu sain, et passez à un modèle de réconciliation.

Au lieu d’attaquer sans relâche la dépendance, votre système reste au repos et attend un signal externe (comme un webhook d’orchestrateur ou un contrôle de santé en arrière-plan réussi) pour reprendre ses opérations en toute sécurité.

Conclusion

Construire des microservices capables de se remettre d’eux-mêmes nécessite des limites claires. Définissez tôt vos hypothèses sur l’environnement. Identifiez ce qui peut changer pendant l’exécution et ce qui reste fixe.

Faites planter tôt et proprement les erreurs fixes et irrécupérables au démarrage. Faites confiance à des réessais intelligents pour les problèmes dynamiques et temporaires pendant l’exécution. Et lorsque ces problèmes temporaires durent trop longtemps, cassez la boucle et faites confiance à la réconciliation.

En séparant votre validation au démarrage de votre exécution à l’exécution, et en sachant exactement quand arrêter les réessais, vous assurez que vos microservices s’arrêtent de manière prévisible lorsqu’ils le doivent et se rétablissent avec élégance lorsqu’ils le peuvent.

Appliquer ces limites fait partie intégrante de la construction d’applications résilientes. Pour en savoir plus sur la conception de systèmes capables de survivre aux pannes, lisez mon article approfondi sur Attributs de qualité des programmes informatiques : implémenter la robustesse logicielle.

Partager :

Articles Connexes

Évolution des données en microservices : éviter les changements cassants grâce à la compatibilité

Évolution des données en microservices : éviter les changements cassants grâce à la compatibilité

Les structures de données en microservices évoluent en permanence. Découvrez pourquoi concevoir pour l’évolutivité et appliquer une stratégie de …

Lire la suite about Évolution des données en microservices : éviter les changements cassants grâce à la compatibilité
Configuration Go YAML : corrigez le bug de configuration silencieux avec `mapstructure`

Configuration Go YAML : corrigez le bug de configuration silencieux avec mapstructure

Évitez les bugs silencieux de configuration Go dans vos microservices. Découvrez pourquoi le chargement direct d’une struct YAML, JSON ou TOML …

Lire la suite about Configuration Go YAML : corrigez le bug de configuration silencieux avec `mapstructure`
Robustesse logicielle et paradigmes de timeout/retry avec backoff

Robustesse logicielle et paradigmes de timeout/retry avec backoff

Les programmes accèdent à des ressources externes, notamment des périphériques I/O et des services distants. Ces ressources peuvent être peu fiables, …

Lire la suite about Robustesse logicielle et paradigmes de timeout/retry avec backoff