Guida alla resilienza4j

1. Panoramica

In questo tutorial parleremo della libreria Resilience4j.

La libreria aiuta a implementare sistemi resilienti gestendo la tolleranza ai guasti per le comunicazioni remote.

La libreria si ispira a Hystrix ma offre un'API molto più conveniente e una serie di altre funzionalità come Rate Limiter (blocca richieste troppo frequenti), Bulkhead (evita troppe richieste simultanee) ecc.

2. Installazione di Maven

Per iniziare, dobbiamo aggiungere i moduli target al nostro pom.xml (ad esempio qui aggiungiamo il Circuit Breaker) :

 io.github.resilience4j resilience4j-circuitbreaker 0.12.1 

Qui stiamo usando il modulo interruttore . Tutti i moduli e le loro ultime versioni possono essere trovati su Maven Central.

Nelle sezioni successive, esamineremo i moduli della libreria più comunemente usati.

3. Interruttore automatico

Nota che per questo modulo abbiamo bisogno della dipendenza resilience4j-circuitbreaker mostrata sopra.

Il modello Circuit Breaker ci aiuta a prevenire una cascata di guasti quando un servizio remoto è inattivo.

Dopo una serie di tentativi falliti, possiamo considerare che il servizio non è disponibile / sovraccarico e rifiutare con entusiasmo tutte le richieste successive ad esso. In questo modo, possiamo risparmiare risorse di sistema per chiamate che potrebbero non riuscire.

Vediamo come possiamo ottenerlo con Resilience4j.

Innanzitutto, dobbiamo definire le impostazioni da utilizzare. Il modo più semplice è utilizzare le impostazioni predefinite:

CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults();

È anche possibile utilizzare parametri personalizzati:

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

Qui, abbiamo impostato la soglia di frequenza al 20% e un numero minimo di 5 tentativi di chiamata.

Quindi, creiamo un oggetto CircuitBreaker e chiamiamo il servizio remoto attraverso di esso:

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

Infine, vediamo come funziona attraverso un test JUnit.

Tenteremo di chiamare il servizio 10 volte. Dovremmo essere in grado di verificare che la chiamata sia stata tentata almeno 5 volte, quindi interrotta non appena il 20% delle chiamate ha avuto esito negativo:

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 Uniti e impostazioni

Un CircuitBreaker può trovarsi in uno dei tre stati:

  • CHIUSO - va tutto bene, nessun cortocircuito coinvolto
  • APERTO : il server remoto è inattivo, tutte le richieste ad esso sono in cortocircuito
  • HALF_OPEN : è trascorso un periodo di tempo configurato dall'ingresso nello stato OPEN e CircuitBreaker consente alle richieste di verificare se il servizio remoto è tornato online

Possiamo configurare le seguenti impostazioni:

  • la soglia di tasso di guasto al di sopra della quale il CircuitBreaker si apre e avvia le chiamate in cortocircuito
  • la durata di attesa che definisce per quanto tempo il CircuitBreaker deve rimanere aperto prima di passare a metà aperto
  • la dimensione del buffer circolare quando CircuitBreaker è semiaperto o chiuso
  • un CircuitBreakerEventListener personalizzato che gestisce gli eventi CircuitBreaker
  • un predicato personalizzato che valuta se un'eccezione deve essere considerata un errore e quindi aumentare il tasso di errore

4. Limitatore di velocità

Simile alla sezione precedente, questa funzionalità richiede la dipendenza resilience4j-ratelimiter .

Come suggerisce il nome, questa funzionalità consente di limitare l'accesso ad alcuni servizi . La sua API è molto simile a CircuitBreaker : ci sono classi Registry , Config e Limiter .

Ecco un esempio di come appare:

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

Ora tutte le chiamate sul blocco di servizi decorato, se necessario, per conformarsi alla configurazione del limitatore di velocità.

Possiamo configurare parametri come:

  • il periodo di aggiornamento del limite
  • il limite di autorizzazioni per il periodo di aggiornamento
  • l'attesa predefinita per la durata dell'autorizzazione

5. Paratia

Qui, avremo prima bisogno della dipendenza resilience4j-bulkhead .

È possibile limitare il numero di chiamate simultanee a un particolare servizio.

Vediamo un esempio di utilizzo dell'API Bulkhead per configurare un numero massimo di chiamate simultanee:

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

Per testare questa configurazione, chiameremo il metodo di un servizio fittizio.

Quindi, ci assicuriamo che Bulkhead non consenta altre chiamate:

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();

Possiamo configurare le seguenti impostazioni:

  • il numero massimo di esecuzioni parallele consentite dalla paratia
  • il tempo massimo che un thread attenderà quando tenta di entrare in una paratia satura

6. Riprova

Per questa funzionalità, dovremo aggiungere la libreria resilience4j-retry al progetto.

Possiamo ritentare automaticamente una chiamata non riuscita utilizzando 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; });

Ora emuliamo una situazione in cui viene generata un'eccezione durante una chiamata al servizio remoto e assicurati che la libreria ritenti automaticamente la chiamata non riuscita:

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 - modulo resilience4j-ratpack
  • Retrofit - modulo resilience4j-retrofit
  • Vertx - modulo resilience4j-vertx
  • Dropwizard - modulo resilience4j-metrics
  • Prometheus - modulo resilience4j-prometheus

10. Conclusione

In questo articolo, abbiamo esaminato diversi aspetti della libreria Resilience4j e abbiamo imparato come utilizzarla per affrontare vari problemi di tolleranza agli errori nelle comunicazioni tra server.

Come sempre, il codice sorgente per gli esempi sopra può essere trovato su GitHub.