Em uma arquitetura de microsserviços, as aplicações são divididas em pequenos serviços independentes que se comunicam por meio de chamadas remotas. Como essas chamadas trafegam pela rede, qualquer instabilidade no provedor — latência elevada, indisponibilidade temporária ou falhas transitórias — pode se propagar para o consumidor, acumulando requisições pendentes, esgotando threads e provocando uma falha em cascata no sistema.
Para evitar esse cenário, o Netflix criou o Hystrix, uma biblioteca de tolerância a falhas que adiciona camadas de proteção ao redor das dependências remotas. O Spring Cloud Hystrix integra essa biblioteca ao ecossistema Spring, oferecendo recursos como fallback, isolamento por threads e circuit breaker.
- Configuração inicial do projeto
Comece criando um consumidor Spring Boot chamado loja-virtual-gateway. Inclua as dependências do Eureka Client, Ribbon e Hystrix no arquivo pom.xml ou no build.gradle:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
Na classe principal, habilite o Hystrix com a anotação composta @SpringCloudApplication, que equivale a usar @SpringBootApplication, @EnableDiscoveryClient e @EnableCircuitBreaker simultaneamente:
package com.exemplo.loja;
import org.springframework.boot.SpringApplication;
import org.springframework.cloud.client.SpringCloudApplication;
@SpringCloudApplication
public class LojaVirtualGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(LojaVirtualGatewayApplication.class, args);
}
}
Configure o registro no Eureka e a porta do serviço no arquivo bootstrap.yml:
spring:
application:
name: loja-virtual-gateway
server:
port: 8082
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
- Expondo um RestTemplate balanceado por Ribbon
Registre um RestTemplate anotado com @LoadBalanced para que o Ribbon resolva o destino pelo nome registrado no Eureka:
package com.exemplo.loja.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class ClienteHttpConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
- Implementando fallback com @HystrixCommand
O fallback é executado quando a chamada remota excede o tempo limite, lança exceção ou retorna erro. Crie uma classe de serviço que consome o endpoint /produtos de outro microsserviço e define um método alternativo:
package com.exemplo.loja.service;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class CatalogoProdutosService {
private final RestTemplate restTemplate;
public CatalogoProdutosService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@HystrixCommand(fallbackMethod = "respostaPadrao")
public String listarProdutos() {
return restTemplate.getForObject(
"http://catalogo-produtos/produtos",
String.class);
}
public String respostaPadrao() {
return "[{\"id\":0,\"nome\":\"Produto temporariamente indisponível\",\"preco\":0.0}]";
}
}
Em seguida, exponha o serviço por meio de um controller:
package com.exemplo.loja.web;
import com.exemplo.loja.service.CatalogoProdutosService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LojaController {
private final CatalogoProdutosService catalogoService;
public LojaController(CatalogoProdutosService catalogoService) {
this.catalogoService = catalogoService;
}
@GetMapping("/loja/produtos")
public String produtos() {
return catalogoService.listarProdutos();
}
}
- Simulando uma falha para testar o fallback
No microsserviço provedor catalogo-produtos, adicione uma latência artificial no endpoint para forçar o timeout do Hystrix:
package com.exemplo.catalogo.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.Map;
@RestController
public class ProdutosController {
@GetMapping("/produtos")
public Map<String, Object> listar() throws InterruptedException {
Thread.sleep(6000);
return Collections.singletonMap("produtos", "lista completa");
}
}
Com o timeout padrão de um segundo, a requisição para http://localhost:8082/loja/produtos falhará no consumidor e o Hystrix retornará o JSON padrão definido no método respostaPadrao, preservando a experiência do cliente mesmo com o catálogo lento.
- Isolamento de dependências
Quando um método é anotado com @HystrixCommand, o Hystrix envolve a chamada em um comando independente, alocando um conjunto separado de threads para cada dependência externa. Isso impede que uma dependência lenta consuma todos os recursos do consumidor.
É possível personalizar o tamanho do pool de threads e o timeout do comando:
@HystrixCommand(
fallbackMethod = "respostaPadrao",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
},
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "8"),
@HystrixProperty(name = "maxQueueSize", value = "20")
}
)
public String listarProdutos() {
return restTemplate.getForObject("http://catalogo-produtos/produtos", String.class);
}
- Funcionamento do circuit breaker
O circuit breaker monitora as execuções de cada comando dentro de uma janela de tempo. Quando a taxa de falhas ultrapassa um limite configurado, o disjuntor abre e passa a rejeitar novas chamadas ao serviço problemático, executando imediatamente o fallback. Após um intervalo de descanso, ele entra em estado semiaberto e permite uma requisição de teste. Se essa requisição for bem-sucedida, o circuito volta ao estado fechado; caso contrário, permanece aberto.
Os três parâmetros principais são:
- Janela de estatísticas: intervalo em que as falhas são contadas. Padrão de 10 segundos.
- Volume mínimo de requisições: quantidade mínima de chamadas necessárias para que o circuit breaker avalie a abertura. Padrão de 20.
- Limite percentual de erro: porcentagem de falhas que dispara a abertura do circuito. Padrão de 50%.
Configure esses valores diretamente na anotação:
@HystrixCommand(
fallbackMethod = "respostaPadrao",
commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "15"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "4000"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "40")
}
)
public String listarProdutos() {
return restTemplate.getForObject("http://catalogo-produtos/produtos", String.class);
}
- Por que fallback e circuit breaker funcionam juntos
O fallback sozinho resolve falhas individuais, mas ainda assim cada requisição aguarda o timeout antes de retornar a resposta alternativa. Sob alta carga, esse tempo de espera acumualdo pode saturar o consumidor. O circuit breaker elimina essa espera ao bloquear imediatamente as chamadas para a dependência com falha, reduzindo a latência e permitindo a recuperação automática quando o serviço voltar a responder.