Spring Cloud Hystrix: resilient services

1
8141

0. Índice de contenidos.


1. Introducción.

Hystrix es una librería que forma parte del stack de Spring Cloud, desarrollada por Netflix, que facilita la implementación del patrón circuit breaker dentro de una arquitectura de servicios distribuidos.

La comunicación entre sistemas adolece de indisponibilidades debidas a las propias características de los mismos: microcortes en las comunicaciones, servicio no disponible temporalmente, lentitud en las respuestas por el excesivo uso de un servicio,… el patrón circuit breaker permite gestionar estos problemas derivados de las comunicaciones estableciendo mecanismos de control, ayudando a mejorar la resiliencia de los sistemas.

De entre las características de hystrix podemos resaltar las siguientes:

  • ejecución de llamadas a sistemas externos o internos en segundo plano,
  • posibilidad de establecer timeouts en las peticiones,
  • posibilidad de establecer semáforos (pools de hilos), para cada petición, de forma que si no hay hilos disponibles la petición será rechazada en lugar de encolada,
  • proporciona métodos para gestionar la propagación de errores en cascada,
  • recolección de métricas de peticiones exitosas, fallidas y timeouts,
  • gestiona las llamadas para que, en caso de que un sistema externo exceda una cuota de errores definida, no se realicen más peticiones al mismo,
  • permite la declaración de fallbacks, en caso de error en una petición se ejecutará la estrategia definida para el caso de fallo,
  • proporciona un dashboard que integra las métricas capturadas,

En este tutorial veremos un ejemplo práctico de uso de hystrix para la gestión de fallbacks, integrado con clientes de feign.


2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.5 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS Sierra 10.12.5
  • Oracle Java: 1.8.0_25
  • Spring Cloud Dalston SR3

3. Ejemplo de definición de fallback.

Vamos a ver un ejemplo de interfaz de servicio:

public interface HelloWorld {

	String sayHi(String name);	

}

Un cliente de feign sin ninguna particularidad especial:

package com.sanchezjm.tuto.feign.dummy.remote;

import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import com.sanchezjm.tuto.feign.dummy.HelloWorld;

@FeignClient(name = "localapp")
public interface HelloWorldClient extends HelloWorld {

	@RequestMapping(value="/hello/{name}")
	String sayHi(@PathVariable("name") String name);	

}

Otro cliente de feign con una estrategia de fallback definida:

package com.sanchezjm.tuto.feign.hystrix.dummy.local;

import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import com.sanchezjm.tuto.feign.dummy.HelloWorld;

@FeignClient(name = "localapp", fallback= HelloWorldFooClient.class)
public interface HelloWorldFallBackClient extends HelloWorld {

	@RequestMapping(value="/hello/{name}")
	String sayHi(@PathVariable("name") String name);
	
}

Apuntando a un cliente foo HelloWorldFooClient, que podría tener el siguiente código, para su ejecución por defecto en caso de error:

package com.sanchezjm.tuto.feign.hystrix.dummy.local;

import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;

@Component
public class HelloWorldFooClient implements HelloWorldFallBackClient {

	public String sayHi(String name){
		return "Bye "+ name + "!";
	}
	
}

Si hacemos uso del cliente HelloWorldClient y no puede comunicarse con el servicio o este devuelve un error, se lanzará una HystrixRuntimeException:

com.netflix.hystrix.exception.HystrixRuntimeException: HelloWorldClient#sayHi(String) failed and no fallback available.
	at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:819)
	at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:804)
	at rx.internal.operators.OperatorOnErrorResumeNextViaFunction$4.onError(OperatorOnErrorResumeNextViaFunction.java:140)
	at rx.internal.operators.OnSubscribeDoOnEach$DoOnEachSubscriber.onError(OnSubscribeDoOnEach.java:87)
	at rx.internal.operators.OnSubscribeDoOnEach$DoOnEachSubscriber.onError(OnSubscribeDoOnEach.java:87)
	at com.netflix.hystrix.AbstractCommand$DeprecatedOnFallbackHookApplication$1.onError(AbstractCommand.java:1472)
	at com.netflix.hystrix.AbstractCommand$FallbackHookApplication$1.onError(AbstractCommand.java:1397)
	at rx.internal.operators.OnSubscribeDoOnEach$DoOnEachSubscriber.onError(OnSubscribeDoOnEach.java:87)
