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