En este artículo les escribiré sobre cómo poder tener un mejor monitoreo de nuestros microservicios empleando las facilidades que nos brinda OpenTracing 1.3, el cual es parte de los API de MicroProfile.

Primeramente debemos tener claro que el propósito del proyecto OpenTracing es brindar un API estándar para instrumentar nuestros microservicios con elementos de trazabilidad distribuida. Esto es importante, pues en un ambiente de microservicios una petición normalmente fluje a través de múltiples servicios que pueden ejecutarse en máquinas diferentes, entre centros de datos diferentes y ubicados en sitios geográficamente distantes.

Para poder cumplir este objetivo de trazabilidad distribuida debemos lograr que cada servicio guarde mensajes con un id de correlación que debe ser propagado. Es de esperar que se acompañe de algún servicio que nos permita almacenar todos esos registros de trazabilidad, y en este artículo usaremos Jaeger.

Si se logra lo anterior, seremos capaces de poder determinar la historia de cada petición, en contraposición al estado general de un sistema que alcanzamos usando componentes como métricas y bitácoras.

Alcanzar lo anterior parecería indicar que debemos agregar una cantidad de código importante en nuestros microservicios. Sin embargo, MicroProfile ha logrado hacer que esto sea muy amigable, ya que no tenemos que agregar código explícito para poder tener esta trazabilidad distribuida.

Veamos un poco más en detalle y consideremos nuestro siguiente microservicio:

@Path("direcciones")
public class DireccionesResource {

    @Inject
    DireccionesService dirService;

    @PUT
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response validar(Direccion direccion) {

        if (!dirService.existe(direccion)) {
            return ResponseGenerator.errorNotFound("La dirección no existe");
        } else {
            return Response.status(Response.Status.OK)
                    .type(MediaType.APPLICATION_JSON)
                    .build();
        }
    }
}

Sin cambiar una línea de código podemos tener OpenTracing funcional, pero si debemos agregar a nuestro POM la dependencia adecuada; en mi caso haremos el ejercicio usando Quarkus.

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-opentracing</artifactId>
</dependency>

Y el otro componente es tener a Jaeger funcionando, por simplicidad usaré Docker empleando el proceso que se describe aquí y que corresponde en síntesis al siguiente comando:

docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 9411:9411 \
  jaegertracing/all-in-one:latest

Ahora bien, ya que tenemos a Jaeger funcionando, debemos agregar estas propiedades al archivo application.properties:

quarkus.jaeger.service-name=DireccionesService
quarkus.jaeger.sampler-type=const
quarkus.jaeger.sampler-param=1
quarkus.jaeger.endpoint=http://localhost:14268/api/traces
  • El primer atributo corresponde al nombre que deseamos poner a nuestro servicio.

  • El segundo nos permite determinar el tipo de muestreo que en este caso es constante, pero existen otros tipos como: probabilístico, remoto, o rate limiting. Puedes tener más detalles al respecto aquí.

  • El tercer parámetro nos dice que hará muestreos sobre todas las trazas.

  • Y el último parámetros corresponde al endpoint de Jaeger, que en mi caso tengo corriendo localmente.

Si invocamos nuestro servicio una cuantas veces e ingresamos a la consola de Jaeger, la cual es http://localhost:16686, podemos ver lo siguiente:

Ejemplo

Noten que aparece en la parte superior nuestro servicio junto con todas las operaciones que tenemos en él. Al lado derecho podemos ver las trazas más recientes de acuerdo al filtro que usemos y para cada una de ellas podemos analizar su detalle. Por ejemplo:

Ejemplo

Aquí podemos observa como nuestro servicio internamente hace llamado a una función para verificar la existencia de una dirección. Esto nos permite tener una traza completa de nuestras invocaciones y es indispensable cuando queremos analizar problemas de rendimiento o cuellos de botella en nuestros servicios. De no contar con ello, se nos dificultaría considerablemente realizar esas labores.

Antes de continuar; es necesario explicar tres conceptos básicos OpenTracing:

  • Tracer: Un tracer es un Singleton que tenemos disponible que puede emplearse para modelar una unidad de trabajo creando un Span.
  • Span: Un spanmodela una invocación en el sistema (por ejemplo: una petición o la llamada a un método). A un conjunto de spans se le llama traza y representa una invocación completa de inicio a fin. Un span también almacena información de tiempo, tags, logs.
  • SpanContext: Es un objeto que encapsula metada de contexto e información de causalidad.

¿Qué pasa si nuestro servicio consume otro microservicio totalmente diferente al que creamos?

Esa es una excelente pregunta, como mencione al inicio, el objetivo de OpenTracing es precisamente ayudarnos con un ambiente distribuido. Pues bien, si nuestro servicio tiene que consumir otro microservicio, el cual ya tiene configurado OpenTracing podemos observar algo como esto en Jaeger:

Ejemplo

Observen como ahora podemos analizar en detalle el tiempo que se consume en ese otro servicio (PaisesService) -el cual se ejecuta en otra máquina-, incluso al nivel de las llamadas internas que están siendo trazadas. Y si este a su vez consumiera a otro servicio, podemos seguir revisando la cadena de invocaciones en cadena. Y esto nos brinda una herramienta invaluable con un esfuerzo mínimo del lado de nuestra programación.

Supongamos que ahora tenemos que agregar metadata adicional que sea de nuestro interés; eso se logra de la siguiente manera:

@Inject
io.opentracing.Tracer tracer;
....
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response validar(Direccion direccion) {
  ...
  tracer.activeSpan().setTag("codigoPais", direccion.getCodigoPais);
  ...
}

Si ejecutamos nuestro servicio y revisamos en Jaeger; notaremos que ya está presente este nuevo metadato.

Ejemplo

Conclusión


Espero que este artículo les permita conocer los beneficios que nos puede dar OpenTracing y cuan sencillo es hacer uso del mismo en MicroProfile.