Tolerancia a fallos en un cliente de microservicio con Spring Cloud Netflix y Hystrix

Escrito por el , actualizado el .
java planeta-codigo programacion spring
Enlace permanente Comentarios

Spring

Java

Los microservicios son independientes unos de otros y se comunican mediante operaciones de red. Dado que las operaciones se realizan a través de un medio no confiable como la red, dada su naturaleza efímera y a que pueden fallar en los microservicios es importante que los clientes estén preparados ante posibles fallos.

Un patrón o técnica que se suele emplear es el de Circuit Breaker, en Java y con Spring se ofrece en el proyecto Spring Cloud Netflix mediante Hystrix y Javanica. Este patrón soluciona dos problemas cuando un microservicio del que se depende falla y hace al microservicio que lo usa tolerante a fallos.

  • Uno es que cuando un microservicio empieza a fallar es necesario dejar de hacerle peticiones para permitirle recuperarse si está saturado que provoca esos fallos. Cuando ocurre un fallo es posible realizar una acción en sustitución de la llamada al microservicio y devolver un valor alternativo como medida paliativa y hacer que el microsevicio afectado tenga la posibilidad de seguir ofreciendo su servicio aunque sea de forma degradada.
  • Otro problema es que el microservicio aunque no falle tarde demasiado en responder, se puede establecer un timeout que si se supera se deja de esperar e igualmente se realiza la acción de sustitución lo que evita que los microservicios que usan uno que tarda demasiado en responder agoten sus recursos y empiecen a fallar o tardar demasiado también.

En ambos casos se evita que la cadena de microservicios empiece a fallar y con ello sistema completo. El proyecto Hystrix ha dejado de desarrollarse de forma activa tal como aparece en el propio README.md y como alternativa se recomienda usar Resilience4j que además está diseñado para Java 8 y la programación funcional, en el artículo Implementar tolerancia a fallos con Resilience4j muestro su implementación.

Hystrix

El patrón circuit breaker se denomina así ya que implementa una lógica similar a un circuito eléctrico. El circuito en su estado normal está cerrado y se realizan las llamadas al microservicio servidor. Si el microservicio servidor empieza a fallar se llama a la acción alternativa con su valor, si se supera un umbral de errores el circuito pasa a estado abierto y se dejan de hacer llamadas al microservicio servidor. Cada cierto tiempo definido se realiza una llamada al servicio servidor para comprobar su estado de forma que si responde correctamente el circuito pasa a estado cerrado nuevamente y las siguientes llamadas se realizan al microservicio servidor dejándose de utilizar la acción alternativa.

Diagrama del patrón circuit breaker Diagrama de estados Fallback del patrón circuit breaker

Diagrama del patrón circuit breaker, de estados y método fallback

Para utilizar Hystrix como implementación del patrón circuit breaker en una aplicación Java con Spring Boot el método que realiza la llamada al microservicio servidor ha de encapsularse en un método anotado con la anotación @HystrixCommand, como parámetro se indica un método con la acción alternativa o fallback que obtiene un valor en los fallos. También se puede indicar el tiemout de espera antes de considerar que la llamada ha fallado con la propiedad execution.isolation.thread.timeoutInMilliseconds. Igualmente se pueden indicar los valores para abrir el circuito con circuitBreaker.requestVolumeThreshold y circuitBreaker.errorThresholdPercentage. Esos son los básicos para utilizar este patrón de tolerancia a fallos. Tiene algunos valores adicionales más que se pueden configurar para adaptar el patrón a los valores óptimos de la aplicación.

En el ejemplo el cliente en un bucle realiza las llamadas al servicio con un método get() anotado con @HystrixCommand. En este método se encapsula la petición HTTP que puede fallar, utilizando la librería Jersey y obtenida la ubicación de una instancia del servicio a su vez del servicio de registro y descubrimiento Eureka.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package io.github.picodotdev.blogbitix.springcloud.client;

...

@Component
public class ClientService {

    @Autowired
    private LoadBalancerClient loadBalancer;

    @HystrixCommand(fallbackMethod = "getFallback", commandProperties = {
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "4"),
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")
    })
    public String get() {
        ServiceInstance instance = loadBalancer.choose("service");
        URI uri = instance.getUri();
        return Client.create().resource(uri).get(String.class);
    }

    private String getFallback() {
        return "Fallback";
    }
}
ClientService.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package io.github.picodotdev.blogbitix.springcloud.client;

