Introdução ao Hystrix
1. Visão geral
Um sistema distribuído típico consiste em muitos serviços colaborando juntos.
Esses serviços são propensos a falhas ou respostas atrasadas. Se um serviço falhar, poderá afetar outros serviços que afetam o desempenho e, possivelmente, tornar outras partes do aplicativo inacessíveis ou, na pior das hipóteses, derrubar o aplicativo inteiro.
Claro, existem soluções disponíveis que ajudam a tornar os aplicativos resilientes e tolerantes a falhas - um desses frameworks é Hystrix.
A biblioteca de estrutura Hystrix ajuda a controlar a interação entre serviços, fornecendo tolerância a falhas e tolerância a latência. Ele melhora a resiliência geral do sistema, isolando os serviços com falha e interrompendo o efeito em cascata das falhas.
Nesta série de postagens, começaremos analisando como o Hystrix é resgatado quando um serviço ou sistema falha e o que o Hystrix pode realizar nessas circunstâncias.
2. Exemplo Simples
A maneira como o Hystrix fornece tolerância a falhas e latência é isolar e agrupar chamadas para serviços remotos.
Neste exemplo simples, envolvemos uma chamada no métodorun() doHystrixCommand:
class CommandHelloWorld extends HystrixCommand {
private String name;
CommandHelloWorld(String name) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.name = name;
}
@Override
protected String run() {
return "Hello " + name + "!";
}
}
e executamos a chamada da seguinte maneira:
@Test
public void givenInputBobAndDefaultSettings_whenCommandExecuted_thenReturnHelloBob(){
assertThat(new CommandHelloWorld("Bob").execute(), equalTo("Hello Bob!"));
}
3. Configuração do Maven
Para usar Hystrix em projetos Maven, precisamos terhystrix-coreerxjava-core dependência da Netflix no projetopom.xml:
com.netflix.hystrix
hystrix-core
1.5.4
A versão mais recente sempre pode ser encontradahere.
com.netflix.rxjava
rxjava-core
0.20.7
A versão mais recente desta biblioteca pode sempre ser encontradahere.
4. Configurando o serviço remoto
Vamos começar simulando um exemplo do mundo real.
In the example below, a classeRemoteServiceTestSimulator representa um serviço em um servidor remoto. Tem um método que responde com uma mensagem após o período de tempo especificado. Podemos imaginar que essa espera é uma simulação de um processo demorado no sistema remoto, resultando em uma resposta atrasada ao serviço de chamada:
class RemoteServiceTestSimulator {
private long wait;
RemoteServiceTestSimulator(long wait) throws InterruptedException {
this.wait = wait;
}
String execute() throws InterruptedException {
Thread.sleep(wait);
return "Success";
}
}
And here is our sample client que chamaRemoteServiceTestSimulator.
A chamada para o serviço é isolada e encapsulada no métodorun() de aHystrixCommand.. É este agrupamento que fornece a resiliência que mencionamos acima:
class RemoteServiceTestCommand extends HystrixCommand {
private RemoteServiceTestSimulator remoteService;
RemoteServiceTestCommand(Setter config, RemoteServiceTestSimulator remoteService) {
super(config);
this.remoteService = remoteService;
}
@Override
protected String run() throws Exception {
return remoteService.execute();
}
}
A chamada é executada chamando o métodoexecute() em uma instância do objetoRemoteServiceTestCommand.
O teste a seguir demonstra como isso é feito:
@Test
public void givenSvcTimeoutOf100AndDefaultSettings_whenRemoteSvcExecuted_thenReturnSuccess()
throws InterruptedException {
HystrixCommand.Setter config = HystrixCommand
.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroup2"));
assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(100)).execute(),
equalTo("Success"));
}
Até agora, vimos como envolver chamadas de serviço remoto no objetoHystrixCommand. Na seção abaixo, vamos ver como lidar com uma situação em que o serviço remoto começa a se deteriorar.
5. Trabalhando com Serviço Remoto e Programação Defensiva
5.1. Programação defensiva com tempo limite
É prática geral de programação definir tempos limite para chamadas para serviços remotos.
Vamos começar observando como definir o tempo limite emHystrixCommand e como isso ajuda em curto-circuito:
@Test
public void givenSvcTimeoutOf5000AndExecTimeoutOf10000_whenRemoteSvcExecuted_thenReturnSuccess()
throws InterruptedException {
HystrixCommand.Setter config = HystrixCommand
.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupTest4"));
HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter();
commandProperties.withExecutionTimeoutInMilliseconds(10_000);
config.andCommandPropertiesDefaults(commandProperties);
assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
equalTo("Success"));
}
No teste acima, estamos atrasando a resposta do serviço definindo o tempo limite para 500 ms. Também estamos configurando o tempo limite de execução emHystrixCommand como 10.000 ms, permitindo tempo suficiente para que o serviço remoto responda.
Agora vamos ver o que acontece quando o tempo limite de execução é menor do que a chamada de tempo limite do serviço:
@Test(expected = HystrixRuntimeException.class)
public void givenSvcTimeoutOf15000AndExecTimeoutOf5000_whenRemoteSvcExecuted_thenExpectHre()
throws InterruptedException {
HystrixCommand.Setter config = HystrixCommand
.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupTest5"));
HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter();
commandProperties.withExecutionTimeoutInMilliseconds(5_000);
config.andCommandPropertiesDefaults(commandProperties);
new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(15_000)).execute();
}
Observe como baixamos a barra e definimos o tempo limite de execução para 5.000 ms.
Esperamos que o serviço responda dentro de 5.000 ms, enquanto configuramos o serviço para responder após 15.000 ms. Se você perceber quando executar o teste, o teste sairá após 5.000 ms em vez de esperar 15.000 ms e lançará umHystrixRuntimeException.
Isso demonstra como Hystrix não espera mais do que o tempo limite configurado por uma resposta. Isso ajuda a tornar o sistema protegido pelo Hystrix mais responsivo.
Nas seções abaixo, examinaremos a configuração do tamanho do conjunto de encadeamentos, o que evita o esgotamento dos encadeamentos e discutiremos seus benefícios.
5.2. Programação defensiva com pool de threads limitado
Definir tempos limite para chamadas de serviço não resolve todos os problemas associados aos serviços remotos.
Quando um serviço remoto começa a responder lentamente, um aplicativo típico continuará a chamar esse serviço remoto.
O aplicativo não sabe se o serviço remoto está íntegro ou não e novos threads são gerados toda vez que uma solicitação chega. Isso fará com que os threads em um servidor já com dificuldades sejam usados.
Não queremos que isso aconteça, pois precisamos desses threads para outras chamadas remotas ou processos em execução em nosso servidor e também queremos evitar o aumento da utilização da CPU.
Vamos ver como definir o tamanho do pool de threads emHystrixCommand:
@Test
public void givenSvcTimeoutOf500AndExecTimeoutOf10000AndThreadPool_whenRemoteSvcExecuted
_thenReturnSuccess() throws InterruptedException {
HystrixCommand.Setter config = HystrixCommand
.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupThreadPool"));
HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter();
commandProperties.withExecutionTimeoutInMilliseconds(10_000);
config.andCommandPropertiesDefaults(commandProperties);
config.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withMaxQueueSize(10)
.withCoreSize(3)
.withQueueSizeRejectionThreshold(10));
assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
equalTo("Success"));
}
No teste acima, estamos configurando o tamanho máximo da fila, o tamanho da fila principal e o tamanho da rejeição da fila. Hystrix começará a rejeitar as solicitações quando o número máximo de threads atingir 10 e a fila de tarefas atingir um tamanho de 10.
O tamanho do núcleo é o número de encadeamentos que sempre permanecem ativos no conjunto de encadeamentos.
5.3. Programação defensiva com padrão de curto-circuito
No entanto, ainda há uma melhoria que podemos fazer nas chamadas de serviço remoto.
Vamos considerar o caso em que o serviço remoto começou a falhar.
Não queremos continuar disparando solicitações e desperdiçar recursos. O ideal seria parar de fazer solicitações por um determinado período de tempo para permitir que o serviço recuperasse antes de retomar as solicitações. Isso é chamado de padrãoShort Circuit Breaker.
Vamos ver como Hystrix implementa esse padrão:
@Test
public void givenCircuitBreakerSetup_whenRemoteSvcCmdExecuted_thenReturnSuccess()
throws InterruptedException {
HystrixCommand.Setter config = HystrixCommand
.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupCircuitBreaker"));
HystrixCommandProperties.Setter properties = HystrixCommandProperties.Setter();
properties.withExecutionTimeoutInMilliseconds(1000);
properties.withCircuitBreakerSleepWindowInMilliseconds(4000);
properties.withExecutionIsolationStrategy
(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD);
properties.withCircuitBreakerEnabled(true);
properties.withCircuitBreakerRequestVolumeThreshold(1);
config.andCommandPropertiesDefaults(properties);
config.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withMaxQueueSize(1)
.withCoreSize(1)
.withQueueSizeRejectionThreshold(1));
assertThat(this.invokeRemoteService(config, 10_000), equalTo(null));
assertThat(this.invokeRemoteService(config, 10_000), equalTo(null));
assertThat(this.invokeRemoteService(config, 10_000), equalTo(null));
Thread.sleep(5000);
assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
equalTo("Success"));
assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
equalTo("Success"));
assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
equalTo("Success"));
}
public String invokeRemoteService(HystrixCommand.Setter config, int timeout)
throws InterruptedException {
String response = null;
try {
response = new RemoteServiceTestCommand(config,
new RemoteServiceTestSimulator(timeout)).execute();
} catch (HystrixRuntimeException ex) {
System.out.println("ex = " + ex);
}
return response;
}
No teste acima, definimos diferentes propriedades do disjuntor. Os mais importantes são:
-
OCircuitBreakerSleepWindow que é definido como 4.000 ms. Isso configura a janela do disjuntor e define o intervalo de tempo após o qual a solicitação ao serviço remoto será retomada.
-
OCircuitBreakerRequestVolumeThreshold que é definido como 1 e define o número mínimo de solicitações necessárias antes que a taxa de falha seja considerada
Com as configurações acima em vigor, nossoHystrixCommand agora será aberto após duas solicitações com falha. A terceira solicitação nem chegará ao serviço remoto, embora tenhamos definido o atraso do serviço em 500 ms,Hystrix entrará em curto-circuito e nosso método retornaránull como resposta.
Em seguida, adicionaremos umThread.sleep(5000) para cruzar o limite da janela de sono que definimos. Isso fará com queHystrix feche o circuito e as solicitações subsequentes serão transmitidas com sucesso.
6. Conclusão
Em resumo, o Hystrix foi projetado para:
-
Fornecer proteção e controle sobre falhas e latência de serviços normalmente acessados pela rede
-
Parar a cascata de falhas resultantes de alguns serviços estarem inativos
-
Falhe rápido e recupere rapidamente
-
Degrade graciosamente sempre que possível
-
Monitoramento e alerta em tempo real do centro de comando sobre falhas
No próximo post, veremos como combinar os benefícios do Hystrix com o framework Spring.
O código completo do projeto e todos os exemplos podem ser encontrados emgithub project.