Resiliência é um aspecto fundamental dos software atuais, especialmente quando se trata de sistemas distribuídos, termo antigo, mas que conhecemos atualmente como microserviços, aplicações nativas para cloud(Cloud native applications) ou ainda aplicações nativas para Kubernetes (K8s native application.

Uma aplicação resiliente mantem provendo seu serviço mesmo na presença de falhas. Erros podem e vão acontecer! Uma “degradação graciosa” deve ser prevista no pior caso, por exemplo: implementar um plano B caso o plano A falhe, ou também conhecido como “fallback”.

Resilience4J é uma biblioteca que implementa os padrões mais comuns de resiliência para aplicações escritas em Java, incluindo limitação de tempo, bulkheads, circuitbreaker, rate limiter, retries, e cache. Neste texto, irei falar sobre como o Resilience4J nos ajuda, oque inclui integração com Spring Boot com auto configuração e métricas para o Spring Actuator.

Eventualmente, você vai precisar simular falhas nos seus testes pra entender e observar como está funcionando sua aplicação e as configurações do Resilience4J. Para isto você vai precisar colocar algo no meio, que tenha poder de interromper a comunicação, provocando falhas controladas, simulando latência ou perda de pacotes de rede. Particularmente recomendo o uso do toxiproxy ou Apache Benchmark.




Configuração

Para o Resilience4J funcionar com Spring Boot basta ir no spring initializr criar o seu projeto incluindo-o como dependência, gere o projeto e renomeie o application.properties para application.yml.



Talvez você precise também da biblioteca io.github.resilience4j:resilience4j-reactor:<versao>, mas no momento que escrevo ela não está sendo necessária.

O código abaixo apesar de funcionar, ele trás alguns problemas, caso o servidor falhe, ele não tem a chance de executar outra consulta ou retornar um valor padrão, ele não tenta novamente realizar a requisição ou usa o valor da consulta anterior.



Para o caso do método falhar, e optarmos por retornar um valor padrão, podemos definir um “fallback”, vamos definir um método que faça isto para nós, veja:


Com este métod nós já poderemos dizer ao Resilience4J que em caso de falha, ele deve utilizar o método fallback(String url, Trowable throwable).



Retry

Quando sua requisição não retorna dentro da janela de tempo, ou retorna com erro você pode usar o padrão “retry” (retentar) para tentar novamente, este é um padrão útil para quando o sistema destino está enfrentando problemas temporários, ou intermitentes. Porém, caso a aplicação destino, aquela que vai receber sua requisição, esteja com mal funcionamento ao ponto de ser incapaz de responder as requisições, talvez por falta de memória ou processador, iniciar imediatamente uma retentativa irá piorar a situação, chamo atenção para este ponto, pois a maioria dos clientes http te oferecem a possíbilidade de realizar a requisição imediatamente após a falha várias vezes de forma configurável, porém sem adicionar nenhum tempo entre cada chamada, oque pode causar um problema conhecido como “starvation” na sua aplicação origem.


Starvation é “Em programação concorrente, ocorre inanição quando um processo nunca é executado, pois processos de prioridade maior sempre o impedem de ser executado. Em um ambiente computacional multitarefa, a execução de diversos processos simultâneos deve seguir uma regra de escalonamento destes para uso do processador.” — Wikipedia

 

Resilience4J oferece uma maneira de utilizar o padrão “Retry” de maneira um pouco mais inteligente, adicionando um tempo de espera exponêncial (fibonacci) entre cada chamada extra — “E você que pensava que nunca iria utilizar aquela aula de algorítimo em que teve de implementar fibonacci em 🤷‍♀️

Veja abaixo um exemplo de configuração com os comentários do que faz cada propriedade configurada, abaixo:



A configuração acima aplica a uma instância de holidayClient a configuração de retentativa que calcula o tempo de espera (delay), iniciando o tempo em 1 segundo, e multiplicando o tempo de espera por 2 a cada nova tentativa. É fibonacci.

Utilizando @Retry você pode aplicar a configuração específica ao método desejado, lhe permitindo ter diversas configurações difentes, uma para cada método ou chamada a serviços. O parâmetro name deve ser o mesmo configurado em “instance” do application.yml, e o parâmetro “fallback” deve ser o nome do método responsável pela estratégia de “fallback”, e caso este falhe então a falha será enviada lançada pra fora.



Por padrão retentar é uma operação que será executada em caso de termos o lançamento de uma Exception, porém, no caso de obtermos um 404 pode ser aceitável e não queremos que a requisição seja automáticamente executada novamente. Refletindo sobre esta situação parece natual que eventualmente precisamos dizer que algumas excessões devem retentar, enquanto outras não.

Tome cuidado com o tipo de operação que está retentando. Está tudo bem retentar operações idempotentes como GET/PUT, mas isto não é recomendado para operações não idempotente como POST/DELETE



Bulkhead

Bulkhead são como partes de um navio, que enchem de agua quando em caso de violação do casco, prevenindo que o barco inteiro afunde por um inundação. Com este padrão, podemos evitar que haja problemas em muitas threads executando requisições simultâneas inundando o sistema destino.



Resilience4J provê o Bulkhead e configuração programática ou através do arquivo application.yml.



A configuração acima, define que para a instância holidayClient pode haver 10 chamadas concorrentes e em paralela sendo executadas. Quando o bulkhead está saturado, a thread é bloqueada por no máximo 100ms antes de desistir da requisição.

Quando você está configurado o Bulkhead através de properties, você pode utilizar a annotation @Bulkhead para aplicar este padrão.




Quando o Bulkhead estiver saturado e o tempo de espera finalizar, o método fallback será executado se estiver presente, caso contrário uma exception será lançada.

Em uma apliação que utiliza o conceito de EventLoop, ou MutiEventLoop deve-se tomar o cuidado para não bloquear o EventLoop. Utilizando Spring Web Flux e Project Reactor o risco de bloquear uma thread é baixo, já que entende-se que o caso de uso para ele é de uma aplicação toda não bloqueante, de ponta a ponta.



Você pode avaliar o número de requisições em um determinado intervalo de tempo. Se você expôe uma API, ter um limite ajuda a protejer sua aplicação de uma inundação de requisições. Você pode limitar o uso de uma API para evitar um DDOS interno, entre suas APIs ou limitar o uso de uma API que seja cobrada no modelo de pagar por uso (pay-per-use).

Resilience4J oferece este controle, aplicando limitações de quantas requisições são realmente efetivadas em um intervalo de tempo.



No exemplo acima, a configuração define que a instância holidayClient, para cada período apenas 10 requisições serão processadas. A cada 1s o RateLimit será reiniciado e será permitido outras novas 10 requisições e cada requisição esperará por 500ms para poder executar caso contrário ela será rejeitada. Em outras palavras, pode ser disparada 10 requisições por segundo.




Cache

Se você procura cache das respostas anteriores, algo como um memoize, ou cache de método, é melhor você não utilizar o cache provido pelo Resilience4J, pois como explica o autor nesta issue 815 o propósito dele é se protejer de problemas conhecidos nas bibliotecas JCache e Hazelcast. Note que final da documentação de Cache do Resilience4J, tem o seguinte ponto de atenção:

Em uma aplicação você já deve ter previsto cache na instância e cache distribuído (Redis), é melhor você utilizar estes como sua estratégia de cache, ou cache na instância local, até por simplicidade e disceminação de conhecimento da arquitetura e práticas da sua aplicação.



Circuitbreaker

Circuitbreaker tem configurações pré definidas. O melhor pra entender cada uma delas, é dar uma olhada na documentação oficial, veja a tabela em https://resilience4j.readme.io/docs/circuitbreaker e as configurações para uso integrado com Spring em https://resilience4j.readme.io/docs/getting-started-3




Monitoramento do Resilience4J

Utilizando Spring framework, temos a possíbilidade de utilizar o actuator para monitorar o estado do circuitbreaker em tempo de execução mesmo em produção. 

Por padrão, os indicadores do Circuitbreaker e RateLimiter estão desativados mas você pode habilitar via configuração. 


Será mostrado UP quando o circuitbreaker estiver fechado, e DOWN quando estiver aberto e UNKNOWN quando estiver half-open.




Também é possível utilizar monitoramento via Prometheus utilizando a extensão co micrometer. https://resilience4j.readme.io/docs/micrometer



Até o momento em que escrevo, não há como ignorar um ou mais “http status” fazendo com que o circuitbreaker não abra, acredito que essa possa ser uma melhoria interessante, na minha experiência encontrei casos de uso que esta funcionalidade seria bem vinda. 



Módulos do resilience4J

Core:

  • resilience4j-circuitbreaker
  • resilience4j-ratelimiter
  • resilience4j-bulkhead
  • resilience4j-retry
  • resilience4j-cache

Add-on:

  • resilience4j-metrics: Dropwizard Metrics exporter
  • resilience4j-prometheus: Prometheus Metrics exporter
  • resilience4j-spring-boot: Spring Boot Starter
  • resilience4j-ratpack: Ratpack Starter
  • resilience4j-retrofit: Retrofit Call Adapter Factories
  • resilience4j-vertx: Vertx Future decorator
  • resilience4j-consumer: Circular Buffer Event consumer
  • resilience4j-rxjava2: integration of internal event system with rxjava2





Para gerar estas imagens com código utilizei o Carbon


Referências

https://martinfowler.com/bliki/CircuitBreaker.html 

Post a Comment

Postagem Anterior Próxima Postagem