Introducción a Hystrix

1. Información general

Un sistema distribuido típico consta de muchos servicios que colaboran juntos.

Estos servicios son propensos a fallar o retrasar las respuestas. Si un servicio falla, puede afectar a otros servicios que afectan el rendimiento y posiblemente hagan que otras partes de la aplicación sean inaccesibles o, en el peor de los casos, desactive toda la aplicación.

Por supuesto, hay soluciones disponibles que ayudan a que las aplicaciones sean resistentes y tolerantes a fallas; uno de esos marcos es Hystrix.

La biblioteca del marco de trabajo de Hystrix ayuda a controlar la interacción entre los servicios proporcionando tolerancia a fallas y tolerancia a la latencia. Mejora la resistencia general del sistema al aislar los servicios que fallan y detener el efecto en cascada de las fallas.

En esta serie de publicaciones, comenzaremos observando cómo Hystrix viene al rescate cuando falla un servicio o sistema y lo que Hystrix puede lograr en estas circunstancias.

2. Ejemplo simple

La forma en que Hystrix proporciona tolerancia a fallas y latencia es aislar y encapsular llamadas a servicios remotos.

En este ejemplo simple, envolvemos una llamada en el método run () del HystrixCommand:

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 + "!"; } }

y ejecutamos la llamada de la siguiente manera:

@Test public void givenInputBobAndDefaultSettings_whenCommandExecuted_thenReturnHelloBob(){ assertThat(new CommandHelloWorld("Bob").execute(), equalTo("Hello Bob!")); }

3. Configuración de Maven

Para usar Hystrix en proyectos de Maven, necesitamos tener la dependencia hystrix-core y rxjava-core de Netflix en el proyecto pom.xml :

 com.netflix.hystrix hystrix-core 1.5.4  

La última versión siempre se puede encontrar aquí.

 com.netflix.rxjava rxjava-core 0.20.7 

La última versión de esta biblioteca siempre se puede encontrar aquí.

4. Configuración del servicio remoto

Comencemos por simular un ejemplo del mundo real.

En el siguiente ejemplo , la clase RemoteServiceTestSimulator representa un servicio en un servidor remoto. Tiene un método que responde con un mensaje después del período de tiempo dado. Podemos imaginar que esta espera es una simulación de un proceso lento en el sistema remoto que resulta en una respuesta retrasada al servicio de llamada:

class RemoteServiceTestSimulator { private long wait; RemoteServiceTestSimulator(long wait) throws InterruptedException { this.wait = wait; } String execute() throws InterruptedException { Thread.sleep(wait); return "Success"; } }

Y aquí está nuestro cliente de muestra que llama a RemoteServiceTestSimulator .

La llamada al servicio está aislada y envuelta en el método run () de un HystrixCommand. Es esta envoltura la que proporciona la resistencia que mencionamos anteriormente:

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(); } }

La llamada se ejecuta llamando al método execute () en una instancia del objeto RemoteServiceTestCommand .

La siguiente prueba demuestra cómo se hace esto:

@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")); }

Hasta ahora hemos visto cómo envolver las llamadas de servicio remoto en el objeto HystrixCommand . En la siguiente sección, veamos cómo lidiar con una situación en la que el servicio remoto comienza a deteriorarse.

5. Trabajar con servicio remoto y programación defensiva

5.1. Programación defensiva con tiempo de espera

Es una práctica de programación general establecer tiempos de espera para llamadas a servicios remotos.

Comencemos por ver cómo establecer el tiempo de espera en HystrixCommand y cómo ayuda al cortocircuitar:

@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")); }

En la prueba anterior, estamos retrasando la respuesta del servicio estableciendo el tiempo de espera en 500 ms. También estamos configurando el tiempo de espera de ejecución en HystrixCommand en 10,000 ms, permitiendo así tiempo suficiente para que responda el servicio remoto.

Ahora veamos qué sucede cuando el tiempo de espera de ejecución es menor que la llamada de tiempo de espera del servicio:

@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 cómo bajamos la barra y configuramos el tiempo de espera de ejecución en 5,000 ms.

Esperamos que el servicio responda en 5.000 ms, mientras que hemos configurado el servicio para que responda después de 15.000 ms. Si nota cuando ejecuta la prueba, la prueba saldrá después de 5,000 ms en lugar de esperar 15,000 ms y arrojará una HystrixRuntimeException.

Esto demuestra cómo Hystrix no espera más que el tiempo de espera configurado para obtener una respuesta. Esto ayuda a que el sistema protegido por Hystrix sea más receptivo.

En las siguientes secciones, veremos cómo establecer el tamaño del grupo de subprocesos que evita que los subprocesos se agoten y discutiremos sus beneficios.

5.2. Programación defensiva con grupo de subprocesos limitado

Establecer tiempos de espera para la llamada de servicio no resuelve todos los problemas asociados con los servicios remotos.

Cuando un servicio remoto comienza a responder lentamente, una aplicación típica continuará llamando a ese servicio remoto.

La aplicación no sabe si el servicio remoto está en buen estado o no y se generan nuevos subprocesos cada vez que entra una solicitud. Esto hará que se utilicen subprocesos en un servidor que ya tiene problemas.

No queremos que esto suceda ya que necesitamos estos subprocesos para otras llamadas remotas o procesos que se ejecutan en nuestro servidor y también queremos evitar que aumente la utilización de la CPU.

Veamos cómo establecer el tamaño del grupo de subprocesos en HystrixCommand :

@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")); }

En la prueba anterior, estamos configurando el tamaño máximo de la cola, el tamaño de la cola principal y el tamaño del rechazo de la cola. Hystrix comenzará a rechazar las solicitudes cuando el número máximo de subprocesos haya llegado a 10 y la cola de tareas haya alcanzado un tamaño de 10.

El tamaño del núcleo es la cantidad de subprocesos que siempre permanecen activos en el grupo de subprocesos.

5.3. Programación defensiva con patrón de cortacircuitos

Sin embargo, todavía hay una mejora que podemos hacer a las llamadas de servicio remoto.

Consideremos el caso de que el servicio remoto haya comenzado a fallar.

No queremos seguir enviando solicitudes y desperdiciar recursos. Idealmente, querríamos dejar de realizar solicitudes durante un cierto período de tiempo para que el servicio se recupere antes de reanudar las solicitudes. Esto es lo que se denomina patrón de interruptor de cortocircuito .

Veamos cómo Hystrix implementa este patrón:

@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; }

En la prueba anterior hemos establecido diferentes propiedades del interruptor automático. Los más importantes son:

  • El CircuitBreakerSleepWindow que se establece en 4.000 ms. Esto configura la ventana del disyuntor y define el intervalo de tiempo después del cual se reanudará la solicitud al servicio remoto.
  • El CircuitBreakerRequestVolumeThreshold que se establece en 1 y define el número mínimo de solicitudes necesarias antes de que se considera la tasa de fracaso

Con la configuración anterior en su lugar, nuestro HystrixCommand ahora se abrirá después de dos solicitudes fallidas. La tercera solicitud ni siquiera llegará al servicio remoto a pesar de que hemos establecido el retraso del servicio en 500 ms, Hystrix hará un cortocircuito y nuestro método devolverá nulo como respuesta.

Posteriormente agregaremos un Thread.sleep (5000) para cruzar el límite de la ventana de suspensión que hemos establecido. Esto hará que Hystrix cierre el circuito y las solicitudes posteriores fluyan con éxito.

6. Conclusión

En resumen, Hystrix está diseñado para:

  1. Brindar protección y control sobre fallas y latencia de los servicios a los que normalmente se accede a través de la red.
  2. Detenga la cascada de fallas que resultan de que algunos de los servicios estén inactivos
  3. Falla rápido y recupera rápidamente
  4. Degradar con gracia siempre que sea posible
  5. Monitoreo y alerta en tiempo real del centro de comando sobre fallas

En el próximo post veremos cómo combinar los beneficios de Hystrix con el framework Spring.

El código completo del proyecto y todos los ejemplos se pueden encontrar en el proyecto github.