Ejemplo de Circuit Breaker con Resilience4j en Spring Boot

resilience4j-circuitbreaker-spring-boot

resilience4j-circuitbreaker-spring-boot


En este nuevo artículo vamos a ver de manera práctica la tolerancia a fallos y resilencia mediante un ejemplo de Circuit Breaker y time limiter con Resilience4j en Spring Boot.

En una arquitectura de microservicios, es de vital importancia hacer uso del patrón de Circuit Breaker para conseguir una arquitectura mucho más resistente y tolerante a fallos.

¿Qué es el Patrón Circuit Breaker?

En nuestra arquitectura de microservicios, vamos a realizar llamadas a otros servicios en muchas ocasiones de manera síncrona. Es por esa razón que en estas llamadas puede darse la circunstancia de que al servicio que se le este llamando no sea accesible o que la latencia de la llamada sea demasiado alta. En estos casos estamos utilizando unos recursos o unos threads los cuales son muy valiosos y estamos ocasionando, por un lado dejar bloqueada una llamada y por otro lado podemos provocar un fallo en cascada.

Una implementación de Circuit Breaker nos va a ayudar en este tipo de escenarios, en los que la llamada a un tercero se ha bloqueado, evitando repetidas llamadas a ese servicio y evitar ese gasto de recursos.

Circuit breaker | Ejemplo de Circuit Breaker con Resilience4j en Spring Boot
Circuit breaker

El dibujo anterior representa de una manera gráfica un Circuit Breaker, como una máquina de estados (Open, Closed,Half_Open). El sistema al intentar comunicar con otro servicio que no responde, se hace un seguimiento de estos errores. Cuando esos errores al comunicar superan un umbral previamente establecido entonces estamos en estado ABIERTO (Open); después de un tiempo de espera el estado pasa a medio abierto (Half_Open) y el sistema intenta reconectarse, si los problemas de comunicación persisten se vuelve a estado abierto y en caso que todo funcione correctamente sería estado Cerrado (closed).

Hands On con Resilience4J y Spring Boot

A continuación vamos a ver un ejemplo en el que hacemos uso de Resilience4J.

¿Qué es Resilience4J?

Resilience4j es una librería que nos va a permitir hacer a nuestras aplicaciones resilientes. Esta orientada a programación funcional, es de fácil uso y ligera ya que apenas tiene dependencias.


Dependencias de Resilience4J en Spring Boot

Vamos a comenzar a crear nuestra aplicación Spring Boot, para ello vamos a ir a la página de Spring Initializr. En el que le diremos las librerías que queremos para nuestro proyecto, entre ellas añadiremos resilience4j:

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
		</dependency>

Configuración de propiedades Resilience4J

En nuestra configuración de resilience4j podemos añadir diferentes configuraciones para diferentes servicios, así como configuraciones para circuitbreaker y timelimiter.

En nuestro ejemplo vamos a añadir la siguiente configuración, en la que le establecemos para la instancia example-resilience, una configuración compartida de manera que sobreescribimos la compartida:

resilience4j:
  circuitbreaker:
    configs:
      shared:
        register-health-indicator: true
        sliding-window-type: count_based
        sliding-window-size: 5
        failure-rate-threshold: 40
        slow-call-rate-threshold: 40
        permitted-number-of-calls-in-half-open-state: 1
        max-wait-duration-in-half-open-state: 10s
        wait-duration-in-open-state: 10s
        slow-call-duration-threshold: 2s
        writable-stack-trace-enabled: true
        automatic-transition-from-open-to-half-open-enabled: true
    instances:
      example:
        base-config: shared
  timelimiter:
    configs:
      shared:
      timeout-duration: 2s
      cancel-running-future: true
    instances:
      example:
        base-config: shared

Creación Listener Circuit Breaker con Resilience4J

En el siguiente paso de nuestra configuración vamos a añadir la configuración para registrar los eventos producidos por nuestro circuit breaker. Para ello vamos a hacer uso de la clase RegistryEventConsumer y a definir un @Bean del tipo RegistryEventConsumer<CircuitBreaker>.

  @Bean
  public RegistryEventConsumer<CircuitBreaker> circuitBreakerEventConsumer() {
    return new RegistryEventConsumer<CircuitBreaker>() {

      @Override
      public void onEntryAddedEvent(EntryAddedEvent<CircuitBreaker> entryAddedEvent) {
        entryAddedEvent.getAddedEntry().getEventPublisher()
            .onFailureRateExceeded(event -> log.error("circuit breaker {} failure rate {} on {}",
                event.getCircuitBreakerName(), event.getFailureRate(), event.getCreationTime())
            )
            .onSlowCallRateExceeded(event -> log.error("circuit breaker {} slow call rate {} on {}",
                event.getCircuitBreakerName(), event.getSlowCallRate(), event.getCreationTime())
            )
            .onCallNotPermitted(event -> log.error("circuit breaker {} call not permitted {}",
                event.getCircuitBreakerName(), event.getCreationTime())
            )
            .onError(event -> log.error("circuit breaker {} error with duration {}s",
                event.getCircuitBreakerName(), event.getElapsedDuration().getSeconds())
            )
            .onStateTransition(
                event -> log.warn("circuit breaker {} state transition from {} to {} on {}",
                    event.getCircuitBreakerName(), event.getStateTransition().getFromState(),
                    event.getStateTransition().getToState(), event.getCreationTime())
            );
      }

      @Override
      public void onEntryRemovedEvent(EntryRemovedEvent<CircuitBreaker> entryRemoveEvent) {
        entryRemoveEvent.getRemovedEntry().getEventPublisher()
            .onFailureRateExceeded(event -> log.debug("Circuit breaker event removed {}",
                event.getCircuitBreakerName()));
      }

      @Override
      public void onEntryReplacedEvent(EntryReplacedEvent<CircuitBreaker> entryReplacedEvent) {
        entryReplacedEvent.getNewEntry().getEventPublisher()
            .onFailureRateExceeded(event -> log.debug("Circuit breaker event replaced {}",
                event.getCircuitBreakerName()));
      }
    };
  }

En este método vamos a poder registrar y analizar los diferentes tipos de eventos que se van a suceder con nuestro circuitbreaker. Entre los diferentes eventos que podemos registrar son los relacionados a añadir uno, reemplazar y eliminar, para todos estos eventos podemos realizar alguna acción.

Creación Listener Time Limiter con Resilience4J

Al igual que hemos hecho añadiendo un listener para el circuit breaker, vamos a hacer lo mismo para poder establecer un time limiter, es decir, un umbral de tiempo en el que se deberán realizar nuestras conexiones.

A continuación añadimos la definición de un @Bean en el que establecemos diferentes listener para nuestro TimeLimiter, para ello creamos un @Bean de tipo RegistryEventConsumer<TimeLimiter>.

  @Bean
  public RegistryEventConsumer<TimeLimiter> timeLimiterEventConsumer() {
    return new RegistryEventConsumer<TimeLimiter>() {
      @Override
      public void onEntryAddedEvent(EntryAddedEvent<TimeLimiter> entryAddedEvent) {
        entryAddedEvent.getAddedEntry().getEventPublisher()
            .onTimeout(event -> log.error("time limiter {} timeout {} on {}",
                event.getTimeLimiterName(), event.getEventType(), event.getCreationTime())
            );
      }

      @Override
      public void onEntryRemovedEvent(EntryRemovedEvent<TimeLimiter> entryRemoveEvent) {
        entryRemoveEvent.getRemovedEntry().getEventPublisher()
            .onTimeout(event -> log.error("time limiter removed {}",
                event.getTimeLimiterName())
            );
      }

      @Override
      public void onEntryReplacedEvent(EntryReplacedEvent<TimeLimiter> entryReplacedEvent) {
        entryReplacedEvent.getNewEntry().getEventPublisher()
            .onTimeout(event -> log.error("time limiter replaced {} ",
                event.getTimeLimiterName())
            );
      }
    };
  }

En el anterior método hemos definido un listener para los eventos de tipo add, remove y replace. Cada vez que uno de estos tres eventos se produzca, se recogerán las acciones que hemos definido en este método.

Creación de un CircuitBreaker en Resilience4J

A continuación vamos a crear un controlador en el que vamos a definir un circuit breaker, es decir, se abrirá un circuito cada vez que se produzca algún problema de comunicación de los definidos con un umbral preestablecido en la configuración.