...

Si bien, si hacemos uso del cliente HelloWorldFallBackClient y se produce un error en las comunicaciones, se invocará a la lógica del cliente alternativo HelloWorldFooClient, devolviendo la cadena «Bye «+ name + «!»


4. Ejecución de un test de integración.

Vamos a ejecutar un primer test de integración para comprobar que efectivamente el comportamiento del primer cliente es ese y, para ello, lo primero será exponer el código del test:

package com.sanchezjm.tuto.feign;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.netflix.ribbon.StaticServerList;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.netflix.hystrix.exception.HystrixRuntimeException;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerList;
import com.sanchezjm.tuto.feign.dummy.HelloWorld;
import com.sanchezjm.tuto.feign.dummy.remote.HelloWorldClient;
import com.sanchezjm.tuto.feign.exception.BusinessException;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {FeignClientsApplication.class, FeignClientsIntegrationTest.Application.class}, webEnvironment = WebEnvironment.RANDOM_PORT, value = {
		"spring.application.name=hello-service", "feign.hystrix.enabled=true",
		"feign.okhttp.enabled=false" , "hystrix.shareSecurityContext=true"})
@DirtiesContext
public class FeignClientsIntegrationTest {

	@Value("${local.server.port}")
	private int port = 0;
	
	@Autowired
	protected HelloWorldClient helloWorldClient;
	
	@Configuration
	@EnableAutoConfiguration
	@RestController
	@EnableFeignClients(clients = { HelloWorldClient.class })
	@RibbonClient(name = "localapp", configuration = LocalRibbonClientConfiguration.class)
	protected static class Application implements HelloWorld {

		@Override
		@RequestMapping(value="/hello/{name}")
		public String sayHi(@PathVariable("name") String name) {
			if ("dummy".equals(name)){
				throw new IllegalArgumentException("Dummies not supported!");
			}
			if ("nil".equals(name)){
				throw new BusinessException("Nil not supported!");
			}			
			return "Hi " + name + "!";
		}

		@RequestMapping(value="/not_found")
		public String notFound() {
			return "";
		}
		
	}

	@Configuration
	static class LocalRibbonClientConfiguration {

		@Value("${local.server.port}")
		private int port = 0;

		@Bean
		public ServerList ribbonServerList() {
			return new StaticServerList<>(new Server("localhost", this.port));
		}
	}
	
    @Test
	public void shouldSayHi(){
		Assert.assertEquals("Hi all!", helloWorldClient.sayHi("all"));
	}

	@Test
	public void shouldThrowIllegalArgumentException(){
		try {
			final String message = helloWorldClient.sayHi("dummy");
			Assert.fail("Must throws an Exception");
		}
		catch(HystrixRuntimeException e) {
			Assert.assertEquals("Dummies not supported!", e.getCause().getMessage());
		}
	}
	
}

Sus características:

  • levanta un contexto de Spring Web con una serie de propiedades que ya vimos para feign, por un puerto random,
  • dentro del propio test declaramos un controlador que implementa la misma interfaz que los clientes que ya hemos visto,
  • habilitamos el cliente de feign que usaremos primero
  • en la configuración del controlador habilitamos un cliente de ribbon con una política de balanceo con un único servidor local, que apunta al mismo puerto que el servicio publicado -el establecido con un random e inyectado con un @Value(«${local.server.port}»); con ello evitamos tener que levantar cualquier otro tipo de infraestructura en local para las pruebas,
  • la lógica del controlador simplemente comprueba el parámetro de entrada para lanzar una excepción de Runtime o de negocio, aunque esta es también unchecked o devolver el resultado concatenando el parámetro de entrada,
  • por último, un par de métodos de test que comprueban tanto el positivo como el negativo y, este último, comprueba que se lanza una HystrixRuntimeException, que encapsula el mensaje de la excepción que lo origina.

Partiendo del test anterior vamos a extender la propia clase de test sobrescribiendo el segundo de los métodos (aunque no cuadre el nombre con su nueva lógica) y haciendo uso del segundo de nuestros clientes, el que sí contiene la estrategia de fallback, lanzamos el test:

package com.sanchezjm.tuto.feign;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.sanchezjm.tuto.feign.hystrix.dummy.local.HelloWorldFallBackClient;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {FeignClientsApplication.class, HystrixFeignClientsIntegrationTest.Application.class}, webEnvironment = WebEnvironment.RANDOM_PORT, value = {
		"spring.application.name=identity-service", "feign.hystrix.enabled=true",
		"feign.okhttp.enabled=false" , "hystrix.shareSecurityContext=true"})
@DirtiesContext
@ComponentScan(basePackages="com.sanchezjm.tuto.hystrix.dummy.local")
public class HystrixFeignClientsIntegrationTest extends FeignClientsIntegrationTest{

	@Autowired
	protected HelloWorldFallBackClient helloWorldFallBackClient;
	
	@Test
	public void shouldThrowIllegalArgumentException(){
		Assert.assertEquals("Bye dummy!", helloWorldFallBackClient.sayHi("dummy"));
	}
	
}

Pese al nombre del método, ahora ya no se lanza la excepcíon puesto que hemos definido la estrategia de fallback; lo podemos comprobar en la salida por consola:

2017-09-13 19:15:21.688 ERROR 53471 --- [o-auto-1-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.IllegalArgumentException: Dummies not supported!] with root cause
...
2017-09-13 19:15:51.964 DEBUG 53477 --- [trix-localapp-1] com.netflix.hystrix.AbstractCommand      : Error executing HystrixCommand.run(). Proceeding to fallback logic ...


5. Configuración

Ejecutando el test con el nivel de logging a TRACE podemos ver todas las propiedades que se pueden establecer para configurar todas las estrategias de circuit braker, tanto a nivel del comando de hystrix (declaración de fallback en el cliente de feign) como a nivel global:

hystrix.command.HelloWorldFallBackClient#sayHi(String).circuitBreaker.enabled to use NEXT property: hystrix.command.default.circuitBreaker.enabled = true
hystrix.command.HelloWorldFallBackClient#sayHi(String).circuitBreaker.requestVolumeThreshold to use NEXT property: hystrix.command.default.circuitBreaker.requestVolumeThreshold = 20
hystrix.command.HelloWorldFallBackClient#sayHi(String).circuitBreaker.sleepWindowInMilliseconds to use NEXT property: hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds = 5000
hystrix.command.HelloWorldFallBackClient#sayHi(String).circuitBreaker.errorThresholdPercentage to use NEXT property: hystrix.command.default.circuitBreaker.errorThresholdPercentage = 50
hystrix.command.HelloWorldFallBackClient#sayHi(String).circuitBreaker.forceOpen to use NEXT property: hystrix.command.default.circuitBreaker.forceOpen = false
hystrix.command.HelloWorldFallBackClient#sayHi(String).circuitBreaker.forceClosed to use NEXT property: hystrix.command.default.circuitBreaker.forceClosed = false
hystrix.command.HelloWorldFallBackClient#sayHi(String).execution.isolation.strategy to use NEXT property: hystrix.command.default.execution.isolation.strategy = THREAD


hystrix.command.HelloWorldFallBackClient#sayHi(String).execution.isolation.thread.timeoutInMilliseconds to use NEXT property: hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds = 3000
hystrix.command.HelloWorldFallBackClient#sayHi(String).execution.timeout.enabled to use NEXT property: hystrix.command.default.execution.timeout.enabled = true
hystrix.command.HelloWorldFallBackClient#sayHi(String).execution.isolation.thread.interruptOnTimeout to use NEXT property: hystrix.command.default.execution.isolation.thread.interruptOnTimeout = true
hystrix.command.HelloWorldFallBackClient#sayHi(String).execution.isolation.thread.interruptOnFutureCancel to use NEXT property: hystrix.command.default.execution.isolation.thread.interruptOnFutureCancel = false
hystrix.command.HelloWorldFallBackClient#sayHi(String).execution.isolation.semaphore.maxConcurrentRequests to use NEXT property: hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests = 10

hystrix.command.HelloWorldFallBackClient#sayHi(String).fallback.isolation.semaphore.maxConcurrentRequests to use NEXT property: hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests = 10
hystrix.command.HelloWorldFallBackClient#sayHi(String).fallback.enabled to use NEXT property: hystrix.command.default.fallback.enabled = true
hystrix.command.HelloWorldFallBackClient#sayHi(String).metrics.rollingStats.timeInMilliseconds to use NEXT property: hystrix.command.default.metrics.rollingStats.timeInMilliseconds = 10000

hystrix.command.HelloWorldFallBackClient#sayHi(String).metrics.rollingStats.numBuckets to use NEXT property: hystrix.command.default.metrics.rollingStats.numBuckets = 10
hystrix.command.HelloWorldFallBackClient#sayHi(String).metrics.rollingPercentile.enabled to use NEXT property: hystrix.command.default.metrics.rollingPercentile.enabled = true
hystrix.command.HelloWorldFallBackClient#sayHi(String).metrics.rollingPercentile.timeInMilliseconds to use NEXT property: hystrix.command.default.metrics.rollingPercentile.timeInMilliseconds = 60000
hystrix.command.HelloWorldFallBackClient#sayHi(String).metrics.rollingPercentile.numBuckets to use NEXT property: hystrix.command.default.metrics.rollingPercentile.numBuckets = 6
hystrix.command.HelloWorldFallBackClient#sayHi(String).metrics.rollingPercentile.bucketSize to use NEXT property: hystrix.command.default.metrics.rollingPercentile.bucketSize = 100
hystrix.command.HelloWorldFallBackClient#sayHi(String).metrics.healthSnapshot.intervalInMilliseconds to use NEXT property: hystrix.command.default.metrics.healthSnapshot.intervalInMilliseconds = 500
hystrix.command.HelloWorldFallBackClient#sayHi(String).requestCache.enabled to use NEXT property: hystrix.command.default.requestCache.enabled = true
hystrix.command.HelloWorldFallBackClient#sayHi(String).requestLog.enabled to use NEXT property: hystrix.command.default.requestLog.enabled = true


hystrix.threadpool.localapp.allowMaximumSizeToDivergeFromCoreSize to use NEXT property: hystrix.threadpool.default.allowMaximumSizeToDivergeFromCoreSize = false
hystrix.threadpool.localapp.coreSize to use NEXT property: hystrix.threadpool.default.coreSize = 10
hystrix.threadpool.localapp.maximumSize to use NEXT property: hystrix.threadpool.default.maximumSize = 10
hystrix.threadpool.localapp.keepAliveTimeMinutes to use NEXT property: hystrix.threadpool.default.keepAliveTimeMinutes = 1
hystrix.threadpool.localapp.maxQueueSize to use NEXT property: hystrix.threadpool.default.maxQueueSize = -1
hystrix.threadpool.localapp.queueSizeRejectionThreshold to use NEXT property: hystrix.threadpool.default.queueSizeRejectionThreshold = 5
hystrix.threadpool.localapp.metrics.rollingStats.timeInMilliseconds to use NEXT property: hystrix.threadpool.default.metrics.rollingStats.timeInMilliseconds = 10000
hystrix.threadpool.localapp.metrics.rollingStats.numBuckets to use NEXT property: hystrix.threadpool.default.metrics.rollingStats.numBuckets = 10

6. Referencias.

7. Conclusiones.

Continuamos examinando las posibilidades del stack de Spring Cloud.

Un saludo.

Jose

1 COMENTARIO

  1. Es importante aclarar que se debe activar hystrix para los clientes feign ya que por defecto, boot tiene dicha configuración en «false» (desactivado)

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad