Introduction à Hystrix

Introduction à Hystrix

1. Vue d'ensemble

Un système distribué typique consiste en plusieurs services collaborant ensemble.

Ces services sont sujets aux échecs ou aux réponses retardées. Si un service échoue, cela peut avoir un impact sur les performances des services et éventuellement rendre d’autres parties de l’application inaccessibles ou, dans le pire des cas, réduire l’ensemble de l’application.

Bien sûr, il existe des solutions disponibles qui aident à rendre les applications résilientes et tolérantes aux pannes - l'un de ces cadres est Hystrix.

La bibliothèque de structure Hystrix permet de contrôler l’interaction entre les services en offrant une tolérance aux pannes et une tolérance au temps de latence. Il améliore la résilience globale du système en isolant les services défaillants et en arrêtant l'effet en cascade des défaillances.

Dans cette série d'articles, nous commencerons par examiner comment Hystrix vient à la rescousse lorsqu'un service ou un système tombe en panne et ce que Hystrix peut accomplir dans ces circonstances.

2. Exemple simple

Hystrix assure la tolérance aux pannes et à la latence en isolant et en renvoyant les appels aux services distants.

Dans cet exemple simple, nous encapsulons un appel dans la méthoderun() desHystrixCommand:

class CommandHelloWorld extends HystrixCommand {

    private String name;

    CommandHelloWorld(String name) {
        super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
        this.name = name;
    }

    @Override
    protected String run() {
        return "Hello " + name + "!";
    }
}

et nous exécutons l'appel comme suit:

@Test
public void givenInputBobAndDefaultSettings_whenCommandExecuted_thenReturnHelloBob(){
    assertThat(new CommandHelloWorld("Bob").execute(), equalTo("Hello Bob!"));
}

3. Maven Setup

Pour utiliser Hystrix dans un projet Maven, nous devons avoir les dépendanceshystrix-core etrxjava-core de Netflix dans le projetpom.xml:


    com.netflix.hystrix
    hystrix-core
    1.5.4

La dernière version peut toujours être trouvéehere.


    com.netflix.rxjava
    rxjava-core
    0.20.7

La dernière version de cette bibliothèque peut toujours être trouvéehere.

4. Configuration du service à distance

Commençons par simuler un exemple du monde réel.

In the example below, la classeRemoteServiceTestSimulator représente un service sur un serveur distant. Il a une méthode qui répond avec un message après la période donnée. Nous pouvons imaginer que cette attente est une simulation d'un processus prenant beaucoup de temps sur le système distant, ce qui entraîne une réponse retardée au service appelant:

class RemoteServiceTestSimulator {

    private long wait;

    RemoteServiceTestSimulator(long wait) throws InterruptedException {
        this.wait = wait;
    }

    String execute() throws InterruptedException {
        Thread.sleep(wait);
        return "Success";
    }
}

And here is our sample client qui appelle lesRemoteServiceTestSimulator.

L'appel au service est isolé et enveloppé dans la méthoderun() d'unHystrixCommand. C'est ce wrapping qui fournit la résilience que nous avons évoquée ci-dessus:

class RemoteServiceTestCommand extends HystrixCommand {

    private RemoteServiceTestSimulator remoteService;

    RemoteServiceTestCommand(Setter config, RemoteServiceTestSimulator remoteService) {
        super(config);
        this.remoteService = remoteService;
    }

    @Override
    protected String run() throws Exception {
        return remoteService.execute();
    }
}

L'appel est exécuté en appelant la méthodeexecute() sur une instance de l'objetRemoteServiceTestCommand.

Le test suivant montre comment cela est fait:

@Test
public void givenSvcTimeoutOf100AndDefaultSettings_whenRemoteSvcExecuted_thenReturnSuccess()
  throws InterruptedException {

    HystrixCommand.Setter config = HystrixCommand
      .Setter
      .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroup2"));

    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(100)).execute(),
      equalTo("Success"));
}

