En un artículo anterior explique como crear microservicios tolerantes a fallas empleandos tres patrones diferentes.

Ahora como un complemento al mismo, les explicaré el uso de Resilience4J como librería para implementar esos patrones y la manera en visualizar las métricas que podemos obtener de ella a fin de optimizar nuestra parametrización. En un artículo posterior veremos como implementar estos patrones pero empleando MicroProfile.

Resilience4J

Para poder utilizar Resilience4J en nuestro proyecto, debemos agregar varias dependiencias a nuestro archivo pom. En particular usaremos las siguientes de la versión 0.17.0, la cual es la más reciente al momento de escribir este artículo.

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-circuitbreaker</artifactId>
    <version>${resilience4jVersion}</version>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-retry</artifactId>
    <version>${resilience4jVersion}</version>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-bulkhead</artifactId>
    <version>${resilience4jVersion}</version>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-all</artifactId>
    <version>${resilience4jVersion}</version>
</dependency>

Resilience4J tiene la ventaja que es modular, por lo cual para nuestro caso agregamos las dependencias para: Circuit Breaker, Retry y Bulkhead.

Ahora bien, como primer paso vamos a configurar un Circuit Breaker que pasa el estado abierto si hay un 20% de fallas y un mínimo de 2 intentos de llamada.

CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
        .failureRateThreshold(20)
        .ringBufferSizeInClosedState(2)
        .build();

Luego, creamos un registro con base en la configuración previa e instanciamos un CircuitBreaker con un nombre. Este nombre que seleccionemos es importante, pues es el que observaremos en la sección de monitoreo.

CircuitBreakerRegistry circuitBreakerRegistry
        = CircuitBreakerRegistry.of(circuitBreakerConfig);

CircuitBreaker circuitBreaker = circuitBreakerRegistry
    .circuitBreaker("backendService");

Haremos un proceso similar con el Retry, esta vez con su configuración default de tres intentos cada 500 ms.

RetryRegistry retryRegistry = RetryRegistry.ofDefaults();
Retry retry = retryRegistry.retry("backendService");

Y para el caso del Bulkhead, es un proceso equivalente.

BulkheadRegistry bulkheadRegistry = BulkheadRegistry.ofDefaults();
Bulkhead bulkhead = bulkheadRegistry.bulkhead("backendService");

Ahora bien, para decorar nuestro llamado al WS debemos hacer lo siguiente:

// Vamos a suponer que llamamos a una funcion invocarMicroServicio que retorna un String
Supplier<String> supplier = () -> this.invocarMicroServicio();

// Y ahora la decoramos con el Retry, CircuitBreaker y Bulkhead que definimos anteriormente.
Supplier<String> decoratedSupplier =
   Decorators.ofSupplier(supplier)
        .withRetry(retry)
        .withCircuitBreaker(circuitBreaker)
        .withBulkhead(bulkhead)
        .decorate();

// Y por último llamamos a la función que decoramos. Observen que tenemos un recover, para
// retornar un valor en caso de problemas.
String result = Try.ofSupplier(decoratedSupplier)
        .recover(throwable -> "Respuesta por Omisión").get();        

Realizado este paso ya estaríamos implementando Resilience4J.

Prometheus

Surge ahora la interrogante: ¿Cómo podemos ver las métricas de Resilience4J?.

La respuesta es sencilla, esta libreria ya cuenta con un mecanismo para utilizar micrometer y envíar todas las métricas a Prometheus. El primer paso es agregar al pom las siguientes dependencias.

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-micrometer</artifactId>
    <version>${resilience4jVersion}</version>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-core</artifactId>
    <version>1.2.0</version>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
    <version>1.2.0</version>
</dependency>

Luego, en nuestro código debemos crear una instancia de PrometheusMeterRegistry y registrar las métricas de nuestro CircuitBreaker, Retry y Bulkhead


@Inject
private PrometheusService prometheusService;
....

PrometheusMeterRegistry meterRegistry = prometheusService.getInstance();
....

// Registramos todos a la vez, esto antes de decorar nuestra función.
TaggedCircuitBreakerMetrics
        .ofCircuitBreakerRegistry(circuitBreakerRegistry)
        .bindTo(meterRegistry);
TaggedRetryMetrics
        .ofRetryRegistry(retryRegistry)
        .bindTo(meterRegistry);
TaggedBulkheadMetrics
        .ofBulkheadRegistry(bulkheadRegistry)
        .bindTo(meterRegistry);        

Para este ejemplo y por facilidad, cree un Singleton donde realizamos la instanciación correspondiente.

@Singleton
@Startup
public class PrometheusService {

    private PrometheusMeterRegistry meterRegistry = null;

    public PrometheusMeterRegistry getInstance() {
        if (meterRegistry == null) {
            meterRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
        }

        return meterRegistry;
    }
}

Ahora bien, para poder exponer estas métricas necesitamos brindar un endpoint para Prometheus. Para ilustrarlo usaremos un servlet, de la siguiente manera:

@WebServlet(name = "PrometheusServlet", urlPatterns = "/metrics")
public class PrometheusServlet extends HttpServlet {

    @Inject
    private PrometheusService prometheusService;

    private static final Logger LOG = Logger.getLogger(PrometheusServlet.class.getName());

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        PrometheusMeterRegistry prometheusRegistry = prometheusService.getInstance();

        resp.setStatus(HttpServletResponse.SC_OK);
        resp.setContentType(TextFormat.CONTENT_TYPE_004);
        try (Writer writer = resp.getWriter()) {
            writer.write(prometheusRegistry.scrape());
            writer.flush();
        }
    }
}

El método scrape de PrometheusMeterRegistry retorna la información en el formato que requerimos.

Configurar Prometheus

Para poder agregar estras métricas a Prometheus, necesitamos ajustar el archivo prometheus.yml agregando un nuevo job.

- job_name: DemoApp
  honor_timestamps: true
  scrape_interval: 15s
  scrape_timeout: 10s
  metrics_path: /patrones/metrics
  scheme: http
  static_configs:
  - targets:
    - 192.168.3.15:8080

En este ejemplo, debemos ajustar el atributo metrics_path y el target según corresponda.

Una vez ajustado el archivo, iniciamos Prometheus con ese nuevo archivo de configuración, si usamos Docker sería de la siguiente manera:

docker run -p 9090:9090 -v d:/configs/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus

Cuando hemos configurado lo anterior y ejecutado nuestro servicio, las métricas son enviadas a Prometheus. En la siguiente imágen podemos observar una de ellas.

Ejemplo

Grafana

Para poder poder visualizar toda la información recolectada en Prometheus de una manera gráfica es el momento de hacer uso de Grafana.

Una vez que lo hemos instalado, el paso inicial es registrar un nuevo datasource.

Ejemplo

Aquí debemos indicar la dirección y puerto donde instalamos Prometheus.

Y luego crear un nuevo dashboard. Afortunadamente Resilience4J ya nos brinda un ejemplo de este aquí, por lo cual solo debemos importarlo a nuestro Grafana.

Las métricas que podemos observar para un CircuitBreaker son:

Ejemplo

Noten que en la parte superior podemos filtrar según el componente de nuestro interés. Por ello es relevante el nombre con el cual registramos cada uno de nuestros CircuitBreakers, Retry, Bulkhead, etc.

Y para el Retry tenemos las siguientes:

Ejemplo

Conclusión


Espero que este artículo aclare los aspectos más importantes referentes a cómo extrar las métricas de Resilience4J y poder graficarlas de una manera sencilla, lo cual nos permite optimizar nuestra configuración y entender mejor los sistemas que desarrollamos. En un próximo artículo veremos como hacer lo anterior pero usando las facilidades de MicroProfile.