...

@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
@EnableHystrixDashboard
public class Main implements CommandLineRunner {

	@Autowired
	private DefaultConfiguration configuration;

	@Autowired
	private ClientService service;
    
	@Override
	public void run(String... args) throws Exception {
		System.out.printf("Valor de propiedad de configuración (%s): %s%n", "config.key", configuration.getKey());
		System.out.printf("Valor de propiedad de configuración (%s): %s%n", "config.password", configuration.getPassword());

		for (int i = 0; i < 20000; ++i) {
			String response = service.get();
			System.out.printf("Service response: %s%n", response);
			Thread.sleep(100);
		}
	}

	public static void main(String[] args) throws Exception {
		SpringApplication application = new SpringApplication(Main.class);
		application.setApplicationContextClass(AnnotationConfigApplicationContext.class);
		SpringApplication.run(Main.class, args);
	}
}
Main.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
plugins {
    id 'application'
    id 'org.springframework.boot' version '2.1.12.RELEASE'
}

application {
    mainClassName = 'io.github.picodotdev.blogbitix.springcloud.client.Main'
}

dependencies {
    implementation(platform('org.springframework.boot:spring-boot-dependencies:2.1.12.RELEASE'))
    implementation(platform('org.springframework.cloud:spring-cloud-dependencies:Greenwich.SR2'))

    // Spring
    def excludeSpringBootStarterLogging = { exclude(group: 'org.springframework.boot', module: 'spring-boot-starter-logging') }
    compile('org.springframework.boot:spring-boot-starter', excludeSpringBootStarterLogging)
    compile('org.springframework.boot:spring-boot-starter-web', excludeSpringBootStarterLogging)
    compile('org.springframework.boot:spring-boot-starter-log4j2', excludeSpringBootStarterLogging)
    compile('org.springframework.boot:spring-boot-starter-actuator', excludeSpringBootStarterLogging)
    compile('org.springframework.cloud:spring-cloud-starter-config', excludeSpringBootStarterLogging)
    compile('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client', excludeSpringBootStarterLogging)
    compile('org.springframework.cloud:spring-cloud-starter-netflix-ribbon', excludeSpringBootStarterLogging)
    compile('org.springframework.cloud:spring-cloud-starter-netflix-hystrix', excludeSpringBootStarterLogging)
    compile('org.springframework.cloud:spring-cloud-starter-netflix-hystrix-dashboard', excludeSpringBootStarterLogging)

    compile('org.glassfish.jersey.core:jersey-client:2.27')
    compile('org.glassfish.jersey.inject:jersey-hk2:2.27')

    runtime('com.google.code.gson:gson:2.8.5')
    runtime('com.fasterxml.jackson.core:jackson-databind:2.9.6')
    runtime('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.6')
}
build.gradle

El circuito se abre cuando el número de llamadas supera un umbral y un porcentaje de fallos, se han de cumplir las dos condiciones. Si el número de llamadas que se realizan no superan el umbral aunque todas fallen el circuito permanece cerrado. Ambos valores son personalizables con las propiedades circuitBreaker.requestVolumeThreshold y circuitBreaker.errorThresholdPercentage. El circuito permanece abierto al menos durante el tiempo indicado por metrics.rollingStats.timeInMilliseconds.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package io.github.picodotdev.blogbitix.springcloud.service;

...

@RestController
public class DefaultController {

	@Autowired
	private DefaultConfiguration configuration;

	private Random random;

	public DefaultController() {
		this.random = new Random();
	}

	@RequestMapping("/")
	public String home(HttpServletRequest request) throws Exception {
		// Timeout simulation
		//Thread.sleep(random.nextInt(2000));

		return String.format("Hello world (%s, %s)", request.getRequestURL(), configuration.getKey());
	}
}
DefaultController.java

