Guia de Resilience4j

Guia de Resilience4j

*1. Visão geral *

Neste tutorial, falaremos sobre a biblioteca Resilience4j.

A biblioteca ajuda na implementação de sistemas resilientes, gerenciando a tolerância a falhas para comunicações remotas.

A biblioteca é inspirada em https://www..com/introduction-to-hystrix [Hystrix], mas oferece uma API muito mais conveniente e vários outros recursos, como o limitador de taxa (bloquear solicitações muito frequentes) e o anteparo (evitar muitos solicitações simultâneas) etc.

===* 2. Configuração do Maven *

Para começar, precisamos adicionar os módulos de destino ao nosso pom.xml (por exemplo, aqui adicionamos o disjuntor) _: _

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-circuitbreaker</artifactId>
    <version>0.12.1</version>
</dependency>

Aqui, estamos usando o módulo circuitbreaker . Todos os módulos e suas versões mais recentes podem ser encontrados em Maven Central.

Nas próximas seções, veremos os módulos mais usados ​​da biblioteca.

===* 3. Disjuntor*

Observe que, para este módulo, precisamos da dependência resilience4j-circuitbreaker mostrada acima.

O Padrão de disjuntor nos ajuda a impedir uma cascata de falhas quando um serviço remoto está inoperante.

*Após várias tentativas fracassadas, podemos considerar que o serviço está indisponível/sobrecarregado e rejeitamos ansiosamente todos os pedidos subsequentes* . Dessa forma, podemos economizar recursos do sistema para chamadas que provavelmente falharão.

Vamos ver como podemos conseguir isso com o Resilience4j.

Primeiro, precisamos definir as configurações a serem usadas. A maneira mais simples é usar as configurações padrão:

CircuitBreakerRegistry circuitBreakerRegistry
  = CircuitBreakerRegistry.ofDefaults();

Também é possível usar parâmetros personalizados:

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

Aqui, definimos o limite de taxa para 20% e um número mínimo de 5 tentativas de chamada.

Em seguida, criamos um objeto CircuitBreaker e chamamos o serviço remoto por meio dele:

interface RemoteService {
    int process(int i);
}

CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config);
CircuitBreaker circuitBreaker = registry.circuitBreaker("my");
Function<Integer, Integer> decorated = CircuitBreaker
  .decorateFunction(circuitBreaker, service::process);

Por fim, vamos ver como isso funciona através de um teste JUnit.

Vamos tentar ligar para o serviço 10 vezes. Deveríamos poder verificar se a chamada foi tentada no mínimo 5 vezes e parar assim que 20% das chamadas falharem:

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. Estados e configurações do disjuntor *

Um CircuitBreaker pode estar em um dos três estados:

  • CLOSED - está tudo bem, sem curto-circuito envolvido

  • OPEN - servidor remoto inoperante, todos os pedidos para ele estão em curto-circuito

  • HALF_OPEN - uma quantidade configurada de tempo desde que entrou no estado OPEN passou e CircuitBreaker permite que solicitações verifiquem se o serviço remoto está novamente online

Podemos definir as seguintes configurações:

  • o limite da taxa de falhas acima do qual o CircuitBreaker é aberto e inicia chamadas em curto-circuito

  • a duração da espera, que define por quanto tempo o CircuitBreaker deve permanecer aberto antes de mudar para meio aberto

  • o tamanho do buffer de anel quando o CircuitBreaker está meio aberto ou fechado

  • um CircuitBreakerEventListener personalizado que lida com eventos CircuitBreaker *um Predicado personalizado que avalia se uma exceção deve contar como falha e, portanto, aumentar a taxa de falha

===* 4. Limitador de taxa *

Semelhante à seção anterior, esse recurso requer a dependência resilience4j-ratelimiter.

Como o nome indica,* essa funcionalidade permite limitar o acesso a algum serviço *. Sua API é muito semelhante à CircuitBreaker’s - existem as classes Registry, Config e Limiter.

Aqui está um exemplo de como fica:

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

Agora todas as chamadas no bloco de serviço decorado, se necessário, estão em conformidade com a configuração do limitador de taxa.

Podemos configurar parâmetros como:

  • o período da atualização limite

  • o limite de permissões para o período de atualização *a espera padrão para a duração da permissão

===* 5. Antepara *

Aqui, primeiro precisamos da dependência resilience4j-bulkhead.

É possível* limitar o número de chamadas simultâneas para um serviço específico. *

Vamos ver um exemplo do uso da API Bulkhead para configurar um número máximo de chamadas simultâneas:

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

Para testar essa configuração, chamaremos o método de um serviço simulado.

Em seguida, garantimos que Bulkhead não permita outras chamadas:

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

Podemos definir as seguintes configurações:

  • a quantidade máxima de execuções paralelas permitidas pela antepara *a quantidade máxima de tempo que um encadeamento aguardará ao tentar inserir uma antepara saturada

===* 6. Repetir *

Para esse recurso, precisamos adicionar a biblioteca resilience4j-retry ao projeto.

Podemos* tentar novamente automaticamente uma chamada com falha * usando a API de repetição:

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

Agora vamos emular uma situação em que uma exceção é lançada durante uma chamada de serviço remoto e verifique se a biblioteca tenta novamente novamente a chamada com falha:

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

Também podemos configurar o seguinte:

  • o número máximo de tentativas

  • a duração da espera antes de novas tentativas

  • uma função customizada para modificar o intervalo de espera após uma falha *um Predicado personalizado que avalia se uma exceção deve resultar na repetição da chamada

===* 7. Cache *

O módulo Cache requer a resilience4j-cache dependência.

A inicialização parece um pouco diferente dos outros módulos:

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

Aqui, o cache é feito pela implementação https://www..com/jcache [JSR-107 Cache] usada e o Resilience4j fornece uma maneira de aplicá-lo.

Observe que não há API para funções de decoração (como Cache.decorateFunction (Function) _), a API suporta apenas os tipos _Supplier e Callable.

===* 8. TimeLimiter *

Para este módulo, precisamos adicionar a dependência resilience4j-timelimiter.

É possível* limitar a quantidade de tempo gasto chamando um serviço remoto *usando o TimeLimiter.

Para demonstrar, vamos configurar um TimeLimiter com um tempo limite configurado de 1 milissegundo:

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

Em seguida, vamos verificar se o Resilience4j chama _Future.get () _ com o tempo limite esperado:

Future futureMock = mock(Future.class);
Callable restrictedCall
  = TimeLimiter.decorateFutureSupplier(timeLimiter, () -> futureMock);
restrictedCall.call();

verify(futureMock).get(ttl, TimeUnit.MILLISECONDS);

Também podemos combiná-lo com CircuitBreaker:

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

===* 9. Módulos adicionais *

O Resilience4j também oferece vários módulos complementares que facilitam sua integração com estruturas e bibliotecas populares.

Algumas das integrações mais conhecidas são:

  • Spring Boot - módulo resilience4j-spring-boot

  • Ratpack - módulo resilience4j-ratpack

  • Retrofit - módulo resilience4j-retrofit

  • Vertx - módulo resilience4j-vertx

  • Dropwizard - módulo resilience4j-metrics *Prometheus - módulo resilience4j-prometheus

===* 10. Conclusão*

Neste artigo, passamos por diferentes aspectos da biblioteca Resilience4j e aprendemos como usá-la para resolver várias preocupações de tolerância a falhas nas comunicações entre servidores.

Como sempre, o código-fonte dos exemplos acima pode ser encontrado over no GitHub.