Jusqu'à présent, nous avons vu comment encapsuler les appels de service à distance dans l'objetHystrixCommand. Dans la section ci-dessous, voyons comment gérer une situation où le service à distance commence à se détériorer.

5. Utilisation du service à distance et de la programmation défensive

5.1. Programmation défensive avec timeout

En général, la programmation fixe des délais d'attente pour les appels vers des services distants.

Commençons par regarder comment définir le délai d'expiration surHystrixCommand et comment cela permet de court-circuiter:

@Test
public void givenSvcTimeoutOf5000AndExecTimeoutOf10000_whenRemoteSvcExecuted_thenReturnSuccess()
  throws InterruptedException {

    HystrixCommand.Setter config = HystrixCommand
      .Setter
      .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupTest4"));

    HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter();
    commandProperties.withExecutionTimeoutInMilliseconds(10_000);
    config.andCommandPropertiesDefaults(commandProperties);

    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
      equalTo("Success"));
}

Dans le test ci-dessus, nous retardons la réponse du service en définissant le délai d’expiration à 500 ms. Nous définissons également le délai d'exécution surHystrixCommand à 10 000 ms, laissant ainsi suffisamment de temps pour que le service distant réponde.

Voyons maintenant ce qui se passe lorsque le délai d’expiration est inférieur au délai d’expiration du service:

@Test(expected = HystrixRuntimeException.class)
public void givenSvcTimeoutOf15000AndExecTimeoutOf5000_whenRemoteSvcExecuted_thenExpectHre()
  throws InterruptedException {

    HystrixCommand.Setter config = HystrixCommand
      .Setter
      .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupTest5"));

    HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter();
    commandProperties.withExecutionTimeoutInMilliseconds(5_000);
    config.andCommandPropertiesDefaults(commandProperties);

    new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(15_000)).execute();
}

Remarquez comment nous avons abaissé la barre et défini le délai d’expiration à 5 000 ms.

Nous nous attendons à ce que le service réponde dans les 5 000 ms, alors que nous avons prévu que le service réponde au bout de 15 000 ms. Si vous remarquez lorsque vous exécutez le test, le test se terminera après 5000 ms au lieu d'attendre 15000 ms et lancera unHystrixRuntimeException.

Cela montre comment Hystrix n'attend pas plus longtemps que le délai d'attente configuré pour une réponse. Cela contribue à rendre le système protégé par Hystrix plus réactif.

Dans les sections ci-dessous, nous examinerons le paramétrage de la taille du pool de threads afin d'éviter l'épuisement des threads.

5.2. Programmation défensive avec pool de threads limité

La définition de délais d'attente pour les appels de service ne résout pas tous les problèmes associés aux services distants.

Lorsqu'un service distant commence à répondre lentement, une application typique continuera d'appeler ce service distant.

L'application ne sait pas si le service distant est sain ou non et de nouveaux threads sont générés chaque fois qu'une demande arrive. Cela entraînera l'utilisation de threads sur un serveur déjà en difficulté.

Nous ne voulons pas que cela se produise car nous avons besoin de ces threads pour d'autres appels ou processus distants exécutés sur notre serveur et nous voulons également éviter une augmentation de l'utilisation du processeur.

Voyons comment définir la taille du pool de threads enHystrixCommand:

@Test
public void givenSvcTimeoutOf500AndExecTimeoutOf10000AndThreadPool_whenRemoteSvcExecuted
  _thenReturnSuccess() throws InterruptedException {

    HystrixCommand.Setter config = HystrixCommand
      .Setter
      .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupThreadPool"));

    HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter();
    commandProperties.withExecutionTimeoutInMilliseconds(10_000);
    config.andCommandPropertiesDefaults(commandProperties);
    config.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
      .withMaxQueueSize(10)
      .withCoreSize(3)
      .withQueueSizeRejectionThreshold(10));

    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
      equalTo("Success"));
}

Dans le test ci-dessus, nous définissons la taille maximale de la file d'attente, la taille de la file d'attente principale et la taille de rejet de la file d'attente. Hystrix commencera à rejeter les demandes lorsque le nombre maximum de threads aura atteint 10 et que la file d'attente des tâches aura atteint une taille de 10.

La taille de base est le nombre de threads qui restent toujours en vie dans le pool de threads.

5.3. Programmation défensive avec modèle de disjoncteur de court-circuit

Cependant, nous pouvons encore améliorer les appels de service à distance.

Prenons le cas où le service distant a commencé à échouer.

Nous ne voulons pas continuer à lui envoyer des demandes et gaspiller des ressources. Nous voudrions idéalement arrêter de faire des demandes pendant un certain temps afin de donner au service le temps de récupérer avant de reprendre les demandes. C'est ce qu'on appelle le motifShort Circuit Breaker.

Voyons comment Hystrix implémente ce modèle:

@Test
public void givenCircuitBreakerSetup_whenRemoteSvcCmdExecuted_thenReturnSuccess()
  throws InterruptedException {

    HystrixCommand.Setter config = HystrixCommand
      .Setter
      .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupCircuitBreaker"));

    HystrixCommandProperties.Setter properties = HystrixCommandProperties.Setter();
    properties.withExecutionTimeoutInMilliseconds(1000);
    properties.withCircuitBreakerSleepWindowInMilliseconds(4000);
    properties.withExecutionIsolationStrategy
     (HystrixCommandProperties.ExecutionIsolationStrategy.THREAD);
    properties.withCircuitBreakerEnabled(true);
    properties.withCircuitBreakerRequestVolumeThreshold(1);

    config.andCommandPropertiesDefaults(properties);
    config.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
      .withMaxQueueSize(1)
      .withCoreSize(1)
      .withQueueSizeRejectionThreshold(1));

    assertThat(this.invokeRemoteService(config, 10_000), equalTo(null));
    assertThat(this.invokeRemoteService(config, 10_000), equalTo(null));
    assertThat(this.invokeRemoteService(config, 10_000), equalTo(null));

    Thread.sleep(5000);

    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
      equalTo("Success"));

    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
      equalTo("Success"));

    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
      equalTo("Success"));
}
public String invokeRemoteService(HystrixCommand.Setter config, int timeout)
  throws InterruptedException {

    String response = null;

    try {
        response = new RemoteServiceTestCommand(config,
          new RemoteServiceTestSimulator(timeout)).execute();
    } catch (HystrixRuntimeException ex) {
        System.out.println("ex = " + ex);
    }

    return response;
}

Dans le test ci-dessus, nous avons défini différentes propriétés de disjoncteur. Les plus importants sont:

  • LeCircuitBreakerSleepWindow qui est réglé sur 4 000 ms. Ceci configure la fenêtre du disjoncteur et définit l’intervalle de temps après lequel la demande au service distant sera reprise.

  • LeCircuitBreakerRequestVolumeThreshold qui est mis à 1 et définit le nombre minimum de requêtes nécessaires avant que le taux d'échec ne soit pris en compte

Avec les paramètres ci-dessus en place, nosHystrixCommand vont maintenant se déclencher après deux demandes échouées. La troisième requête n'atteindra même pas le service distant même si nous avons défini le délai de service sur 500 ms,Hystrix court-circuitera et notre méthode renverranull comme réponse.

Nous ajouterons par la suite unThread.sleep(5000) afin de franchir la limite de la fenêtre de sommeil que nous avons définie. Cela entraînera la fermeture du circuit parHystrix et les demandes suivantes seront transmises avec succès.

6. Conclusion

En résumé, Hystrix est conçu pour:

  1. Assure la protection et le contrôle des pannes et de la latence des services généralement accessibles via le réseau

  2. Arrêtez la cascade d'échecs résultant de la panne de certains services

  3. Échouer rapidement et récupérer rapidement

  4. Se dégrader gracieusement si possible

  5. Surveillance en temps réel et alerte du centre de commande en cas de panne

Dans le prochain article, nous verrons comment combiner les avantages d'Hystrix avec le framework Spring.

Le code complet du projet et tous les exemples peuvent être trouvés sur lesgithub project.