En la aplicación ejemplo hay un microservicio servidor y un microservicio cliente, iniciada una instancia de microservicio servidor y una instancia del microservicio cliente que implementa el patrón circuit breaker inicialmente las peticiones se realizarán correctamente si no ocurre un timeout. Si se finaliza el proceso del microservicio servidor las peticiones del cliente al servidor empezarán a fallar y este obtiene el valor alternativo del método fallback, si se supera el umbral de llamadas y de fallos el circuito pasa a estado abierto. Mientras el circuito permanezca abierto el cliente sondea con una petición cada cierto tiempo el estado del servidor, si el servicio servidor se inicia unos instantes después de que esté disponible el cliente con la petición de sondeo comprobará que el servidor funciona, se cerrará el circuito y el cliente empezará a obtener los valores devueltos por el microservicio servidor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ ./gradlew client:run
...
Service response: Hello world (http://192.168.1.4:8080/, value)
Service response: Hello world (http://192.168.1.4:8080/, value)
Service response: Hello world (http://192.168.1.4:8080/, value)
Service response: Hello world (http://192.168.1.4:8080/, value)
Service response: Hello world (http://192.168.1.4:8080/, value)
... # kill service, Ctrl+C
Service response: Fallback
Service response: Fallback
Service response: Fallback
Service response: Fallback
Service response: Fallback
Service response: Fallback
Service response: Fallback
... # restart service
Service response: Fallback
Service response: Fallback
Service response: Hello world (http://192.168.1.4:8080/, value)
Service response: Hello world (http://192.168.1.4:8080/, value)
Service response: Hello world (http://192.168.1.4:8080/, value)
Service response: Hello world (http://192.168.1.4:8080/, value)
...
System.out

Para monitorizar en tiempo real el estado del sistema y de los circuitos se ofrece un dashboard en el que visualizan el número de peticiones que se están realizando, las fallidas, el estado de los circuitos, las que fallan por timeout o las que fallan con error. Para tener acceso a esta página hay que incluir la dependencia org.springframework.cloud:spring-cloud-starter-netflix-hystrix-dashboard. La página dashboard está disponible en la dirección http://localhost:8085/hystrix. Este dashboard que ofrece Hystrix es muy básico y con un diseño mejorable, se pueden exponer las métricas de Hystrix en Grafana con Prometheus para tener un panel con exactamente la información que se desee en una gráficas mucho más vistosas además de observar los datos exactamente en el periodo de tiempo deseado.

Circuit breaker cerrado Circuit breaker abierto

Estados del circuit breaker
1
2
3
4
$ ./gradlew discoveryserver:run --args="--port=8761"
$ ./gradlew configserver:run --args="--port=8090"
$ ./gradlew service:run --args="--port=8080"
$ ./gradlew client:run
gradle-run.sh
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradle-run.sh


Este artículo forma parte de la serie spring-cloud:

  1. Datos de sesión externalizados con Spring Session
  2. Aplicación Java autocontenida con Spring Boot
  3. Configuración de una aplicación en diferentes entornos con Spring Cloud Config
  4. Información y métricas de la aplicación con Spring Boot Actuator
  5. Registro y descubrimiento de servicios con Spring Cloud y Consul
  6. Aplicaciones basadas en microservicios
  7. Registro y descubrimiento de servicios con Spring Cloud Netflix
  8. Recargar sin reiniciar la configuración de una aplicación Spring Boot con Spring Cloud Config
  9. Almacenar cifrados los valores de configuración sensibles en Spring Cloud Config
  10. Tolerancia a fallos en un cliente de microservicio con Spring Cloud Netflix y Hystrix
  11. Balanceo de carga y resiliencia en un microservicio con Spring Cloud Netflix y Ribbon
  12. Proxy para microservicios con Spring Cloud Netflix y Zuul
  13. Monitorizar una aplicación Java de Spring Boot con Micrometer, Prometheus y Grafana
  14. Exponer las métricas de Hystrix en Grafana con Prometheus de una aplicación Spring Boot
  15. Servidor OAuth, gateway y servicio REST utilizando tokens JWT con Spring
  16. Trazabilidad en microservicios con Spring Cloud Sleuth
  17. Implementar tolerancia a fallos con Resilience4j
  18. Iniciar una aplicación de Spring Boot en un puerto aleatorio
  19. Utilizar credenciales de conexión a la base de datos generadas por Vault en una aplicación de Spring
  20. Microservicios con Spring Cloud, Consul, Nomad y Traefik
  21. Trazabilidad en servicios distribuidos con Sleuth y Zipkin
  22. Configuración de una aplicación con Spring Boot y configuración centralizada con Spring Cloud Config
Comparte el artículo: