Points Clés
- Le projet Spring Cloud Hystrix est obsolète. Les nouvelles applications ne devraient donc pas utiliser ce projet.
- Resilience4j est une nouvelle option pour les développeurs Spring pour implémenter le modèle de disjoncteur.
- Resilience4j est livré avec d'autres fonctionnalités comme Rate Limiter, Retry et Bulkhead ainsi que le modèle de disjoncteur.
- Resilience4j fonctionne bien avec Spring Boot et à l'aide de bibliothèques de micromètres, il peut émettre des métriques pour la surveillance.
- Il n'y a pas de remplacement introduit par Spring pour Hystrix Dashboard, les utilisateurs doivent donc utiliser prometheus ou NewRelic pour la surveillance.
Le projet Spring Cloud Hystrix a été conçu comme un wrapper au-dessus de la bibliothèque Netflix Hystrix. Depuis lors, il a été adopté par de nombreuses entreprises et développeurs pour mettre en œuvre le pattern disjoncteur (Circuit Breaker).
En novembre 2018, lorsque Netflix a annoncé qu'il mettait ce projet en mode maintenance, il a incité Spring Cloud à annoncer la même chose. Depuis lors, aucune autre amélioration ne se produit dans cette bibliothèque Netflix. Lors de SpringOne 2019, Spring a annoncé que Hystrix Dashboard sera supprimé de la version Spring Cloud 3.1, ce qui le rend officiellement mort.
Comme le pattern disjoncteur a été largement promulgué, de nombreux développeurs l'ont utilisé ou veulent l'utiliser, et ont maintenant besoin d'un remplacement. Resilience4j a été introduit pour combler ce manque et fournir une solution de migration pour les utilisateurs Hystrix.
Resilience4j
Resilience4j a été inspiré par Netflix Hystrix mais est conçu pour Java 8 et la programmation fonctionnelle. Il est léger par rapport à Hystrix car il a la bibliothèque Vavr comme seule dépendance. Netflix Hystrix, en revanche, a une dépendance vers Archaius qui a plusieurs autres dépendances de bibliothèques externes telles que Guava et Apache Commons.
Une nouvelle bibliothèque a toujours un avantage sur une bibliothèque précédente - elle peut apprendre des erreurs de son prédécesseur. Resilience4j est également livré avec de nombreuses nouvelles fonctionnalités :
Disjoncteur (CircuitBreaker)
Lorsqu'un service appelle un autre service, il est toujours possible qu'il soit en panne ou qu'il présente une latence élevée. Cela peut entraîner l'épuisement des threads car ils peuvent attendre la fin des autres requêtes pour arriver à ses fins. Le pattern CircuitBreaker fonctionne de manière similaire à un disjoncteur électrique :
- Lorsqu'un certain nombre de défaillances consécutives franchissent le seuil défini, le disjoncteur se déclenche.
- Pendant la durée du délai d'expiration, toutes les demandes appelant le service distant échouent immédiatement.
- Une fois le délai expiré, le disjoncteur permet à un nombre limité de demandes de test de passer.
- Si ces demandes aboutissent, le disjoncteur reprend son fonctionnement normal.
- Sinon, en cas d'échec, le délai d'expiration recommence.
Limitation de débit (RateLimiter)
Le pattern limitation de débit garantit qu'un service accepte uniquement un nombre maximal défini de demandes au cours d'une fenêtre. Cela garantit que les ressources sous-jacentes sont utilisées conformément à leurs limites et ne s'épuisent pas.
Retentative (Retry)
Le pattern retentative permet à une application de gérer les échecs transitoires lors de l'appel à des services externes. Il garantit la relance des opérations sur les ressources externes un nombre défini de fois. S'il échoue après toutes les tentatives de nouvelle invocation, il doit échouer et la réponse doit être gérée avec élégance par l'application.
Cloisonnement (Bulkhead)
Le pattern cloisonnement garantit que la défaillance d'une partie du système ne provoque pas la panne de l'ensemble du système. Il contrôle le nombre d'appels simultanés qu'un composant peut prendre. De cette façon, le nombre de ressources en attente de la réponse de ce composant est limité. Il existe deux types de mise en œuvre de cloisonnement :
- L'approche d'isolement par sémaphores limite le nombre de demandes simultanées au service. Il rejette les demandes immédiatement une fois la limite atteinte.
- L'approche d'isolement par pool de threads utilise un pool de threads pour séparer le service de l'appelant et le contenir dans un sous-ensemble de ressources système.
L'approche du pool de threads fournit également une file d'attente, ne rejetant les demandes que lorsque le pool et la file d'attente sont pleins. La gestion du pool de threads ajoute une surcharge, ce qui réduit légèrement les performances par rapport à l'utilisation d'un sémaphore, mais permet de suspendre les threads jusqu'à expiration.
Créer une application Spring Boot avec Resilience4j
Dans cet article, nous allons créer 2 services : Book Management pour la gestion des livres et Library Management pour la gestion des bibliothèques.
Dans ce système, Library Management appelle Book Management. Nous devrons démarrer et arrêter le service Book Management pour simuler différents scénarios pour les fonctionnalités CircuitBreaker, RateLimit, Retry et Bulkhead.
Prérequis
- JDK 8
- Spring Boot 2.1.x
- resilience4j 1.1.x (la dernière version de resilience4j est 1.3 mais resilience4j-spring-boot2 a la version 1.1.x uniquement)
- un IDE comme Eclipse, VSC ou IntelliJ (préférez VSC car il est très léger. Je l'aime plus par rapport à Eclipse et IntelliJ)
- Gradle
- L'APM NewRelic (vous pouvez également utiliser Prometheus avec Grafana)
Le service Book Management de gestion des livres
- Les dépendances Gradle
Ce service est une simple API REST et nécessite des starters spring-boot standards pour les dépendances Web et les test. Nous allons également activer swagger pour tester l'API :
dependencies {
//REST
implementation 'org.springframework.boot:spring-boot-starter-web'
//swagger
compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
- La Configuration
La configuration n'a qu'un seul port comme configuration détaillée :
server:
port: 8083
- L'implémentation du service
Il a deux méthodes addBook et retrieveBookList. Juste à des fins de démonstration, nous utilisons un objet ArrayList pour stocker les informations du livre :
@Service
public class BookServiceImpl implements BookService {
List<Book> bookList = new ArrayList<>();
@Override
public String addBook(Book book) {
String message = "";
boolean status = bookList.add(book);
if(status){
message = "Book is added successfully to the library.";
}
else{
message = "Book could not be added in library due to some technical issue. Please try later!";
}
return message;
}
@Override
public List<Book> retrieveBookList() {
return bookList;
}
}
- Le Controller
Le Controller Rest expose deux API - l'une est POST pour ajouter un livre et l'autre GET pour récupérer les informations du livre :
@RestController
@RequestMapping("/books")
public class BookController {
@Autowired
private BookService bookService ;
@PostMapping
public String addBook(@RequestBody Book book){
return bookService.addBook(book);
}
@GetMapping
public List<Book> retrieveBookList(){
return bookService.retrieveBookList();
}
}
- Test du service Book Management
Générez et démarrez l'application à l'aide des commandes ci-dessous :
// construire l'application
gradlew build
// lancer l'application
java -jar build/libs/bookmanangement-0.0.1-SNAPSHOT.jar
// url de l'endpoint
http://localhost:8083/books
Nous pouvons maintenant tester l'application à l'aide de Swagger UI - http://localhost:8083/swagger-ui.html
Assurez-vous que le service est opérationnel avant de passer à la création du service de gestion de bibliothèque Library Management.
Le service Library Management de gestion de bibliothèque
Dans ce service, nous activerons toutes les fonctionnalités de Resilience4j.
- Les dépendances Gradle
Ce service est également une simple API REST et a également besoin de starters spring-boot pour les dépendances Web et de test. Pour activer CircuitBreaker et d'autres fonctionnalités de resilience4j dans l'API, nous avons ajouté quelques autres dépendances comme - resilience4j-spring-boot2, spring-boot-starter-actuator, spring-boot-starter-aop. Nous devons également ajouter des dépendances pour micrometer (micrometer-registry-prometheus, micrometer-registry-new-relic) pour activer les métriques de surveillance. Et enfin, nous activons Swagger pour tester l'API :
dependencies {
compile 'org.springframework.boot:spring-boot-starter-web'
// resilience
compile "io.github.resilience4j:resilience4j-spring-boot2:${resilience4jVersion}"
compile 'org.springframework.boot:spring-boot-starter-actuator'
compile('org.springframework.boot:spring-boot-starter-aop')
// swagger
compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'
// monitoring
compile "io.micrometer:micrometer-registry-prometheus:${resilience4jVersion}"
compile 'io.micrometer:micrometer-registry-new-relic:latest.release'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
- La Configuration
Ici, nous devons faire quelques configurations -
- Par défaut, les API actuator de CircuitBreaker et RateLimiter sont désactivées dans Spring 2.1.x. Nous devons les activer à l'aide des propriétés de gestion. Reportez-vous à ces propriétés dans le lien de code source partagé à la fin de l'article. Nous devons également ajouter les autres propriétés suivantes :
- Configurer la clé d'API NewRelic Insight et l'ID de compte
management:
metrics:
export:
newrelic:
api-key: xxxxxxxxxxxxxxxxxxxxx
account-id: xxxxx
step: 1m
- Configurer les propriétés de resilience4j CircuitBreaker pour les API des services "add" et "get".
resilience4j.circuitbreaker:
instances:
add:
registerHealthIndicator: true
ringBufferSizeInClosedState: 5
ringBufferSizeInHalfOpenState: 3
waitDurationInOpenState: 10s
failureRateThreshold: 50
recordExceptions:
- org.springframework.web.client.HttpServerErrorException
- java.io.IOException
- java.util.concurrent.TimeoutException
- org.springframework.web.client.ResourceAccessException
- org.springframework.web.client.HttpClientErrorException
ignoreExceptions:
- Configurer les propriétés resilience4j RateLimiter pour l'API de service "add".
resilience4j.ratelimiter:
instances:
add:
limitForPeriod: 5
limitRefreshPeriod: 100000
timeoutDuration: 1000ms
- Configurer les propriétés Retry de resilience4j pour l'API de service "get".
resilience4j.retry:
instances:
get:
maxRetryAttempts: 3
waitDuration: 5000
- Configurer les propriétés Bulkhead de résilience4j pour l'API de service "get".
resilience4j.bulkhead:
instances:
get:
maxConcurrentCall: 10
maxWaitDuration: 10ms
Maintenant, nous allons créer une classe LibraryConfig pour définir un bean pour RestTemplate afin de faire un appel au service de gestion de livre. Nous avons également codé en dur l'URL du endpoint du service de gestion des livres. Ce n'est pas une bonne idée pour une application de type production, mais le but de cette démo est uniquement de présenter les fonctionnalités de resilience4j. Pour une application de production, nous souhaitons peut-être utiliser le service de découverte de service (service discovery).
@Configuration
public class LibraryConfig {
Logger logger = LoggerFactory.getLogger(LibrarymanagementServiceImpl.class);
private static final String baseUrl = "https://bookmanagement-service.apps.np.sdppcf.com";
@Bean
RestTemplate restTemplate(RestTemplateBuilder builder) {
UriTemplateHandler uriTemplateHandler = new RootUriTemplateHandler(baseUrl);
return builder
.uriTemplateHandler(uriTemplateHandler)
.build();
}
}
- Le service
L'implémentation du service a des méthodes qui sont marquées avec les annotations @CircuitBreaker, @RateLimiter, @Retry et @Bulkhead. Toutes ces annotations possèdent l'attribut fallbackMethod et redirigent l'appel vers les fonctions de fallback en cas de pannes observées par chaque pattern. Nous avons besoin de définir l'implémentation de ces méthodes de fallback :
Cette méthode a été marquée avec l'annotation CircuitBreaker. Donc, si l'endpoint /books ne retourne pas la réponse, il va appeler la méthode fallbackForaddBook().
@Override
@CircuitBreaker(name = "add", fallbackMethod = "fallbackForaddBook")
public String addBook(Book book){
logger.error("Inside addbook call book service. ");
String response = restTemplate.postForObject("/books", book, String.class);
return response;
}
Cette méthode a été marquée avec l'annotation RateLimiter. Si l'endpoint /books atteint le seuil défini dans la configuration ci-dessus, il appellera la méthode fallbackForRatelimitBook().
@Override
@RateLimiter(name = "add", fallbackMethod = "fallbackForRatelimitBook")
public String addBookwithRateLimit(Book book){
String response = restTemplate.postForObject("/books", book, String.class);
logger.error("Inside addbook, cause ");
return response;
}
Cette méthode a été marquée avec l'annotation Retry. Si l'endpoint /books atteint le seuil défini dans la configuration ci-dessus, il appellera la méthode fallbackRetry().
@Override
@Retry(name = "get", fallbackMethod = "fallbackRetry")
public List<Book> getBookList(){
return restTemplate.getForObject("/books", List.class);
}
Cette méthode a été marquée avec l'annotation Bulkhead. Si l'endpoint /books atteint le seuil défini dans la configuration ci-dessus, il appellera la méthode fallbackBulkhead().
@Override
@Bulkhead(name = "get", type = Bulkhead.Type.SEMAPHORE, fallbackMethod = "fallbackBulkhead")
public List<Book> getBookListBulkhead() {
logger.error("Inside getBookList bulk head");
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return restTemplate.getForObject("/books", List.class);
}
Une fois la couche de service configurée, nous devons exposer les API REST correspondantes pour chacune des méthodes afin de pouvoir les tester. Pour cela, nous devons créer la classe RestController.
- Controller
La classe RestController a exposé 4 API -
- La première est un POST pour ajouter un livre
- La seconde est à nouveau un POST pour ajouter un livre, mais cela sera utilisé pour faire la démonstration de la fonction Rate-Limit.
- Le troisième est GET pour récupérer les détails du livre.
- Le quatrième est l'API GET pour récupérer les détails du livre, mais activé avec la fonction Bulkhead.
@RestController
@RequestMapping("/library")
public class LibrarymanagementController {
@Autowired
private LibrarymanagementService librarymanagementService;
@PostMapping
public String addBook(@RequestBody Book book){
return librarymanagementService.addBook(book);
}
@PostMapping ("/ratelimit")
public String addBookwithRateLimit(@RequestBody Book book){
return librarymanagementService.addBookwithRateLimit(book);
}
@GetMapping
public List<Book> getSellersList() {
return librarymanagementService.getBookList();
}
@GetMapping ("/bulkhead")
public List<Book> getSellersListBulkhead() {
return librarymanagementService.getBookListBulkhead();
}
}
Maintenant, le code est prêt. Nous devons le construire et l'exécuter.
- Créer et tester le service Library Management
Générez et démarrez l'application à l'aide des commandes ci-dessous:
// Construire l'application
gradlew build
// Lancer l'application
java -jar build/libs/librarymanangement-0.0.1-SNAPSHOT.jar
// URL de l'Endpoint
http://localhost:8084/library
Nous pouvons maintenant tester l'application à l'aide de Swagger UI - http://localhost:8084/swagger-ui.html
Exécution des scénarios de test pour CircuitBreaker, RateLimiter, Retry et Bulkhead
Disjoncteur (CircuitBreaker) - Circuit Breaker a été appliqué à l'API addBook. Pour tester si cela fonctionne, nous arrêterons le service Book Management.
- Tout d'abord, observez la santé de l'application en cliquant sur l'URL http://localhost:8084/actuator/health
- Arrêtez maintenant le service Book Management et appuyez sur l'API addBook du service Library Management à l'aide de l'interface de Swagger UI
À la première étape, il devrait afficher l'état du disjoncteur comme "CLOSED". Il s'agit des métriques Prometheus que nous avons activées via la dépendance vers micrometer.
Après avoir exécuté la seconde étape, il commencera à échouer et à rediriger vers la méthode de fallback.
Une fois qu'il franchit le seuil, qui dans ce cas est de 5, il ouvre le circuit. Et, chaque appel suivant ira directement à la méthode de secours sans tenter d'invoquer le service Book Management. (Vous pouvez le vérifier en accédant aux journaux et en observant l'enregistrement du logger. Maintenant, nous pouvons observer le endpoint /health montrant l'état de CircuitBreaker comme "OPEN".
{
"status": "DOWN",
"details": {
"circuitBreakers": {
"status": "DOWN",
"details": {
"add": {
"status": "DOWN",
"details": {
"failureRate": "100.0%",
"failureRateThreshold": "50.0%",
"slowCallRate": "-1.0%",
"slowCallRateThreshold": "100.0%",
"bufferedCalls": 5,
"slowCalls": 0,
"slowFailedCalls": 0,
"failedCalls": 5,
"notPermittedCalls": 0,
"state": "OPEN"
}
}
}
}
}
}
Nous avons déployé le même code sur PCF (Pivotal Cloud Foundry) afin de pouvoir l'intégrer à NewRelic pour créer le tableau de bord de cette métrique. Nous avons utilisé la dépendance micrometer-registry-new-relic à cette fin.
Image 2 - Graphique NewRelic Insight CircuitBreaker Closed
Limit de débit (Rate Limiter) - Nous avons créé une API distincte (http://localhost:8084/library/ratelimit) ayant la même fonctionnalité addBook mais activée avec la fonction Rate-Limit. Dans ce cas, nous aurions besoin que le service Book Management soit opérationnel. Avec la configuration actuelle de la limite de débit, nous pouvons avoir un maximum de 5 demandes par 10 secondes.
Image 3 - Configuration de la limite de débit
Une fois que nous avons invoqué l'API 5 fois en 10 secondes, elle atteindra le seuil et sera limitée. Pour éviter la limitation, il passera à la méthode de secours et répondra en fonction de la logique implémentée. Le graphique ci-dessous montre qu'il a atteint la limite de seuil 3 fois au cours de la dernière heure :
Image 4 - NewRelic Insight RateLimit Throttling
Retentative (Retry) - La fonctionnalité Retry permet à l'API de réessayer la transaction ayant échoué encore et encore jusqu'à la valeur maximale configurée. S'il réussit, il actualisera le compteur à zéro. S'il atteint le seuil, il le redirigera vers la méthode de secours définie et s'exécutera en conséquence. Pour simuler cela, invoquer l'API GET (http://localhost:8084/library) lorsque le service de gestion des livres est arrêté. Nous observerons dans les logs qu'ils contiennent la réponse de l'implémentation de la méthode de secours.
Cloisonnement (Bulkhead) - Dans cet exemple, nous avons implémenté l'implémentation Semaphore du cloisonnement. Pour simuler des appels simultanés, nous avons utilisé Jmeter et configuré les 30 appels utilisateur dans le groupe de Thread.
Image 5 - Configuration de Jmeter
Nous allons utiliser l'API GET() activée avec l'annotation @Bulkhead. Nous avons également mis un certain temps de sommeil dans cette API afin que nous puissions atteindre la limite d'exécution simultanée. Nous pouvons observer dans les journaux qu'il va à la méthode de secours pour certains des appels de thread. Voici le graphique des appels concurrents disponibles pour une API :
Image 6 - Tableau de bord des appels concurrents disponibles
Résumé
Dans cet article, nous avons vu diverses fonctionnalités qui sont désormais indispensables dans une architecture Microservices, qui peuvent être implémentées à l'aide d'une seule bibliothèque resilience4j. En utilisant Prometheus avec Grafana ou NewRelic, nous pouvons créer des tableaux de bord autour de ces métriques et augmenter la stabilité des systèmes.
Comme d'habitude, le code peut être télécharger sur Github - spring-boot-resilience4j
A propos de l'auteur
Rajesh Bhojwani est un Solution Architect qui aide les équipes à migrer des applications on premise vers des plateformes Cloud telles que PCF et AWS. Il a plus de 15 ans d'expérience dans le développement, la conception et le développement d'applications. Il est évangéliste, blogueur technique et Microservice champion. Ses derniers blogs peuvent être consultés ici.