De esta manera podremos ver los logs de nuestros eventos definidos anteriormente:

  @GetMapping(value = "/timeDelay/{delay}", produces = MediaType.APPLICATION_JSON_VALUE)
  @CircuitBreaker(name = RESILIENCE4J_INSTANCE_NAME, fallbackMethod = FALLBACK_METHOD)
  public Mono<Response<Boolean>> timeDelay(@PathVariable int delay) {
    return Mono.just(toOkResponse())
        .delayElement(Duration.ofSeconds(delay));
  }

Vamos a forzar la apertura de nuestro circuit breaker, para eso tenemos un umbral configurado del 50% y unas slow call de 2 segundos.

resilience4j.circuitbreaker.configs.shared.failure-rate-threshold=50
resilience4j.circuitbreaker.configs.shared.slow-call-rate-threshold=50
resilience4j.circuitbreaker.configs.shared.slow-call-duration-threshold=2s

A continuación vamos a realizar varias peticiones al siguiente endpoint http://localhost:8080/api/timeDelay/10, una vez el umbral supere al 50% con llamadas que tardan más de 2 segundos, nuestro circuit breaker se abrirá y veremos algo así:

2021-10-23 17:07:19.176 ERROR 16008 --- [     parallel-6] c.n.c.listener.Resilience4jListener      : circuit breaker example slow call rate 100.0 on 2021-10-23T17:07:19.176497500+02:00[Europe/Madrid]
2021-10-23 17:07:19.183  WARN 16008 --- [     parallel-6] c.n.c.listener.Resilience4jListener      : circuit breaker example state transition from CLOSED to OPEN on 2021-10-23T17:07:19.183016+02:00[Europe/Madrid]

Una vez ha transcurrido el tiempo de espera que hemos preestablecido en 10 segundos, el estado del CircuitBreaker cambia de OPEN a HALP_OPEN. En este momento permite un número configurable de llamadas para ver si el backend esta o no disponible. En el momento en el que el número de fallos o llamadas lentas se encuentra por debajo del umbral establecido entonces el estado será de nuevo CLOSED.

Creación de TimeLimiter en Resilience4J

A continuación vamos a hacer uso del timelimiter con Resilience4j para ello vamos a crear un bloqueo por encima del umbral que tenemos preestablecido, de esta manera lograremos un timeout y saltará nuestro evento.

  @GetMapping(
      value = "/timeout/{timeout}",
      produces = MediaType.APPLICATION_JSON_VALUE
  )
  @TimeLimiter(name = RESILIENCE4J_INSTANCE_NAME, fallbackMethod = FALLBACK_METHOD)
  public Mono<Response<Boolean>> timeout(@PathVariable int timeout) {
    return Mono.just(toOkResponse())
        .delayElement(Duration.ofSeconds(timeout));
  }

Ejecuta la siguiente url con nuestro ejemplo ejecutado para poder comprobar el timeout, http://localhost:8080/api/timeout/5

Al ejecutar la url anterior obtendremos la siguiente traza por la consola:

2021-10-23 17:03:30.481 ERROR 16008 --- [     parallel-1] c.n.c.listener.Resilience4jListener      : time limiter example timeout TIMEOUT on 2021-10-23T17:03:30.480110200+02:00[Europe/Madrid]
2021-10-23 17:03:30.484  WARN 16008 --- [     parallel-1] c.n.c.web.Resilience4jController         : fallback executed

Hemos establecido un timelimiter de 2 segundos por lo que al realizar la petición con 5 segundos salta nuestro timelimiter.

Conclusión

Cuando creamos una arquitectura de microservicios, es muy importante hacer uso de un circuit breaker como de un timeout para evitar dejar conexiones e hilos ocupados. En este Ejemplo de Circuit Breaker con Resilience4j en Spring Boot, hemos podido ver como aplicar tanto una limitación de tiempo para nuestra conexión como hacer uso de un circuit breaker con un sencillo ejemplo.

Si quieres echar un ojo al ejemplo completo lo puedes ver en nuestro github.

Si necesitas más información puedes escribirnos un comentario o un correo electrónico a refactorizando.web@gmail.com o también nos puedes contactar por nuestras redes sociales Facebook o twitter y te ayudaremos encantados!


Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *