BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos O futuro do projeto Hystrix do Spring Cloud

O futuro do projeto Hystrix do Spring Cloud

Pontos Principais

  • O Spring Cloud Hystrix está depreciado. Portanto, novas aplicações não devem utilizar esse projeto;

  • O Resilience4j é uma nova opção para desenvolvedores Spring implementarem o pattern Circuit Breaker;

  • O Resilience4j traz outras funcionalidades como o Rate Limiter, Retry e Bulkhead, além do pattern Circuit Breaker;

  • O Resilience4j trabalha bem com o spring boot e com ferramentas de monitoramento, permitindo a emissão de métricas;

  • Não há uma nova ferramenta para substituir o Hystrix Dashboard, então os usuários precisam utilizar o Prometheus ou o NewRelic para o monitoramento.

O projeto Hystrix do Spring Cloud foi desenvolvido como uma camada em cima do projeto Hystrix da Netflix. Desde então, tem sido adotado por muitas empresas e desenvolvedores como a implementação do pattern de Circuit Breaker.

Em novembro de 2018, a Netflix anunciou que colocaria o projeto em modo de manutenção, o que fez com que o Spring Cloud fizesse o mesmo. Desde então, não foram feitas melhorias no projeto. No SpringOne 2019, houve o anúncio de que o Hystrix Dashboard seria removido do Spring Cloud 3.1, o que fez o projeto ser oficialmente declarado como morto.

Como o conceito do Circuit Breaker foi amplamente disseminado, muitos desenvolvedores o utilizam ou desejam utilizar, mas agora necessitam de um substituto. O Resilience4j foi introduzido para preencher esse vazio e fornecer um caminho de migração para os usuários Hystrix.

Resilience4j

O Resilience4j foi inspirado no Hystrix da Netflix, porém, foi desenvolvido em Java 8 e utilizando programação funcional. É bem leve, pois tem apenas a biblioteca Vavr como dependência, já o Hystrix tem como dependência o Archaius, que possui várias outras dependências externas, como o Guava e o Apache Commons.

Uma nova biblioteca sempre terá vantagem quando comparada com uma biblioteca mais antiga, já que a novata pode aprender com os erros do seu antecessor. Além disso, o Resilience4j vem com diversas funcionalidades novas:

Circuit Breaker

Quando um serviço invoca outro serviço, sempre há a possibilidade de que o serviço externo não esteja executando ou a latência esteja muito alta. Isso pode levar à exaustão do número de threads, pois as mesmas estarão esperando outras requisições terminarem. O pattern de Circuit Breaker funciona de maneira similar a um Circuit Breaker elétrico:

  • Quando um número de falhas consecutivas ultrapassa determinado limite, o Circuit Breaker se abre;
  • Durante certo tempo, todas as requisições invocando este serviço remoto irão falhar imediatamente;
  • Após este período, o Circuit Breaker permite que um determinado número de requisições de testes passem;
  • Se estas requisições de testes terminarem com sucesso, o Circuit Breaker se fecha e volta a operar normalmente;
  • Caso contrário, se continuar havendo falhas, o período sem requisições ao serviço externo é reiniciado.

Rate Limiter

O Rate Limiter garante que um serviço só aceite determinado número de requisições durante uma janela de tempo, garantindo que os recursos sejam utilizados de acordo com os limites desejados e que não sejam utilizados até a sua exaustão.

Retry

O Retry permite que uma aplicação trate falhas momentâneas quando fizerem chamadas para serviços externos, garantindo que retentativas sejam feitas por um certo número de vezes. Caso não obtenham sucesso após todas as retentativas, a chamada ao método falha e a resposta deve ser tratada normalmente pela aplicação.

Bulkhead

O Bulkhead garante que a falha em uma parte do sistema não cause o falha no sistema todo. Ele controla o número de chamadas concorrentes que um componente pode ter. Dessa maneira, o número de recursos esperando resposta do componente é limitado. Há dois tipos de implementações do bulkhead:

  • O Isolamento por semáforo: limita o número de chamadas concorrentes ao serviço, rejeitando imediatamente outras chamadas assim que o limite é alcançado;
  • O isolamento por thread pool: utiliza um thread pool para separar o serviço dos consumidores e limita cada consumidor a um subgrupo dos recursos do sistema.

O abordagem por thread pool também provê uma fila de espera, rejeitando requisições apenas quando o pool e a fila estão cheias. O gerenciamento da thread pool adiciona um pouco desobrecarga, o que diminui um pouco a performance quando comparado ao uso de semáforos, mas permite que threads fiquem suspensas até expirar, caso não sejam executadas.

Construindo uma aplicação Spring Boot com o Resilience4j

Neste artigo, serão construídos dois serviços: o serviço de gerenciamento de livros e, o serviço de gerenciamento de bibliotecas.

Neste sistema, o serviço de bibliotecas chama o serviço de livros. Será necessário subir e derrubar o serviço de livros para simular diferentes cenários para o Circuit Breaker, Rate Limiter, Retry e Bulkhead.

Pré-requisitos

  • JDK 8;
  • Spring Boot 2.1.x;
  • Resilience4j 1.1.x (a última versão do resilience4j é a 1.3 mas o resilience4j-spring-boot2 utiliza a versão 1.1.x);
  • IDE;
  • Gradle;
  • NewRelic APM (Ou Prometheus com Grafana).

Serviço de Gerenciamento de Livros

1. Dependências Gradle

Este serviço é uma API REST simples e precisa dos jars padrões do spring boot. Também vamos habilitar o swagger para testar a API:  

dependencies {
    //REST
    implementation 'org.springframework.boot:spring-boot-starter-web'
    //swagger
    compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
    implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

2. Configuração

O arquivo de configuração possui apenas uma porta a ser configurada:  

server:
    port: 8083

3. Implementação do Serviço

O serviço possui dois métodos, addBook e retrieveBookList. Para a demo, será utilizado uma instância de ArrayList para armazenar as informações dos livros:    

@Service
public class BookServiceImpl implements BookService {
    List<Book> bookList = new ArrayList<>();
    @Override
    public String addBook(Book book) {
        String message  =   "";
        boolean status  =   bookList.add(book);
        if(status){
            message = "O livro foi adicionado na biblioteca.";
        }
        else{
             message = "O livro não pôde ser adicionado na biblioteca devido a problemas técnicos. Por favor, tente mais tarde!";
        }
        return message;
    }

    @Override
    public List<Book> retrieveBookList() {
        return bookList;
    }
}

4. Controller

O Rest Controller expõe duas APIs: Um POST para adicionar um livro e, um GET para buscar os dados dos livros: 

@RestController
@RequestMapping("/books")
public class BookController {

    @Autowired
    private BookService bookService  ;

    @PostMapping
    public String addBook(@RequestBody Book book){
        return bookService.addBook(book);
    }

    @GetMapping
    public List<Book> retrieveBookList(){
        return bookService.retrieveBookList();
    }
}

5. Testes do serviço de gerenciamento de livros

Para construir e iniciar a aplicação, precisamos utilizar os comandos abaixo:  

// construir a aplicação
gradlew build

// inicia a aplicação
java -jar build/libs/bookmanangement-0.0.1-SNAPSHOT.jar

// url do serviço
http://localhost:8083/books

Agora podemos testar a aplicação utilizando a interface de usuário do Swagger: http://localhost:8083/swagger-ui.html

Garanta que o serviço esteja executando antes de iniciar o desenvolvimento do serviço de gerenciamento de bibliotecas.

Serviço de gerenciamento de bibliotecas

Neste serviço, vamos habilitar todas as features do Resilience4j.

1. Dependências Gradle

Este serviço também é uma API REST simples que necessita das dependências padrões do spring boot. Para habilitar o Circuit Breaker e as demais features do resilience4, iremos adicionar outras dependências como o resilience4j-spring-boot2, o spring-boot-starter-actuator, e o spring-boot-starter-aop. Além disso, necessitamos adicionar as dependências para habilitar o monitoramento (micrometer-registry-prometheus, micrometer-registry-new-relic). Por fim, será necessário habilitar o swagger para os testes da API: 

dependencies {
    
    compile 'org.springframework.boot:spring-boot-starter-web'
    
    //resilience
    compile "io.github.resilience4j:resilience4j-spring-boot2:${resilience4jVersion}"
    compile 'org.springframework.boot:spring-boot-starter-actuator'
    compile('org.springframework.boot:spring-boot-starter-aop')
 
    //swagger
    compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
    implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'

    // monitoring
        compile "io.micrometer:micrometer-registry-prometheus:${resilience4jVersion}"
      compile 'io.micrometer:micrometer-registry-new-relic:latest.release'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

2. Configuração

Aqui, necessitamos adicionar algumas configurações:

Por padrão, as APIs do actuator do Circuit Breaker e do Rate Limiter estão desabilitadas no spring 2.1.x. Por isso, necessitamos habilitá-las utilizando as propriedades de gerenciamento. As referências a essas propriedades estão nos links compartilhados no final deste artigo.

Configurar a chave da API do NewRelic Insight e o id da conta:    

management:
   metrics:
    export:
      newrelic:
        api-key: xxxxxxxxxxxxxxxxxxxxx
        account-id: xxxxx
        step: 1m

Configurar as propriedades do Circuit Breaker para as APIs de "add" e "get":    

resilience4j.circuitbreaker:
  instances:
    add:
      registerHealthIndicator: true
      ringBufferSizeInClosedState: 5
      ringBufferSizeInHalfOpenState: 3
      waitDurationInOpenState: 10s
      failureRateThreshold: 50
      recordExceptions:
        - org.springframework.web.client.HttpServerErrorException
        - java.io.IOException
        - java.util.concurrent.TimeoutException
        - org.springframework.web.client.ResourceAccessException
        - org.springframework.web.client.HttpClientErrorException
      ignoreExceptions:

Configurar as propriedades do Rate Limiter para o serviço "add" da API:  

resilience4j.ratelimiter:
  instances:
    add:
      limitForPeriod: 5
      limitRefreshPeriod: 100000
          timeoutDuration: 1000ms

Configurar as propriedades do Retry para o serviço "get" da API:  

resilience4j.retry:
  instances:
    get:
      maxRetryAttempts: 3
      waitDuration: 5000

Configurar as propriedades do Bulkhead para o serviço "get" da API:  

resilience4j.bulkhead:
  instances:
    get:
      maxConcurrentCall: 10
      maxWaitDuration: 10ms

Agora, vamos criar a classe LibraryConfig para definir um bean de RestTemplate que irá efetuar as chamadas ao serviço de gerenciamento de livros. Deixaremos hardcoded a URL do endpoint do serviço de gerenciamento de livros. Não é uma boa ideia para aplicações em produção, mas é o suficiente para os propósitos desta demo, que é demonstrar as funcionalidades do resilience4j. Para uma aplicação em produção, é melhor utilizar um serviço de service discovery.  

@Configuration
public class LibraryConfig {
    Logger logger = LoggerFactory.getLogger(LibrarymanagementServiceImpl.class);
    private static final String baseUrl = "https://bookmanagement-service.apps.np.sdppcf.com";

    @Bean
    RestTemplate restTemplate(RestTemplateBuilder builder) {
        UriTemplateHandler uriTemplateHandler = new RootUriTemplateHandler(baseUrl);
        return builder
                .uriTemplateHandler(uriTemplateHandler)
                .build();
   }
}

3. Serviço

A implementação dos serviços possui métodos anotados com o @CircuitBreak, @RateLimiter, @Retry e @Bulkhead. Todas essas anotações suportam o atributo fallbackMethod e redirecionam a chamada para a função de fallback caso falhas sejam encontradas em cada padrão. Nesse caso, é necessário definir a implementação destes métodos de fallback:

Este método habilita o Circuit Breaker. Se o endpoint /books falhar ao responder, irá chamar o método fallbackForaddBook().    

@Override
    @CircuitBreaker(name = "add", fallbackMethod = "fallbackForaddBook")
    public String addBook(Book book){
        logger.error("Inside addbook call book service. ");
        String response = restTemplate.postForObject("/books", book, String.class);
        return response;
    }

Este método habilita o Rate Limiter. Se o endpoint /books atingir o limite definido nas configurações acima, irá chamar o método fallbackForRatelimitBook().    

@Override
    @RateLimiter(name = "add", fallbackMethod = "fallbackForRatelimitBook")
    public String addBookwithRateLimit(Book book){
        String response = restTemplate.postForObject("/books", book, String.class);
        logger.error("Inside addbook, cause ");
        return response;
    }

Este método habilita o Retry. Se o endpoint /books atingir o limite definido nas configurações acima, irá chamar o método fallbackRetry().  

@Override
    @Retry(name = "get", fallbackMethod = "fallbackRetry")
    public List<Book> getBookList(){
        return restTemplate.getForObject("/books", List.class);
    }

Este método habilita o Bulkhead. Se o endpoint /books atingir o limite definido nas configurações acima, irá chamar o método fallbackBulkhead().    

@Override
    @Bulkhead(name = "get", type = Bulkhead.Type.SEMAPHORE, fallbackMethod = "fallbackBulkhead")
    public List<Book> getBookListBulkhead() {
        logger.error("Inside getBookList bulk head");
        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return restTemplate.getForObject("/books", List.class);
    }

Uma vez que a camada de serviço está definida, necessitamos expor os serviços REST correspondentes de cada um dos métodos para testá-los. Para isso, vamos criar uma classe de RestController.

4. Controller

O RestController expõe quatro APIs:

A primeira é um POST para adicionar livros;

  • A segunda é um POST para adicionar livros, porém será utilizada para testar a funcionalidade de Rate-Limiter;
  • A terceira é um GET para obter os dados dos livros;
  • A quarta é um GET para obter os dados dos livros com a funcionalidade de bulkhead habilitada.  
@RestController
@RequestMapping("/library")
public class LibrarymanagementController {

    @Autowired
    private LibrarymanagementService librarymanagementService;

    @PostMapping
    public String addBook(@RequestBody Book book){
        return librarymanagementService.addBook(book);
    }

    @PostMapping ("/ratelimit")
    public String addBookwithRateLimit(@RequestBody Book book){
        return librarymanagementService.addBookwithRateLimit(book);
    }

    @GetMapping
    public List<Book> getSellersList() {
        return librarymanagementService.getBookList();
    }

    @GetMapping ("/bulkhead")
    public List<Book> getSellersListBulkhead() {
        return librarymanagementService.getBookListBulkhead();
    }
}

Agora o código está pronto. Necessitamos apenas construir e executar o serviço.

5. Construir e testar o serviço de gerenciamento de biblioteca

Para construir e iniciar a aplicação, utilize os comandos abaixo:  

// construir
gradlew build

// inicia a aplicação
java -jar build/libs/librarymanangement-0.0.1-SNAPSHOT.jar

// URL do endpoint
http://localhost:8084/library

Agora podemos testar a aplicação utilizando a interface de usuário do Swagger: http://localhost:8084/swagger-ui.html

Execute os cenários de testes para o Circuit Breaker, Rate Limiter, Retry e Bulkhead

Circuit Breaker - O Circuit Breaker foi aplicado na API de addBook. Para testar se está funcionando, vamos parar o serviço de livros:

  • Primeiro, verifique a saúde da aplicação acessando a URL http://localhost:8084/actuator/heatlh;
  • Agora pare o serviço de livros e acesse a API de addBook do serviço de biblioteca através do Swagger.

No primeiro momento, o circuit breaker deveria estar com o status "CLOSED". Isto é uma métrica do Prometheus que habilitamos ao adicionar a dependência micrometer-registry-prometheus.

Após executar o segundo passo, a chamada ao serviço de livros começará a falhar e ocorrerá o redirecionamento ao serviço de fallback.

Uma vez que o limite é atingido, que no caso é cinco, o circuito irá abrir. E, cada chamada subsequente, irá cair diretamente no método de fallback sem ao menos tentar chamar o serviço de livros. É possível verificar este comportamento observando os logs. Agora, podemos acessar o endpoint /health novamente, que mostrará o estado do Circuit Breaker como sendo "OPEN".  

{
    "status": "DOWN",
    "details": {
        "circuitBreakers": {
            "status": "DOWN",
            "details": {
                "add": {
                    "status": "DOWN",
                    "details": {
                        "failureRate": "100.0%",
                        "failureRateThreshold": "50.0%",
                        "slowCallRate": "-1.0%",
                        "slowCallRateThreshold": "100.0%",
                        "bufferedCalls": 5,
                        "slowCalls": 0,
                        "slowFailedCalls": 0,
                        "failedCalls": 5,
                        "notPermittedCalls": 0,
                        "state": "OPEN"
                    }                        
                }
            }
        }
    }
}

Para criar um dashboard com essas métricas, o código foi implementado no PCF (Pivotal Cloud Foundry) para habilitar a integração com o NewRelic. A dependência micrometer-registry-new-relic foi adicionada justamente para este propósito.


Imagem 2 - Gráfico do NewRelic Insight com o Circuit Breaker Closed

Rate Limiter - We have created separate API (http://localhost:8084/library/ratelimit) having the same addBook functionality but enabled with Rate-Limit feature. In this case, we would need Book Management Service up and running. With the current configuration for the rate limit, we can have a maximum of 5 requests per 10 seconds.

Rate Limiter - Criamos uma API separada (http://localhost:8084/library/ratelimit) com a mesma funcionalidade do addBook porém, com o Rate-Limit habilitado. Neste caso, precisamos do serviço de livros em execução. Com a configuração atual do rate limiter, podemos ter no máximo cinco requests a cada 10 segundos.

Imagem 3 - Configuração do Rate Limiter

Quando acessamos a API cinco vezes dentro de 10 segundos, o limite será atingido e o acesso ao serviço ficará bloqueado. Neste caso, o método de fallback será chamado e responderá de acordo com a lógica implementada. Abaixo, o gráfico mostra que o limite foi atingido três vezes na última hora:


Imagem 4 - Número de vezes que o Rate Limit foi atingido

Retry - O retry permite que a API retente executar uma chamada externa que falhou diversas vezes, até atingir o valor máximo configurado. Se for bem sucedido, o contador irá zerar. Se o limite é alcançado, o método de fallback será chamado. Para emular este comportamento, basta acessar a API de GET (http://localhost:8084/library) quando o serviço de livros não estiver no ar. É possível observar nos logs que o método de fallback é chamado.

Bulkhead - Neste exemplo, foi utilizado a implementação de semáforo do bulkhead. Para emular as chamadas concorrentes é possível utilizar o Jmeter configurado para utilizar 30 threads para realizar as chamadas ao serviço.


Image 5 - Configuração do Jmeter

A API GET habilitada com a anotação @Bulkhead será chamada. Na implementação do serviço, há um trecho de código para fazer a thread ficar inativa por um tempo para que seja possível atingir mais facilmente o número de execuções concorrentes. Podemos observar nos logs que algumas chamadas vão parar no método de fallback. Abaixo, o gráfico com as chamadas concorrentes disponíveis para uma API:

Imagem 6 - Chamadas concorrentes disponíveis de Bulkhead

Sumário

Neste artigo, foi possível observar algumas funcionalidades que agora são quase que obrigatórias em uma arquitetura de microservices. Elas podem ser implementadas utilizando uma única biblioteca, o resilience4j. Utilizando Prometheus com Grafana ou NewRelic, é possível criar dashboards dessas métricas e melhorar a estabilidade dos sistemas.

O código utilizado no artigo pode ser encontrado no Github, spring-boot-resilience4j.

Sobre o autor

Rajesh Bhojwani é um arquiteto de soluções que ajuda times a migrar aplicações on premises para plataformas cloud, como PCF e AWS. Rajesh possui mais de quinze anos de experiência em desenvolvimento de aplicações, design e devops. É evangelista, escritor de artigos técnicos para blogs e é um champion de microservices. Seus artigos podem ser encontrados aqui.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT