Tutoriel pour comprendre Circuit breaker, le patron de conception pour fiabiliser vos systèmes distribués (ou microservices)

| Antonio Gomes-Rodrigues | Edouard Perret |

Ce tutoriel s'intéresse à présenter Circuit breaker, qui est un patron de conception (design pattern) pour fiabiliser vos systèmes distribués (ou microservices).

Pour réagir à ce tutoriel, un espace de dialogue vous est proposé sur le forum Commentez .

- Titre : Tutoriel pour comprendre Circuit breaker, le patron de conception pour fiabiliser vos systèmes distribués (ou microservices)

- Auteurs : Antonio Gomes-Rodrigues, Edouard Perret

- Parution : 23 février 2017

- Mise à jour : 23 février 2017

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2016 Antonio Gomes-Rodrigues. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Et pourquoi ne pas utiliser la solution XXX à la place ?

- III. Migration dans le cloud avec du lift and shift (aussi appelée fork and lift)

- IV. Utilisation de répartiteurs de charge (Load balancers)

- V. Patron de conception : Timeout

- V-A. Patron de conception : Retry

- VI. Une solution possible : le patron de conception circuit breaker ?

- VII. Et dans la vraie vie ?

- VII-A. Cas 1 : Fonctionnalité de recherche qui ne répond plus et graceful degradation

- VII-B. Cas 2 : Affichage du montant de sa carte de fidélité indisponible et graceful degradation

- VII-C. Cas 3 : Réduction de la consommation des ressources du serveur

- VII-D. Cas 4 : Stop cascading failures ou comment éviter l'effet domino

- VII-E. Cas 5 : Réduire les temps de réponse et fail fast

- VIII. Comment l'implémenter ?

- IX. Et comment fais-je pour le tester ?

- IX-A. Commençons par les tests unitaires

- IX-B. Passons aux tests de robustesse

- IX-B-1. Apache JMeter pour simuler de la charge et piloter la génération des défaillances

- IX-B-2. Netflix Chaos Monkey pour générer des défaillances

- IX-B-3. Bibliothèques de manipulation de bytecode : Red Hat Byteman/Byte Buddy/… pour générer des défaillances

- IX-B-4. Outils système pour générer des défaillances

- IX-B-5. Un peu de supervision pour suivre nos tests de robustesse

- IX-B-6. Template de test

- X. Comment fais-je pour le superviser en production ?

- XI. C'est génial, y a-t-il des limites ?

- XII. En conclusion

- XIII. Remerciements

I - Introduction

L'évolution des besoins (réductions des coûts et du time to market, concept d'ATAWAD (AnyTime, AnyWhere, AnyDevice)…) a mis en avant certaines architectures (architecture applicative cloud ready, architecture microservices, architecture distribuée…).

Cela a engendré de nouvelles problématiques, en particulier l'augmentation du nombre de dépendances et donc potentiellement soumises à la latence du réseau.

C'est à ce moment qu'apparaissent à nouveau les Illusions de l'informatique distribuée :

- Le réseau est fiable.

- Le temps de latence est nul.

- La bande passante est infinie.

- Le réseau est sûr.

- La topologie du réseau ne change pas.

- Il y a un et un seul administrateur réseau.

- Le coût de transport est nul.

- Le réseau est homogène.

Et ces illusions ne prennent pas en compte la partie dépendance et son lot de problèmes (crash, temps de réponse lent, réponse non conforme…).

Pour répondre à ces défis, la philosophie de design for failure (les traitements applicatifs doivent, dès leur conception, prévoir le cas où les composants qu'ils appellent pourraient tomber en erreur) a pris encore plus d'importance.

Et donc nous sommes passés de : « prévenir toutes les défaillances » à : « les défaillances font partie du jeu ».

Parmi toutes les solutions composant le design for failure, nous allons nous pencher sur le patron de conception (design pattern) « Circuit Breaker » popularisé par Michael Nygard dans le livre «  Release It! ».

Mais avant cela, faisons un tour rapide sur d'autres solutions permettant de gérer des problèmes de dépendances.

II - Et pourquoi ne pas utiliser la solution XXX à la place ?

Le problème que nous voulons résoudre est la gestion des dépendances (externes ou internes) qui peuvent être (et le seront tôt ou tard) défaillantes lors de l'exécution de notre application.

Par exemple lorsqu'une application appelle le service de paiement, que faire lorsque celui-ci n'est pas accessible ?

Comme pour toutes les difficultés, plusieurs solutions sont possibles. En voici quelques-unes.

III - Migration dans le cloud avec du lift and shift (aussi appelée fork and lift)

Une solution simple est de se dire qu'il suffit de prendre son application telle quelle et de la mettre sur le cloud sans faire de modification (technique du lift and shift). Cela permet de tirer parti de tous les avantages de son fournisseur de cloud :

- Utilisation de services en haute disponibilité (par exemple AWS RDS PostgreSQL qui met à disposition un serveur de base de données en haute disponibilité d'un simple clic) ;

- utilisation d'une infrastructure moderne.

Malheureusement cela n'est pas suffisant, car même les fournisseurs de cloud les plus connus peuvent avoir des problèmes et provoquer des indisponibilités comme Google en 2013.

De plus si le problème est applicatif (ne supporte pas la charge, crash, envoi de réponse non conforme…), la migration dans le cloud n'apporte aucune solution à notre problème.

Dernières précisions :

- Certaines applications ne pourront pas profiter de cette solution, car elles ne pourront jamais être migrées sur le cloud, car :

- ne respectant pas les bonnes pratiques de développement (IP en dur dans le code…) ,

- nécessitant du hardware trop particulier ;

- Le passage dans le cloud, pour en tirer vraiment parti et éviter l'effet big bang, demande aussi un changement de culture.

- Le cloud ne rattrapera pas les architectures s'appuyant sur l'infrastructure (VMware Fault Tolerance…) et les exploitants qui compensaient à des coûts prohibitifs le manque de prise en compte de la résilience dès la conception.

IV - Utilisation de répartiteurs de charge (Load balancers)

Solution plus évoluée que la précédente, mais qui peut demander beaucoup de modifications du code source de notre application (ajout de health check, passage en stateless pour vraiment en tirer profit…), le répartiteur de charge.

C'est un composant réseau ou applicatif qui répartit les requêtes sur différentes instances de façon à équilibrer la charge.

Par exemple AWS ELB d'Amazon et HAProxy.

Dans notre cas (gérer les dépendances et réseaux défaillants) le répartiteur de charge va :

- sortir du cluster les instances défaillantes ou mettant trop de temps pour répondre ;

- répartir au mieux la charge.

Encore une fois, cela ne sera pas suffisant dans notre cas pour différentes raisons :

- impossibilité d'ajout de répartiteur de charge dans certains cas (service externe, protocole non supporté, stockage des données…) ;

- complexification de l'architecture (plus de composants/serveur/…) et donc de l'exploitation et du diagnostic des problèmes de production ;

- complexité de trouver la bonne configuration pour les lignes de vie (health check).

V - Patron de conception : Timeout

Autre solution possible, qui par contre va sûrement demander des modifications du code source de l'application, le patron de conception timeout.

Il permet de ne pas attendre indéfiniment une réponse en positionnant un temps d'attente maximal.

Le problème de ce patron de conception est qu'il n'est « pas très intelligent », car si le service appelé est hors service, le service appelant va quand même faire l'appel. Ce qui implique les points suivants :

- l'erreur n'arrivera qu'après le temps d'attente maximum (contraire au fail fast) ;

- consommation de ressources (connexion, mémoire…) inutile, car nous aurons une erreur à la fin.

V-A - Patron de conception : Retry

Dernier patron de conception présenté, le retry. Patron de conception qui demandera des modifications de code.

Il consiste à envoyer à nouveau la requête qui a échoué. Et donc si le service appelé « tombe en marche », cela sera transparent pour l'utilisateur au prix d'une latence significative.

Par contre si le service appelé reste hors service, cela provoque un risque de surcharge de l'application en multipliant les requêtes.

Et donc ce patron de conception n'est à utiliser que dans le cas particulier où nous savons, par expérience (mais dont la cause n'a pas encore été corrigée, car la solution est impossible/trop chère/inconnue), que l'interruption de service est temporaire et courte.

Mais le plus important est que les requêtes sur lesquelles on veut appliquer le patron de conception doivent être idempotentes, car sinon il y a un risque de corruption des données ou de comportement non prévu.

Par exemple supposons que notre application permette d'accélérer ou de ralentir une voiture à l'aide de deux boutons.

Lors de l'appui sur le bouton d'accélération, nous envoyons l'ordre d'accélérer de 5 km/h. Pour une raison quelconque, la requête n'arrive pas tout de suite et donc notre patron de conception envoie un deuxième ordre d'accélérer de 5 km/h. Résultat au lieu d'accélérer de 5 km/h nous accélérons de 10 km/h.

La bonne solution aurait été d'envoyer l'ordre idempotent : passer à 110 km/h. Dans ce cas peu importe le nombre d'ordres reçus.

Si l'utilisation d'une requête idempotente n'est pas possible, il faudra utiliser un autre patron de conception nommé Exactly-once Delivery qui peut être compliqué à mettre en place.

Nous pourrions encore parler longtemps des patrons de conception, mais il est temps de passer à l'essentiel de cet article : le patron de conception « circuit breaker ».

Pour avoir une idée des autres patrons de conception (Bulkhead, Event sourcing, Feature Flipping, Pets versus Cattle…), nous vous laissons lire notre livre blanc « Cloud Ready Apps ».

VI - Une solution possible : le patron de conception circuit breaker ?

Le circuit breaker permet de contrôler la collaboration entre différents services afin d'offrir une grande tolérance à la latence et à l'échec.

Pour cela, en fonction d'un certain nombre de critères d'erreur (timeout, nombre d'erreurs, élément dans la réponse), ce patron de conception permet de désactiver l'envoi de requêtes au service appelé et de renvoyer plus rapidement une réponse alternative de repli (fallback), aussi appelée graceful degradation.

Il agit comme un proxy implémentant une machine à états (Ouvert, Passant (fermé), Semi-ouvert) pour l'apprentissage de l'état du service.

On peut aussi le voir comme un feu tricolore de signalisation.

Comme nous pouvons le voir sur le schéma ci-dessus, en temps normal le circuit breaker est en mode passant.

Lorsque le nombre d'échecs successifs (ou toute autre métrique) dépasse un seuil, le circuit s'ouvre pour ne plus laisser passer de requêtes. À ce moment-là, deux mécanismes se déclenchent :

- mise en place de la réponse alternative de repli ;

- activation du processus du passage à l'état semi-ouvert (ici nous déclenchons un minuteur).

Une fois le seuil de passage à l'état semi-ouvert atteint (ici seuil du temps d'attente), le circuit breaker laisse à nouveau passer quelques requêtes et passe dans l'état passant si tout se déroule bien.

Avec un peu d'imagination, nous pouvons envisager une infinité de possibilités :

- stocker toutes les requêtes en erreur avec le maximum de détails pour les traiter plus tard ;

- avoir plusieurs stratégies de réponses alternatives en fonction du type d'erreur renvoyé par le service appelé (code « HTTP 503 Service Unavailable », mauvaise réponse…) ;

- piloter le passage d'un état à l'autre à l'aide d'une API (utile par exemple lors de tests) ;

- avoir des seuils intelligents qui s'adaptent après une période d'apprentissage ;

- etc.

Toutes ces possibilités nous permettent d'implémenter d'autres patrons de conception tels que les points suivants :

- Fail Fast : échouer rapidement pour réduire l'impact sur l'utilisateur ;

- Fail Silently : échouer de manière transparente pour l'utilisateur (pas de stacktrace…) ;

- Graceful degradation : adaptation automatique de l'application à une situation dégradée ;

- Stop cascading failures : éviter l'effet domino.

- Timeout ;

- Retry.

Comme pour tous les patrons de conception, utilisez-le à bon escient.

VII - Et dans la vraie vie ?

Bon c'est bien beau tout ça, mais cela reste de la théorie. Dans ce cas, passons à des cas concrets.

Mais avant d'aller plus loin il est important de rappeler qu'il faut bien connaître le fonctionnel pour implémenter et paramétrer le circuit breaker.

VII-A - Cas 1 : Fonctionnalité de recherche qui ne répond plus et graceful degradation

Vous êtes aux commandes d'un site d'e-commerce de vente de modèles réduits de planeurs. Tout se passe bien jusqu'au jour où la fonctionnalité de recherche tombe en panne.

Sans circuit breaker et le passage en mode dégradé, point de salut et une course contre la montre commence pour restaurer au plus vite le service coupable. Pendant ce temps vos clients ne peuvent plus faire de recherche et s'en vont chez les concurrents.

Avec le circuit breaker, le mode dégradé est déclenché rapidement et les clients reçoivent la réponse alternative de repli :

- affichage de la liste des produits les plus achetés avec un petit mot d'excuse et un coupon de réduction pour tenter de retenir les clients ;

- ou mieux, affichage de la liste des produits approuvés par un spécialiste de renommée mondiale.

VII-B - Cas 2 : Affichage du montant de sa carte de fidélité indisponible et graceful degradation

Cette fois-ci vous êtes aux commandes d'un site de gestion de cartes de fidélité. Les clients peuvent :

- voir les dernières transactions ;

- voir le montant de sa carte ;

- échanger des points contre des cadeaux ;

- etc.

Pas de chance, le service de récupération du dernier montant de la carte de fidélité tombe en panne.

Sans circuit breaker, le client n'a plus l'information et appelle le support.

Avec le circuit breaker, on affiche la dernière valeur que l'on a mise en cache (par exemple toutes les nuits ou un peu avant le pic de consultation si l'on connaît l'heure…) avec la date de la valeur. Solution qui a été trouvée avec le métier après avoir hésité entre cette solution qui demande un effort d'implémentation et une solution beaucoup plus basique (affichage d'un petit message d'erreur compréhensible par le client).

VII-C - Cas 3 : Réduction de la consommation des ressources du serveur

Supposons que nous ayons deux services.

Le service 1 appelle le service 2 qui fait de lourds traitements (calcul mathématique, appel de traitement PL/SQL lourd…) qui finissent tous en erreur à cause d'un bug sur la nouvelle version.

Sans circuit breaker, le service 2 va passer son temps à faire de lourds traitements inutilement et donc consommer des ressources pour rien. Le service 1 va aussi consommer plus de ressources que nécessaire, car il va devoir maintenir des connexions/threads/objets en mémoire le temps d'avoir la réponse négative du service 2.

Si en plus on est dans le cloud où l'on paye à la consommation et qu'on a utilisé des Auto Scaling, la facture risque d'être salée.

Avec le circuit breaker, une fois déclenché, cette surconsommation aurait été évitée en passant en mode dégradé et en désactivant le service 2.

VII-D - Cas 4 : Stop cascading failures ou comment éviter l'effet domino

L'affichage d'une page de notre application fait appel à plusieurs services.

Pas de chance le service en bout de chaîne (service 4) tombe.

Le service 3 tombe à son tour, car dépendant des réponses du service 4. Les raisons du crash peuvent être nombreuses et diverses :

- surconsommation de ressource ;

- mauvaise gestion des réponses fausses du service 4 ;

- service 3 et service 4 sur le même serveur qui ne répond plus à cause du crash du service 4 ;

- éviction du service 3 par un répartiteur de charge juste devant, car il ne répond plus correctement ;

- etc.

Et ainsi de suite, jusqu'à l'affichage d'une erreur incompréhensible au client.

Le circuit breaker aurait évité la propagation et seul le service 4 aurait été impacté.

Dans cet exemple, nous constatons bien l'avantage par rapport aux patrons de conception timeout et retry qui auraient été inutiles, voire auraient empiré la situation.

Un autre patron de conception intéressant dans ce cas est le bulkhead, voir notre livre blanc pour plus de détails.

VII-E - Cas 5 : Réduire les temps de réponse et fail fast

Toujours avec la même application, une autre fonctionnalité dépend de plusieurs services.

En temps normal tous les services répondent en moins de 500 ms et donc notre fonctionnalité à un temps de réponse en dessous de la seconde (l'application met 500 ms pour traiter tous les résultats des services).

Un problème intervient sur notre service catalogue qui ne répond plus. La réponse arrive 2 s après (réglage du timeout).

Notre temps de réponse est donc maintenant égal au maximum des temps de réponse des services et du temps de traitement.

Ce qui nous donne 2 s + 500 ms =  2,5 s.

Sans circuit breaker notre fonctionnalité prendra toujours 2,5 s pour répondre le temps que le service catalogue soit réparé.

Avec le circuit breaker, une fois ouvert et donc passage en mode dégradé, le temps de réponse descendra à nouveau à 1 s. Ici le choix a été fait d'avoir un bon temps de réponse avec un mode dégradé, plutôt qu'un temps de réponse très mauvais avec la bonne réponse.

VIII - Comment l'implémenter ?

Plusieurs solutions sont possibles pour l'implémenter.

Par exemple en Java il existe des bibliothèques qui le font pour nous comme :

- Spring Cloud Netflix ;

- Netflix Hystrix ;

- Breakr.

Focalisons-nous sur Netflix Hystrix.

Hystrix est un framework open source libéré par Netflix en 2012 et intégré dans Spring Cloud.

Pour l'utiliser dans notre projet Java, rien de plus simple :

1. Étendre la class HystrixCommand

1. Dans le constructeur de sa class, appeler le constructeur de HystrixCommand avec les paramètres désirés

1. Surcharger la méthode run() et y mettre son code

1. Implémenter la réponse alternative de repli dans la méthode getFallback()

1. Ajouter les dépendances d'Hystrix à son projet, Par exemple avec Apache Maven

Ce qui nous donne à la fin :

Comme nous venons de le voir, l'utilisation de Hystrix n'est pas très compliquée.

Les difficultés sont plutôt de pouvoir :

- avoir une réponse alternative de repli pertinente, rapide et toujours disponible ;

- paramétrer finement le circuit breaker en fonction du cas d'utilisation ;

- connaître tous les appels sortants ;

Sur les appels sortants, plusieurs solutions sont possibles pour les détecter :

- utilisation d'un APM bien configuré ;

- instrumenter le bytecode Java pour récupérer la stacktrace de chaque appel sortant ;

- utiliser la solution Hystrix Network Auditor Agent qui se présente sous la forme d'un agent Java.

IX - Et comment fais-je pour le tester ?

Une fois l'implémentation réalisée, l'étape suivante est de la tester pour être sûr que tout marche comme convenu.

Nous allons séparer les tests en deux parties :

- les tests unitaires qui doivent être nombreux, automatisés, disponibles dès le début du projet afin d'aider à construire l'application ;

- les tests de robustesse (test avec la charge cible du système durant lequel on dégrade volontairement une ressource afin d'éprouver la robustesse du système) qui doivent être joués régulièrement, et de préférence automatisés afin de contrôler notre application sous charge.

IX-A - Commençons par les tests unitaires

Si Netflix Hystrix est utilisé, un moyen simple de tester que tout fonctionne correctement lorsque le circuit breaker est ouvert, est d'utiliser la propriété forceOpen.

Une autre solution plus générique est d'utiliser un outil de mock où l'on peut scripter les réponses.

WireMock est l'un d'eux. Il permet de mocker une API et de paramétrer les réponses (ajout de délai, réponse erronée…).

Une solution plus simple si nous ne voulons pas ajouter un nouvel outil dans le projet est de mocker la fonction de wrapping pour avoir le résultat attendu.

Mais la meilleure solution est de prévoir dans votre code le moyen d'ouvrir à la demande le circuit breaker pour réaliser les tests le plus simplement possible (comme dans Hystrix et sa propriété forceOpen).

IX-B - Passons aux tests de robustesse

Cette étape ne doit pas être négligée, car pour avoir réalisé de nombreux tests de robustesse dans ma carrière, régulièrement j'ai eu des surprises (effet domino d'un crash qui s'étend à toute l'application, mécanisme d'éviction de la dépendance défaillante buggée, mécanisme de retour de la dépendance défaillante une fois le problème corrigé instable…).

De plus réaliser ces tests a de nombreux avantages :

- détecter les effets boule de neige ;

- réduire les risques en production ;

- améliorer la supervision de la production ;

- entraîner les équipes d'exploitation aux cas de défaillance ;

- documenter le processus à suivre en cas de défaillance ;

- tester le PRA ;

- etc.

Quelques possibilités de test de robustesse :

- faire tomber une dépendance ;

- dégrader le réseau (paquets perdus, latence élevée, réduction du débit…) ;

- dégrader le temps de réponse du service appelé de manière aléatoire ;

- générer un comportement anormal (lever d'exception…) des services ;

- renvoyer des réponses mal construites ;

- etc.

Passons au concret avec quelques exemples, mais avant cela une présentation rapide de quelques outils est nécessaire.

IX-B-1 - Apache JMeter pour simuler de la charge et piloter la génération des défaillances

JMeter est un outil libre qui permet d'effectuer des tests de performance sur des applications. Il permet de simuler le comportement de plusieurs utilisateurs agissant de manière simultanée.

Il est aussi possible avec cet outil de conserver les résultats et de les enregistrer au format CSV et en base de données, par exemple InfluxDB.

Quelques fonctionnalités utiles lors de test de robustesse :

- groupe d'unités de début : va nous permettre d'initialiser un certain nombre d'actions en début de test ;

- groupe d'unités : va nous permettre de regrouper les utilisateurs en population ;

- échantillon JSR223 et Requête Java : va nous permettre d'exécuter du code Java/Groovy/JavaScript/… ;

- requête HTTP : va nous permettre de faire des appels HTTP ;

- appel de processus système : va nous permettre de faire des appels à des programmes.

IX-B-2 - Netflix Chaos Monkey pour générer des défaillances

Chaos Monkey est un outil open source développé par Netflix pour tester le bon fonctionnement de son écosystème cloud. Sachant que les pannes sont inévitables, cet outil est destiné à stopper aléatoirement des instances de machines virtuelles et des services dans le but de détecter les points faibles de l'architecture mise en place. L'arrêt des machines simule d'hypothétiques pannes et permet de s'assurer que le système est construit avec un degré de redondance suffisant.

IX-B-3 - Bibliothèques de manipulation de bytecode : Red Hat Byteman/Byte Buddy/… pour générer des défaillances

Ces bibliothèques vont nous aider à injecter du code à la volée pendant l'exécution de notre application. Et bien sûr, le code injecté permettra de provoquer une défaillance :

- exceptions ;

- timeout ;

- augmentation du temps d'attente ;

- injection de valeurs fausses dans la réponse ;

- etc.

Regardons quelques exemples avec Byteman, mais avant cela voici une petite explication rapide (n'hésitez pas à lire la documentation officielle pour plus d'informations) de comment il marche.

Byteman se présente sous la forme d'un agent Java (qui s'attache à la JVM avec le paramètre javaagent lors du lancement de notre application ou le programme bmjava.sh livré avec Byteman).

Il ne nous reste plus qu'à injecter des Rules.

Une Rule se compose de :

- un nom ;

- la classe et la méthode où nous allons injecter notre code ;

- notre code.

Et lors de l'exécution de notre application, à chaque exécution de la méthode ciblée, notre code sera lui aussi exécuté.

Exemple de perte de connexion à la base de données :

Exemple d'augmentation du temps de réponse :

IX-B-4 - Outils système pour générer des défaillances

De nombreux outils livrés avec votre système d'exploitation préféré peuvent aider à créer des défaillances.

Par exemple sous Linux :

- la commande kill pour tuer un processus ;

- la commande tc pour modifier le comportement du réseau.

Exemple de simulation de perte de paquet avec la commande tc :

IX-B-5 - Un peu de supervision pour suivre nos tests de robustesse

Et qui dit test dit supervision afin de comprendre ce qui se passe.

Pour cela nous allons utiliser :

- InfluxDB pour stocker nos données horodatées ;

- Grafana pour visualiser les résultats ;

- AWS CloudWatch pour avoir des informations sur les instances des services.

IX-B-6 - Template de test

Maintenant que tout est en place, il ne nous reste plus qu'à implémenter nos tests de robustesse en suivant ce modèle.

Avec les parties suivantes :

- préparation de l'environnement : nous réalisons toutes les actions préalables (action de son outil de manipulation de bytecode, chargement des caches…) ;

- simulation d'utilisateurs : nous simulons des actions utilisateurs pendant toute la durée du test ;

- génération de la défaillance : au bout de X minutes, on provoque la défaillance ;

- correction de la défaillance : au bout de X + Y minute, on répare la défaillance pour vérifier que l'application revient dans un état stable.

Comme on peut le voir, JMeter joue aussi le rôle d'orchestration en activant et désactivant la défaillance pendant que l'application est sous charge.

Résumons le tout avec des schémas :

Étape 1 :

JMeter simule des utilisateurs jusqu'à la vitesse de croisière (charge atteinte, application et temps de réponse stables).

Étape 2 :

JMeter :

- demande au simulateur de défaillance de générer un problème (perte du réseau, crash d'un service…) ;

- envoie l'heure exacte du déclenchement dans l'outil de supervision.

Puis nous laissons tourner assez longtemps pour récupérer toutes les informations nécessaires pour l'analyse et le bon déclenchement du circuit breaker.

Étape 3 :

Nous corrigeons la défaillance pour vérifier que tout revient à l'état prévu.

Et comme pour tous les tests, n'oubliez pas d'automatiser ce qui est possible (attention au coût de maintenance) pour les jouer souvent.

X - Comment fais-je pour le superviser en production ?

Notre application a passé tous les tests et il est temps de passer en production.

Si l'on reste sur Hystrix, il existe beaucoup de métriques.

La liste est disponible sur le site officiel.

Une des difficultés d'une bonne supervision est de réussir à obtenir des tableaux de bord où d'un simple coup d'œil on peut obtenir le maximum d'information.

La première étape est de choisir les bonnes métriques.

Notre sélection est :

- état du circuit breaker (ouvert, semi-ouvert ou fermé) ;

- ressenti utilisateur (pages en erreur ou non) ;

- état du service appelé.

Avec ces trois métriques, nous connaissons l'état de l'application rapidement.

Ce qui nous donne ce résultat :

Cette capture montre bien le fonctionnement d'un circuit breaker.

On y voit toutes les étapes :

- on commence par un service up et un circuit breaker fermé. Le ressenti utilisateur est bon ;

- vers 12 h 55 le service tombe et nous commençons à avoir les premières erreurs côté utilisateur ;

- le circuit breaker se déclenche (le temps d'avoir dépassé le seuil d'ouverture) et on passe en mode dégradé ;

- vers 13 h 55 le service se met en nouveau en route, mais nous restons en mode dégradé le temps que le circuit breaker passe du mode ouvert à semi-ouvert et enfin fermé ;

- une fois fermé, tout devient à nouveau normal comme nous le constatons.

XI - C'est génial, y a-t-il des limites ?

Avant de vouloir en mettre partout, il faut connaître les limites et ce que cela implique.

Le but du circuit breaker est de gérer des problèmes de dépendances.

Et cerise sur le gâteau, il peut aussi dans certains cas gérer les pics de charge de notre application en désactivant (avec le passage en mode dégradé) les services qui ne supportent pas la charge.

Dans un premier temps le circuit breaker ne peut pas s'appliquer à tous les cas, car une solution de repli est nécessaire. Par exemple un batch qui tourne toutes les nuits et qui doit générer des résultats fiables à partir de plusieurs services : Si un service n'est pas disponible, le calcul ne pourra pas être réalisé.

Une autre difficulté est la partie paramétrage du circuit breaker (condition pour ouvrir le circuit breaker, à partir de quand on passe dans l'état semi-ouvert….). Sans une bonne supervision de l'application et une bonne connaissance fonctionnelle, ce paramétrage peut devenir très fastidieux et compliqué à mettre en place.

Une préconisation pour le bon fonctionnement du circuit breaker est d'avoir une solution de repli (fallback) qui réponde rapidement et soit toujours disponible. Dans certains cas, une solution simple peut être de stocker la réponse alternative de manière statique sur un AWS S3.

Dernier point, avec la multiplication des couches (plusieurs circuit breakers, répartiteur de charge…), le timeout configuré dans le circuit breaker peut s'accumuler avec les autres timeout et donc ne pas être aussi réactif par rapport au client que cela devrait être.

XII - En conclusion

Après cette série d'articles se pose la question suivante  ?« Est-ce qu'il faut utiliser le patron de conception circuit breaker, et si oui quand ? ».

Quelques questions à se poser avant son utilisation :

- A-t-on une solution de repli (fallback) rapide et toujours disponible ?

- L'utilisation d'une autre solution mieux maîtrisée (load balancer, timeout…) suffit-elle ?

- La dépendance est-elle sur un chemin critique ?

Une fois les réponses obtenues, nous pouvons nous demander si d'autres personnes l'utilisent et si on trouve facilement de la documentation.

La réponse est oui pour ces deux questions : Netflix l'utilise sans problème depuis des années dans des conditions extrêmes et trouver de la documentation est trivial.

De plus le circuit breaker est vraiment le couteau suisse d'une architecture cloud, car il permet d'implémenter d'autres patrons de conception (timeout, graceful degradation, fail fast…).

Mais comme tout patron de conception, il induit de la complexité et il convient donc d'en peser le pour et le contre.

XIII - Remerciements

Cet article a été publié avec l'aimable autorisation de la société Octo.

Nous tenons à remercier f-leb et Maxy35 pour la relecture orthographique attentive de cet article et Mickael BARON pour la mise au gabarit.