Resilience4j, tolerancia a fallos completa

En las últimas semanas hemos estado hablando de resiliencia, una característica sin dudas deseada en las arquitecturas de software y de microservicios de forma particular.

Luego de hablar sobre Hystrix, Spring Retry y Bulkhead vamos a cerrar la línea de entradas de resiliencia hablando de la evolución de Hystrix, librería que ya esta en proceso de quedarse obsoleta.

¿Qué es Resilience4j?

Resilience4j es una biblioteca ligera de tolerancia a fallas inspirada en Netflix Hystrix, pero diseñada para programación funcional.
Esta biblioteca permite implementar las principales prácticas asociadas a la resiliencia.

  • Circuit Breaker
  • Bulkhead
  • Rate limiter
  • Retry
  • Time limiter
  • Cache

Resilience4j es una librería ligera basada en Vavr.

Para este artículo usaremos adicionalmente Prometheus y Grafana. Estas herramientas las emplearemos para el almacenamiento de métricas y visualización del estado del circuit breaker y otros de los patrones de arriba.

Ejemplo de Resilience4j con Spring Boot

Las dependencias necesarias para integrar Resilience4j con Spring Boot se muestran a continuación.

<!-- https://mvnrepository.com/artifact/io.github.resilience4j/resilience4j-spring-boot2 -->
        <dependency>
            <groupId>io.github.resilience4j</groupId>
            <artifactId>resilience4j-spring-boot2</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.github.resilience4j/resilience4j-all -->
        <dependency>
            <groupId>io.github.resilience4j</groupId>
            <artifactId>resilience4j-all</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>2.3.0.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.micrometer/micrometer-registry-prometheus -->
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
            <version>1.5.1</version>
        </dependency>

Sobre micrometer y prometheus hablamos en un artículo anterior.

En el ejemplo vamos a implementar las funcionalidades de la siguiente interfaz.

public interface IService {
    String success();
    String failure();
    String failureWithFallback();
    String successException();
    String timeOut();
}

Implementaremos esta interfaz en un servicio OrdersDummy de ejemplo.

/**
 * In this sample, we are going to simulate call to other microservices, and
 * depend of responses the resilience go to work properly
 */
@Component(value = "ordersDummy")
public class OrdersDummyService implements IService {
    public static final String ORDERS = "ordersDummy";
    @Override
    @CircuitBreaker(name = ORDERS)
    @Bulkhead(name = ORDERS)
    @Retry(name = ORDERS)
    public String success() {
        return "Response HTTP 201 CREATED, order was create successfully";
    }
    @Override
    @CircuitBreaker(name = ORDERS)
    @Bulkhead(name = ORDERS)
    @Retry(name = ORDERS)
    public String failure() {
        throw new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR, "This is a remote exception for orders service");
    }
    @Override
    @CircuitBreaker(name = ORDERS)
    @Bulkhead(name = ORDERS)
    @Retry(name = ORDERS)
    public String successException() {
        throw new CustomException("This is a remote exception for orders service, but is welcome");
    }
    @Override
    public String timeOut() {
        try {
            Thread.sleep(3500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "Response 200 with slow remote call or method execution";
    }
    @Override
    @CircuitBreaker(name = ORDERS, fallbackMethod = "fallback")
    public String failureWithFallback() {
        return failure();
    }
    private String fallback(HttpServerErrorException ex) {
        return "Response 200, fallback method for error:  " + ex.getMessage();
    }
}

Cada uno de los nombres de las funciones es descriptivo y las anotaciones asociadas describen como implementar los patrones correspondientes.

La pregunta es: ¿Cuál es el comportamiento de estas anotaciones? ¿cómo se define?

Aprovechando la integración entre spring-boot y resilience4j (resilience4j-spring-boot2), una vía muy sencilla para definir el funcionamiento es a través de properties. El siguiente ejemplo incluye las properties que dan comportamiento al servicio de arriba, además de a otro servicio que se incluye en el ejemplo llamado paymentsDummy.

resilience4j.circuitbreaker:
  configs:
    default:
      registerHealthIndicator: true
      slidingWindowSize: 10
      minimumNumberOfCalls: 5
      permittedNumberOfCallsInHalfOpenState: 3
      automaticTransitionFromOpenToHalfOpenEnabled: true
      waitDurationInOpenState: 5s
      failureRateThreshold: 50
      eventConsumerBufferSize: 10
      recordExceptions:
        - org.springframework.web.client.HttpServerErrorException
        - java.util.concurrent.TimeoutException
        - java.io.IOException
      ignoreExceptions:
        - com.yoandypv.microservices.resilience4j.exception.CustomException
    shared:
      slidingWindowSize: 100
      permittedNumberOfCallsInHalfOpenState: 30
      waitDurationInOpenState: 1s
      failureRateThreshold: 50
      eventConsumerBufferSize: 10
      ignoreExceptions:
        - com.yoandypv.microservices.resilience4j.exception.CustomException
  instances:
    ordersDummy:
      baseConfig: default
    paymentsDummy:
      registerHealthIndicator: true
      slidingWindowSize: 10
      minimumNumberOfCalls: 10
      permittedNumberOfCallsInHalfOpenState: 3
      waitDurationInOpenState: 5s
      failureRateThreshold: 50
      eventConsumerBufferSize: 10
resilience4j.retry:
  configs:
    default:
      maxRetryAttempts: 3
      waitDuration: 100
      retryExceptions:
        - org.springframework.web.client.HttpServerErrorException
        - java.util.concurrent.TimeoutException
        - java.io.IOException
      ignoreExceptions:
        - com.yoandypv.microservices.resilience4j.exception.CustomException
  instances:
    ordersDummy:
      baseConfig: default
    paymentsDummy:
      baseConfig: default
resilience4j.bulkhead:
  configs:
    default:
      maxConcurrentCalls: 100
  instances:
    ordersDummy:
      maxConcurrentCalls: 5
    paymentsDummy:
      maxWaitDuration: 10ms
      maxConcurrentCalls: 20
resilience4j.thread-pool-bulkhead:
  configs:
    default:
      maxThreadPoolSize: 4
      coreThreadPoolSize: 2
      queueCapacity: 2
  instances:
    ordersDummy:
      baseConfig: default
    paymentsDummy:
      maxThreadPoolSize: 1
      coreThreadPoolSize: 1
      queueCapacity: 1
resilience4j.timelimiter:
  configs:
    default:
      cancelRunningFuture: true
      timeoutDuration: 2s
  instances:
    ordersDummy:
      baseConfig: default
    paymentsDummy:
      baseConfig: default

Como se puede ver el formato resilience4j.<patron>, permite definir el funcionamiento del patrón en sí. Se puede definir un comportamiento por defecto (genérico, default). Luego se definen las instancias, que son los componentes (@Component) Spring a los que se le va a aplicar el patrón. Se puede aplicar un config predefinida, por ej: default o crear una personalizada para el componente.

Tener estas configuraciones en properties es muy factible, pues para ejecutar cambios no se deberá tocar el código, este desacoplamiento es muy beneficioso. En este artículo hemos hablado de configuración centralizada, la cual podemos usar para centralizar toda esta configuración en un solo punto.

El controlador para poder probar se expone debajo.

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    @Autowired
    @Qualifier("ordersDummy")
    private  OrdersDummyService ordersDummyService;
    @GetMapping("/success")
    public String successOrder(){
        return this.ordersDummyService.success();
    }
    @GetMapping("/failure")
    public String failureOrder(){
        return this.ordersDummyService.failure();
    }
    @GetMapping("/success-exception")
    public String successException(){
        return this.ordersDummyService.successException();
    }
    @GetMapping("/fallback")
    public String fallBack(){
        return this.ordersDummyService.failureWithFallback();
    }
    @GetMapping("/timeout")
    public String timeOutExecution(){
        return this.ordersDummyService.timeOut();
    }
}

Monitoreando todo el sistema

Lo bueno de Hystrix, es que tiene integrado un dashboard para monitorear todas las métricas. Esto en resilience4j no es parte integrada de la librería, sin embargo con micrometer, podemos sacar métricas que luego prometheus busca y almacena para visualizar finalmente en un dashboard en Grafana. Toda esta parte está dockerizada y se sencilla de poner en producción.

Es importante saber que esta monitorización de Resilience4j se puede mirar en tiempo real, los eventos que van pasando se van visualizando, incluso se pueden generar alertas si fuera necesario.

Ejemplo de CircuitBreaker

En la gráfica anterior, se puede visualizar que en nuestro ejemplo hemos provocado un fallo sobre ordersDummy, esto ha derivado en una apertura del circuito, quedando un circuito abierto (ordersDummy) y otro cerrado, operando normalmente (paymentDummy).

Fallas de servicio

La gráfica anterior muestra la fallas del servicio, esta es especialmente importante porque nos puede alertar cuando un servicio está fallando por encima de un umbral y tomar acciones.

Toda esta magia de que el sistema exponga información es gracias a Spring Actuator, puede visualizar más métricas al levantar el proyecto en el endpoint:

GET http://localhost:9080/actuator

Todo el ejemplo completo, así como los pasos para ponerlo en operación puedes encontrarlos en GitHub.

Si queres aprender más de microservicios y las mejores prácticas para su diseño e implementación te recomiendo el libro de patrones que recién publicamos.

Espero te sea útil el artículo. Nos vemos en la próxima entrada.

1 thought on “Resilience4j, tolerancia a fallos completa

Comments are closed.