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.