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

Escrito por el , actualizado el .
java planeta-codigo programacion spring
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.

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, 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";
    }
}
 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);
    }
}
 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
plugins {
    id 'application'
    id 'org.springframework.boot' version '2.0.7.RELEASE'
    id 'io.spring.dependency-management' version '1.0.6.RELEASE'
}

mainClassName = 'io.github.picodotdev.blogbitix.springcloud.client.Main'

dependencyManagement {
    imports {
        mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Finchley.SR2'
    }
}

dependencies {
    // 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')
}

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

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)
...

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 dahsboard 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 muhco más vistosas además de observar los datos exactamente en el periodo de tiempo deseado.

Estados del circuit breaker

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 programacion funcional.

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

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 el comando ./gradle-run.sh.