Guide de Resilience4j

1. Vue d'ensemble

Dans ce didacticiel, nous parlerons de la bibliothèque Resilience4j.

La bibliothèque aide à mettre en œuvre des systèmes résilients en gérant la tolérance aux pannes pour les communications à distance.

La bibliothèque est inspirée d'Hystrix mais offre une API beaucoup plus pratique et un certain nombre d'autres fonctionnalités comme Rate Limiter (bloquer les requêtes trop fréquentes), Bulkhead (éviter trop de requêtes simultanées) etc.

2. Configuration de Maven

Pour commencer, nous devons ajouter les modules cibles à notre pom.xml (par exemple, nous ajoutons ici le disjoncteur ) :

 io.github.resilience4j resilience4j-circuitbreaker 0.12.1 

Ici, nous utilisons le module disjoncteur . Tous les modules et leurs dernières versions sont disponibles sur Maven Central.

Dans les sections suivantes, nous passerons en revue les modules les plus couramment utilisés de la bibliothèque.

3. Disjoncteur

Notez que pour ce module, nous avons besoin de la dépendance resilience4j-circuitbreaker indiquée ci-dessus.

Le modèle de disjoncteur nous aide à éviter une cascade de pannes lorsqu'un service distant est en panne.

Après un certain nombre de tentatives infructueuses, nous pouvons considérer que le service est indisponible / surchargé et rejeter avec empressement toutes les demandes ultérieures . De cette manière, nous pouvons économiser les ressources système pour les appels susceptibles d'échouer.

Voyons comment nous pouvons y parvenir avec Resilience4j.

Tout d'abord, nous devons définir les paramètres à utiliser. Le moyen le plus simple consiste à utiliser les paramètres par défaut:

CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults();

Il est également possible d'utiliser des paramètres personnalisés:

CircuitBreakerConfig config = CircuitBreakerConfig.custom() .failureRateThreshold(20) .ringBufferSizeInClosedState(5) .build();

Ici, nous avons fixé le seuil de taux à 20% et un nombre minimum de 5 tentatives d'appel.

Ensuite, nous créons un objet CircuitBreaker et appelons le service distant via celui-ci:

interface RemoteService { int process(int i); } CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config); CircuitBreaker circuitBreaker = registry.circuitBreaker("my"); Function decorated = CircuitBreaker .decorateFunction(circuitBreaker, service::process);

Enfin, voyons comment cela fonctionne grâce à un test JUnit.

Nous tenterons d'appeler le service 10 fois. Nous devrions pouvoir vérifier que l'appel a été tenté au moins 5 fois, puis arrêté dès que 20% des appels ont échoué:

when(service.process(any(Integer.class))).thenThrow(new RuntimeException()); for (int i = 0; i < 10; i++) { try { decorated.apply(i); } catch (Exception ignore) {} } verify(service, times(5)).process(any(Integer.class));

3.1. Circuit Breaker États et paramètres

Un CircuitBreaker peut être dans l'un des trois états:

  • FERMÉ - tout va bien, aucun court-circuit n'est impliqué
  • OPEN - le serveur distant est en panne, toutes les requêtes qui lui sont adressées sont court-circuitées
  • HALF_OPEN - un laps de temps configuré depuis l'entrée dans l'état OPEN s'est écoulé et CircuitBreaker permet aux demandes de vérifier si le service distant est de nouveau en ligne

Nous pouvons configurer les paramètres suivants:

  • le seuil de taux de défaillance au-dessus duquel le CircuitBreaker s'ouvre et commence à court-circuiter les appels
  • la durée d'attente qui définit la durée pendant laquelle le CircuitBreaker doit rester ouvert avant de passer à mi-ouverture
  • la taille du tampon en anneau lorsque le CircuitBreaker est à moitié ouvert ou fermé
  • un CircuitBreakerEventListener personnalisé qui gère les événements CircuitBreaker
  • un prédicat personnalisé qui évalue si une exception doit compter comme un échec et ainsi augmenter le taux d'échec

4. Limiteur de débit

Semblable à la section précédente, cette fonctionnalité nécessite la dépendance resilience4j-ratelimiter .

Comme son nom l'indique, cette fonctionnalité permet de limiter l'accès à certains services . Son API est très similaire à celle de CircuitBreaker - il existe des classes Registry , Config et Limiter .

Voici un exemple de son apparence:

RateLimiterConfig config = RateLimiterConfig.custom().limitForPeriod(2).build(); RateLimiterRegistry registry = RateLimiterRegistry.of(config); RateLimiter rateLimiter = registry.rateLimiter("my"); Function decorated = RateLimiter.decorateFunction(rateLimiter, service::process);

Maintenant, tous les appels sur le bloc de service décoré si nécessaire pour se conformer à la configuration du limiteur de débit.

Nous pouvons configurer des paramètres tels que:

  • la période de rafraîchissement de la limite
  • la limite d'autorisations pour la période d'actualisation
  • l'attente par défaut pour la durée de l'autorisation

5. Cloison

Ici, nous aurons d'abord besoin de la dépendance resilience4j-bulkhead .

Il est possible de limiter le nombre d'appels simultanés à un service particulier.

Voyons un exemple d'utilisation de l'API Bulkhead pour configurer un nombre maximal d'appels simultanés:

BulkheadConfig config = BulkheadConfig.custom().maxConcurrentCalls(1).build(); BulkheadRegistry registry = BulkheadRegistry.of(config); Bulkhead bulkhead = registry.bulkhead("my"); Function decorated = Bulkhead.decorateFunction(bulkhead, service::process);

Pour tester cette configuration, nous appellerons la méthode d'un service simulé.

Ensuite, nous nous assurons que Bulkhead n'autorise aucun autre appel:

CountDownLatch latch = new CountDownLatch(1); when(service.process(anyInt())).thenAnswer(invocation -> { latch.countDown(); Thread.currentThread().join(); return null; }); ForkJoinTask task = ForkJoinPool.commonPool().submit(() -> { try { decorated.apply(1); } finally { bulkhead.onComplete(); } }); latch.await(); assertThat(bulkhead.isCallPermitted()).isFalse();

Nous pouvons configurer les paramètres suivants:

  • le nombre maximum d'exécutions parallèles autorisées par la cloison
  • la durée maximale qu'un thread attendra lors de la tentative d'entrer dans une cloison saturée

6. Réessayer

Pour cette fonctionnalité, nous devons ajouter la bibliothèque resilience4j-retry au projet.

Nous pouvons réessayer automatiquement un appel ayant échoué à l'aide de l'API Retry:

RetryConfig config = RetryConfig.custom().maxAttempts(2).build(); RetryRegistry registry = RetryRegistry.of(config); Retry retry = registry.retry("my"); Function decorated = Retry.decorateFunction(retry, (Integer s) -> { service.process(s); return null; });

Maintenant, émulons une situation dans laquelle une exception est levée lors d'un appel de service distant et nous assurons que la bibliothèque réessaye automatiquement l'appel ayant échoué:

when(service.process(anyInt())).thenThrow(new RuntimeException()); try { decorated.apply(1); fail("Expected an exception to be thrown if all retries failed"); } catch (Exception e) { verify(service, times(2)).process(any(Integer.class)); }

We can also configure the following:

  • the max attempts number
  • the wait duration before retries
  • a custom function to modify the waiting interval after a failure
  • a custom Predicate which evaluates if an exception should result in retrying the call

7. Cache

The Cache module requires the resilience4j-cache dependency.

The initialization looks slightly different than the other modules:

javax.cache.Cache cache = ...; // Use appropriate cache here Cache cacheContext = Cache.of(cache); Function decorated = Cache.decorateSupplier(cacheContext, () -> service.process(1));

Here the caching is done by the JSR-107 Cache implementation used and Resilience4j provides a way to apply it.

Note that there is no API for decorating functions (like Cache.decorateFunction(Function)), the API only supports Supplier and Callable types.

8. TimeLimiter

For this module, we have to add the resilience4j-timelimiter dependency.

It's possible to limit the amount of time spent calling a remote service using the TimeLimiter.

To demonstrate, let's set up a TimeLimiter with a configured timeout of 1 millisecond:

long ttl = 1; TimeLimiterConfig config = TimeLimiterConfig.custom().timeoutDuration(Duration.ofMillis(ttl)).build(); TimeLimiter timeLimiter = TimeLimiter.of(config);

Next, let's verify that Resilience4j calls Future.get() with the expected timeout:

Future futureMock = mock(Future.class); Callable restrictedCall = TimeLimiter.decorateFutureSupplier(timeLimiter, () -> futureMock); restrictedCall.call(); verify(futureMock).get(ttl, TimeUnit.MILLISECONDS);

We can also combine it with CircuitBreaker:

Callable chainedCallable = CircuitBreaker.decorateCallable(circuitBreaker, restrictedCall);

9. Add-on Modules

Resilience4j also offers a number of add-on modules which ease its integration with popular frameworks and libraries.

Some of the more well-known integrations are:

  • Spring Boot – resilience4j-spring-boot module
  • Ratpack - module resilience4j-ratpack
  • Rénovation - module résilience4j-retrofit
  • Vertx - module resilience4j-vertx
  • Dropwizard - module resilience4j- metrics
  • Prometheus - module resilience4j-prometheus

10. Conclusion

Dans cet article, nous avons examiné différents aspects de la bibliothèque Resilience4j et appris à l'utiliser pour résoudre divers problèmes de tolérance aux pannes dans les communications inter-serveurs.

Comme toujours, le code source des exemples ci-dessus se trouve à l'adresse over sur GitHub.