Introduction à Hystrix

1. Vue d'ensemble

Un système distribué typique se compose de nombreux 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 d'autres services affectant les performances et éventuellement rendre d'autres parties de l'application inaccessibles ou, dans le pire des cas, faire tomber 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 frameworks est Hystrix.

La bibliothèque de cadres Hystrix aide à contrôler l'interaction entre les services en offrant une tolérance aux pannes et à la 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 qu'Hystrix peut accomplir dans ces circonstances.

2. Exemple simple

La façon dont Hystrix fournit une tolérance aux pannes et à la latence consiste à isoler et à encapsuler les appels aux services distants.

Dans cet exemple simple, nous encapsulons un appel dans la méthode run () de HystrixCommand:

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. Configuration de Maven

Pour utiliser Hystrix dans un projet Maven, nous devons avoir les dépendances hystrix-core et rxjava-core de Netflix dans le projet pom.xml :

 com.netflix.hystrix hystrix-core 1.5.4  

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

 com.netflix.rxjava rxjava-core 0.20.7 

La dernière version de cette bibliothèque est toujours disponible ici.

4. Configuration du service à distance

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

Dans l'exemple ci - dessous , la classe RemoteServiceTestSimulator représente un service sur un serveur distant. Il a une méthode qui répond avec un message après la période de temps donnée. On peut imaginer que cette attente est une simulation d'un processus chronophage au niveau du système distant entraînant 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"; } }

Et voici notre exemple de client qui appelle le RemoteServiceTestSimulator .

L'appel au service est isolé et encapsulé dans la méthode run () d'un HystrixCommand. C'est cet emballage qui fournit la résilience dont nous avons parlé 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éthode execute () sur une instance de l' objet RemoteServiceTestCommand .

Le test suivant montre comment procéder:

@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' objet HystrixCommand . Dans la section ci-dessous, voyons comment gérer une situation où le service à distance commence à se détériorer.

5. Travailler avec un service à distance et une programmation défensive

5.1. Programmation défensive avec timeout

C'est une pratique de programmation générale de définir des délais d'attente pour les appels vers des services distants.

Commençons par regarder comment définir le délai d'expiration sur HystrixCommand et comment cela aide en court-circuitant:

@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 sur HystrixCommand à 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'exécution 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'exécution sur 5 000 ms.

Nous nous attendons à ce que le service réponde dans les 5 000 ms, alors que nous avons configuré le service pour qu'il réponde après 15 000 ms. Si vous remarquez lorsque vous exécutez le test, le test se terminera après 5 000 ms au lieu d'attendre 15 000 ms et lancera une exception HystrixRuntimeException.

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 la définition de la taille du pool de threads qui empêche l'épuisement des threads et nous discuterons de ses avantages.

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

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

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 requête arrive. Cela entraînera l'utilisation des threads sur un serveur déjà en difficulté.

We don't want this to happen as we need these threads for other remote calls or processes running on our server and we also want to avoid CPU utilization spiking up.

Let's see how to set the thread pool size in HystrixCommand:

@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")); }

In the above test, we are setting the maximum queue size, the core queue size and the queue rejection size. Hystrix will start rejecting the requests when the maximum number of threads have reached 10 and the task queue has reached a size of 10.

The core size is the number of threads that always stay alive in the thread pool.

5.3. Defensive Programming With Short Circuit Breaker Pattern

However, there is still an improvement that we can make to remote service calls.

Let's consider the case that the remote service has started failing.

We don't want to keep firing off requests at it and waste resources. We would ideally want to stop making requests for a certain amount of time in order to give the service time to recover before then resuming requests. This is what is called the Short Circuit Breaker pattern.

Let's see how Hystrix implements this pattern:

@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; }

In the above test we have set different circuit breaker properties. The most important ones are:

  • The CircuitBreakerSleepWindow which is set to 4,000 ms. This configures the circuit breaker window and defines the time interval after which the request to the remote service will be resumed
  • The CircuitBreakerRequestVolumeThreshold which is set to 1 and defines the minimum number of requests needed before the failure rate will be considered

With the above settings in place, our HystrixCommand will now trip open after two failed request. The third request will not even hit the remote service even though we have set the service delay to be 500 ms, Hystrix will short circuit and our method will return null as the response.

We will subsequently add a Thread.sleep(5000) in order to cross the limit of the sleep window that we have set. This will cause Hystrix to close the circuit and the subsequent requests will flow through successfully.

6. Conclusion

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

  1. Assurer la protection et le contrôle des pannes et de la latence des services généralement accessibles sur le réseau
  2. Arrêtez la cascade des pannes résultant de la panne de certains services
  3. Échouer rapidement et récupérer rapidement
  4. Dégrader gracieusement si possible
  5. Surveillance et alerte en temps réel 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 le projet github.