Circuit breaker, un pattern pour fiabiliser vos systèmes distribués (ou microservices) : partie 3

Maintenant que nous avons vu la théorie sur les précédents articles disponibles ici et ici, penchons-nous sur la pratique.

Comment l’implémenter ?

Plusieurs solutions sont possibles pour l’implémenter. Par exemple en Java il existe des librairies qui le font pour nous comme :

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 :

  • Étendre la class HystrixCommand
public class CallController extends HystrixCommand {
  • Dans le constructeur de sa class, appeler le constructeur de HystrixCommand avec les paramètres désirés
public CallController() {
		super(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("MyGroup2"))
				.andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withCircuitBreakerEnabled(true)
						.withExecutionTimeoutEnabled(true)
						.withExecutionTimeoutInMilliseconds(5000)
						.withExecutionIsolationThreadInterruptOnTimeout(true)
						.withCircuitBreakerRequestVolumeThreshold(5)
						.withCircuitBreakerSleepWindowInMilliseconds(1 * 60 * 1000)
						.withMetricsRollingStatisticalWindowInMilliseconds(3 * 61 * 1000)));
  • Surcharger la méthode run() et y mettre son code
@Override
	protected String run() {

		String message_distant;
		Socket socket;
		BufferedReader in;
	

		
		try {	
			System.out.println(" Demande de connexion " + InetAddress.getLocalHost());
			...
			System.out.println(" Message distant " + message_distant);
			socket.close();
		} catch (UnknownHostException e) {
			System.out.println("UnknownHostEXCEPTION");
			throw new RuntimeException();
		} catch (IOException e) {
			System.out.println(" IOEXCEPTION");
			// e.printStackTrace();
			throw new RuntimeException();
		}
		if (message_distant == null) {
			throw new RuntimeException();
		}
		return "-----" + message_distant + " -------";

	}
  • Implémenter la réponse alternative de repli dans la méthode getFallback()
@Override
	protected String getFallback() {
		return "---- Fallback : No Answer from distant Server ----- ";
	}
  • Ajouter les dépendances d’Hystrix à son projet

Par exemple avec Apache Maven.

com.netflix.hystrix
    hystrix-core
    x.y.z

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

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, de préférence automatisés afin de contrôler notre application sous charge.

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…). 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).

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 Plan de reprise d'activité (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égradé le temps de réponse du service appelé de manière aléatoire
  • Générer un comportement anormal (levée 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.

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

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.

Librairies de manipulation de bytecode : Red Hat Byteman/Byte Buddy/… pour générer des défaillances

Ces librairies 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.

Nous allons prendre pour exemple Byteman. (N’hésitez pas à lire sa documentation officielle pour plus d’informations.)

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 Rules se compose :

  • D'un nom
  • De la classe et la méthode dans laquelle nous allons injecter notre code
  • De notre code en lui-même

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 :

RULE JdbcOwnerRepositoryImpl.findById throw an exception
CLASS JdbcOwnerRepositoryImpl
METHOD findById
AT ENTRY
IF true
DO throw new org.springframework.dao.DataRetrievalFailureException("Probleme de connexion a la base de donnees")
ENDRULE

Exemple d’augmentation du temps de réponse :

RULE Wait in OwnerController.processFindForm entry
CLASS OwnerController
METHOD processFindForm
AT ENTRY
IF true
DO Thread.sleep(8000)
ENDRULE

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 :

tc qdisc add dev wlan0 root netem loss 10%

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

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.

JMeter template

Avec les parties suivantes :

  • Préparation 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).

Schéma global

Étape 2 : JMeter demande au simulateur de défaillance de générer un problème (perte du réseau, crash d’un service…) et envoie l’heure exacte du déclenchement dans l’outil de supervision.

Ensuite 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.

Schéma global

É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.

Conclusion

Nous savons maintenant comment tester le circuit breaker.

Nous verrons lors du prochain et dernier article comment le superviser et ses limites.

Pour aller plus loin, notre nouveau livre blanc sur le sujet vient de sortir :

TELECHARGER LE LIVRE BLANC