Un guide de Spring Cloud Netflix - Hystrix

1. Vue d'ensemble

Dans ce didacticiel, nous aborderons Spring Cloud Netflix Hystrix - la bibliothèque de tolérance aux pannes. Nous utiliserons la bibliothèque et implémenterons le modèle d'entreprise Circuit Breaker, qui décrit une stratégie contre les pannes en cascade à différents niveaux dans une application.

Le principe est analogue à l'électronique: Hystrix surveille les méthodes pour les appels en échec aux services associés. S'il y a un tel échec, il ouvrira le circuit et transférera l'appel vers une méthode de secours.

La bibliothèque tolérera les pannes jusqu'à un certain seuil. Au-delà, cela laisse le circuit ouvert. Cela signifie qu'il transmettra tous les appels ultérieurs à la méthode de secours, pour éviter de futurs échecs. Cela crée une mémoire tampon pour que le service associé récupère de son état d'échec.

2. Producteur REST

Pour créer un scénario illustrant le modèle de disjoncteur, nous avons d'abord besoin d'un service. Nous l'appellerons «REST Producer» car il fournit des données pour le «REST Consumer» activé par Hystrix, que nous créerons à l'étape suivante.

Créons un nouveau projet Maven en utilisant la dépendance spring-boot-starter-web :

 org.springframework.boot spring-boot-starter-web 2.2.6.RELEASE  

Le projet lui-même est volontairement maintenu simple. Il se compose d'une interface de contrôleur avec une méthode GET annotée @RequestMapping renvoyant simplement une chaîne, un @RestController implémentant cette interface et une @SpringBootApplication .

Nous allons commencer par l'interface:

public interface GreetingController { @GetMapping("/greeting/{username}") String greeting(@PathVariable("username") String username); }

Et la mise en œuvre:

@RestController public class GreetingControllerImpl implements GreetingController { @Override public String greeting(@PathVariable("username") String username) { return String.format("Hello %s!\n", username); } }

Ensuite, nous allons écrire la classe d'application principale:

@SpringBootApplication public class RestProducerApplication { public static void main(String[] args) { SpringApplication.run(RestProducerApplication.class, args); } }

Pour compléter cette section, la seule chose à faire est de configurer un port d'application sur lequel nous allons écouter. Nous n'utiliserons pas le port par défaut 8080 car le port doit rester réservé à l'application décrite à l'étape suivante.

De plus, nous définissons un nom d'application pour pouvoir rechercher notre producteur à partir de l'application cliente que nous présenterons plus tard.

Précisons ensuite un port de 9090 et un nom de rest-producteur dans notre fichier application.properties :

server.port=9090 spring.application.name=rest-producer

Nous pouvons maintenant tester notre producteur en utilisant cURL:

$> curl //localhost:9090/greeting/Cid Hello Cid!

3. Consommateur REST avec Hystrix

Pour notre scénario de démonstration, nous allons implémenter une application Web, qui consomme le service REST de l'étape précédente à l'aide de RestTemplate et Hystrix . Par souci de simplicité, nous l'appellerons le «consommateur REST».

Par conséquent, nous créons un nouveau projet Maven avec spring-cloud -starter- hystrix , spring-boot-starter-web et spring-boot-starter-thymeleaf comme dépendances:

 org.springframework.cloud spring-cloud-starter-hystrix 1.4.7.RELEASE   org.springframework.boot spring-boot-starter-web 2.2.6.RELEASE   org.springframework.boot spring-boot-starter-thymeleaf 2.2.6.RELEASE 

Pour que le disjoncteur fonctionne, Hystix analysera les classes annotées @Component ou @Service pour les méthodes annotées @HystixCommand , implémentera un proxy pour celui-ci et surveillera ses appels.

Nous allons d'abord créer une classe @Service , qui sera injectée dans un @Controller . Puisque nous construisons une application Web à l'aide de Thymeleaf, nous avons également besoin d'un modèle HTML pour servir de vue.

Ce sera notre @Service injectable implémentant un @HystrixCommand avec une méthode de secours associée. Cette solution de secours doit utiliser la même signature que l'original:

@Service public class GreetingService { @HystrixCommand(fallbackMethod = "defaultGreeting") public String getGreeting(String username) { return new RestTemplate() .getForObject("//localhost:9090/greeting/{username}", String.class, username); } private String defaultGreeting(String username) { return "Hello User!"; } }

RestConsumerApplication sera notre principale classe d'application. L' annotation @EnableCircuitBreaker analysera le chemin de classe pour toute implémentation de disjoncteur compatible.

Pour utiliser Hystrix explicitement, nous devons annoter cette classe avec @EnableHystrix :

@SpringBootApplication @EnableCircuitBreaker public class RestConsumerApplication { public static void main(String[] args) { SpringApplication.run(RestConsumerApplication.class, args); } }

Nous allons configurer le contrôleur à l'aide de notre GreetingService :

@Controller public class GreetingController { @Autowired private GreetingService greetingService; @GetMapping("/get-greeting/{username}") public String getGreeting(Model model, @PathVariable("username") String username) { model.addAttribute("greeting", greetingService.getGreeting(username)); return "greeting-view"; } }

Et voici le modèle HTML:

   Greetings from Hystrix   

Pour nous assurer que l'application écoute sur un port défini, nous mettons les éléments suivants dans un fichier application.properties :

server.port=8080

Pour voir un disjoncteur Hystix en action, nous démarrons notre consommateur et pointons notre navigateur vers // localhost: 8080 / get-greet / Cid . Dans des circonstances normales, les éléments suivants seront affichés:

Hello Cid!

Pour simuler un échec de notre producteur, nous l'arrêterons simplement, et après avoir terminé d'actualiser le navigateur, nous devrions voir un message générique, renvoyé par la méthode de secours dans notre @Service :

Hello User!

4. Consommateur REST avec Hystrix et Feign

Maintenant, nous allons modifier le projet de l'étape précédente pour utiliser Spring Netflix Feign comme client REST déclaratif, au lieu de Spring RestTemplate .

L'avantage est que nous sommes par la suite en mesure de refactoriser facilement notre interface client Feign pour utiliser Spring Netflix Eureka pour la découverte de services.

Pour démarrer le nouveau projet, nous allons faire une copie de notre consommateur et ajouter notre producteur et spring-cloud-starter-feign en tant que dépendances:

 com.baeldung.spring.cloud spring-cloud-hystrix-rest-producer 1.0.0-SNAPSHOT   org.springframework.cloud spring-cloud-starter-feign 1.1.5.RELEASE 

Now, we’re able to use our GreetingController to extend a Feign Client. We'll implement Hystrix fallback as a static inner class annotated with @Component.

Alternatively, we could define a @Bean annotated method returning an instance of this fallback class.

The name property of the @FeignClient is mandatory. It is used, to look-up the application either by service discovery via a Eureka Client or by URL, if this property is given:

@FeignClient( name = "rest-producer" url = "//localhost:9090", fallback = GreetingClient.GreetingClientFallback.class ) public interface GreetingClient extends GreetingController { @Component public static class GreetingClientFallback implements GreetingController { @Override public String greeting(@PathVariable("username") String username) { return "Hello User!"; } } }

For more on using Spring Netflix Eureka for service discovery have a look at this article.

In the RestConsumerFeignApplication, we’ll put an additional annotation to enable Feign integration, in fact, @EnableFeignClients, to the main application class:

@SpringBootApplication @EnableCircuitBreaker @EnableFeignClients public class RestConsumerFeignApplication { public static void main(String[] args) { SpringApplication.run(RestConsumerFeignApplication.class, args); } }

We’re going to modify the controller to use an auto-wired Feign Client, rather than the previously injected @Service, to retrieve our greeting:

@Controller public class GreetingController { @Autowired private GreetingClient greetingClient; @GetMapping("/get-greeting/{username}") public String getGreeting(Model model, @PathVariable("username") String username) { model.addAttribute("greeting", greetingClient.greeting(username)); return "greeting-view"; } }

To distinguish this example from the previous, we'll alter the application listening port in the application.properties:

server.port=8082

Finally, we'll test this Feign-enabled consumer like the one from the previous section. The expected result should be the same.

5. Cache Fallback With Hystrix

Now, we are going to add Hystrix to our Spring Cloud project. In this cloud project, we have a rating service that talks to the database and gets ratings of books.

Let's assume that our database is a resource under demand, and its response latency might vary in time or might not be available in times. We'll handle this scenario with the Hystrix Circuit Breaker falling back to a cache for the data.

5.1. Setup and Configuration

Let us add the spring-cloud-starter-hystrix dependency to our rating module:

 org.springframework.cloud spring-cloud-starter-hystrix 

When ratings are inserted/updated/deleted in the database, we'll replicate the same to the Redis cache with a Repository. To learn more about Redis, check this article.

Let's update the RatingService to wrap the database querying methods in a Hystrix command with @HystrixCommand and configure it with a fallback to reading from Redis:

@HystrixCommand( commandKey = "ratingsByIdFromDB", fallbackMethod = "findCachedRatingById", ignoreExceptions = { RatingNotFoundException.class }) public Rating findRatingById(Long ratingId) { return Optional.ofNullable(ratingRepository.findOne(ratingId)) .orElseThrow(() -> new RatingNotFoundException("Rating not found. ID: " + ratingId)); } public Rating findCachedRatingById(Long ratingId) { return cacheRepository.findCachedRatingById(ratingId); }

Note that the fallback method should have the same signature of a wrapped method and must reside in the same class. Now when the findRatingById fails or gets delayed more than a given threshold, Hystrix fallbacks to findCachedRatingById.

As the Hystrix capabilities are transparently injected as AOP advice, we have to adjust the order in which the advice is stacked, in case if we have other advice like Spring's transactional advice. Here we have adjusted the Spring's transaction AOP advice to have lower precedence than Hystrix AOP advice:

@EnableHystrix @EnableTransactionManagement( order=Ordered.LOWEST_PRECEDENCE, mode=AdviceMode.ASPECTJ) public class RatingServiceApplication { @Bean @Primary @Order(value=Ordered.HIGHEST_PRECEDENCE) public HystrixCommandAspect hystrixAspect() { return new HystrixCommandAspect(); } // other beans, configurations }

Here, we have adjusted the Spring's transaction AOP advice to have lower precedence than Hystrix AOP advice.

5.2. Testing Hystrix Fallback

Now that we have configured the circuit, we can test it by bringing down the H2 database our repository interacts with. But first, let's run the H2 instance as an external process instead of running it as an embedded database.

Let's copy the H2 library (h2-1.4.193.jar) to a known directory and start the H2 server:

>java -cp h2-1.4.193.jar org.h2.tools.Server -tcp TCP server running at tcp://192.168.99.1:9092 (only local connections)

Let's now update our module's data source URL in rating-service.properties to point to this H2 server:

spring.datasource.url = jdbc:h2:tcp://localhost/~/ratings

We can start our services as given in our previous article from the Spring Cloud series, and test ratings of each book by bringing down the external H2 instance we are running.

We could see that when the H2 database is not reachable, Hystrix automatically falls back to Redis to read the ratings for each book. The source code demonstrating this use case can be found here.

6. Using Scopes

Normally a @HytrixCommand annotated method is executed in a thread pool context. But sometimes it needs to be running in a local scope, for example, a @SessionScope or a @RequestScope. This can be done via giving arguments to the command annotation:

@HystrixCommand(fallbackMethod = "getSomeDefault", commandProperties = { @HystrixProperty(name = "execution.isolation.strategy", value = "SEMAPHORE") })

7. The Hystrix Dashboard

A nice optional feature of Hystrix is the ability to monitor its status on a dashboard.

To enable it, we’ll put spring-cloud-starter-hystrix-dashboard and spring-boot-starter-actuator in the pom.xml of our consumer:

 org.springframework.cloud spring-cloud-starter-hystrix-dashboard 1.4.7.RELEASE   org.springframework.boot spring-boot-starter-actuator 2.2.6.RELEASE 

The former needs to be enabled via annotating a @Configuration with @EnableHystrixDashboard and the latter automatically enables the required metrics within our web application.

After we’ve done restarting the application, we’ll point a browser at //localhost:8080/hystrix, input the metrics URL of a Hystrix stream and begin monitoring.

Finally, we should see something like this:

Monitoring a Hystrix stream is something fine, but if we have to watch multiple Hystrix-enabled applications, it will become inconvenient. For this purpose, Spring Cloud provides a tool called Turbine, which can aggregate streams to present in one Hystrix dashboard.

La configuration de Turbine dépasse le cadre de cet article, mais la possibilité doit être mentionnée ici. Il est donc également possible de collecter ces flux via la messagerie, en utilisant le flux Turbine.

8. Conclusion

Comme nous l'avons vu jusqu'à présent, nous sommes maintenant en mesure de mettre en œuvre le modèle de disjoncteur en utilisant Spring Netflix Hystrix avec Spring RestTemplate ou Spring Netflix Feign.

Cela signifie que nous sommes en mesure de consommer des services avec une solution de secours incluse en utilisant les données par défaut, et que nous sommes en mesure de surveiller l'utilisation de ces données.

Comme d'habitude, nous pouvons trouver les sources sur GitHub.