Trazabilidad Distribuida con Spring Cloud: Sleuth y Zipkin

 El auge de las arquitecturas de microservicios ha traído consigo algunos retos que debemos ser capaces de abordar para conseguir un sistema consistente, como son la monitorización, gestión de la configuración centralizada, centralización de logs…

En este tipo de arquitecturas una petición de un consumidor de nuestro sistema puede desencadenar varias llamadas internas entre microservicios, por lo que es importante poder asociar un identificador único de petición para que se pueda propagar entre estas llamadas y así poder posteriormente consultar las peticiones de forma centralizada. Spring pone a nuestra disposición herramientas que nos facilitan este trabajo: Spring Cloud Sleuth y Zipkin.

En este post explicaremos la forma de almacenar y explotar de forma centralizada la trazabilidad de peticiones a nuestros servicios utilizando estas herramientas.

trazabilidad-distribuida

Spring Cloud Sleuth

La primera pieza de la ecuación para conseguir identificar inequívocamente una petición es Spring Cloud Sleuth. Es una librería que implementa una solución de trazado distribuido para Spring Cloud. Con tan solo incluir la dependencia con Sleuth en los microservicios, dotamos al ecosistema de un mecanismo automático de identificación de peticiones, ya que añade varios campos útiles a las mismas para identificarlas.

De estos campos extra, los más importantes son traceId y spanId (son los que realmente aseguran una traza única), los cuales incluye en las salida de log del microservicio. El sistema de trazado utilizado por Sleuth se basa en terminología Dapper, cuyos principales conceptos son:

  • Trace: Un conjunto de spans que forman una estructura de árbol de llamadas, forma la traza de la petición.
  • Span: Es la unidad básica de trabajo, por ejemplo una llamada a un servicio. Se identifican con un id de span y un id de trace a la que pertenece dicho span. Tienen inicio y fin, y con ello se consigue trackear el tiempo de respuesta entre peticiones.
  • Annotation: Se usa para grabar un evento en el tiempo. Las anotaciones más importantes que usa internamente son:
    • cs (Client Sent): El cliente ha hecho una petición. Inicio del span.
    • sr (Server Received): El servidor ha recibido la petición y empieza su procesado. timestampsr – timestampcs = latencia de red.
    • ss (Server Sent): Envío a cliente desde el servidor de la respuesta. timestampss – timestampsr = tiempo de procesamiento de petición en servidor.
    • cr (Client Received): Fin del span. El cliente ha recibido correctamente la respuesta del servidor. timestampcs – timestampcr = tiempo total de la petición.
  • Tag: Par clave/valor que identifica cierta información en el span. No contiene timestamps, simplemente identifica.

A continuación se muestra un diagrama de cómo se comportaría una petición en una llamada entre dos microservicios que hacen uso de Sleuth:

trazabilidad-distribuida

Además de generar los identificadores únicos y añadirlos a los logs de la aplicación, es necesario que se propaguen de forma correcta entre los microservicios que forman parte de la petición.

Para ello incluye una capa que gestiona automáticamente dicha propagación de los datos de trazado de Sleuth entre los sistemas más comunes de comunicación entre microservicios Spring Cloud, como son los RestTemplate, servlets, filtros Zuul, clientes feign, mensajería…

A continuación se presentan las posibilidades de implementación de la trazabilidad que ofrece Sleuth.

Implementaciones Spring Cloud Sleuth

Spring initializr es una herramienta que Spring ha puesto a nuestra disposición para agilizar la creación de aplicaciones Spring Boot. Según la misma, existen dos posibles dependencias Sleuth:

trazabilidad-distribuida

  • Sleuth: Dota a la aplicación de correlación de trazas de log entre distintos microservicios mediante cabeceras HTTP. Para tenerlo disponible, se debe incluir la siguiente dependencia en el proyecto:
<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

 

Una vez incluida se lanza una petición con un sistema de llamadas compatible con Sleuth, como puede ser RestTemplate:

ResponseEntity<String> response  =
restTemplate.getForEntity("http://localhost:8089/getName", String.class);

 

Por cada petición que trace contendrá identificadores para definirla unívocamente, como se ve en la imagen (5501a4bacd8faaa8 el traceId y 44d49402e8b56e98 el spanId del servicio llamante):

Como se ha comentado anteriormente, estos identificadores se envían de un servicio a otro por cabeceras HTTP:

  • Sleuth StreamAdemás de dotar de correlación de mensajes, incluye las trazas en un binder de Spring Cloud Stream para su transmisión. Existen varias posibilidades de broker sobre el que enviar las trazas, Kafka, Redis o RabbitMQ. Para disponibilizarlo en la aplicación, se debe incluir la dependencia:
<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-sleuth-stream</artifactId>
</dependency>

 

Además de la dependencia con el broker, en este caso RabbitMQ:

<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

 

A continuación se define el canal de comunicación tanto en el emisor como en el receptor:

spring:
  cloud:
    stream:
      bindings:
        sleuth-stream-save:
          destination: sleuth-stream-save
          group: sleuth-stream
          content-type: application/json

 

  • Emisor
public interface SleuthStreamProcessor {
       String SLEUTH_SAVE = "sleuth-stream-save";
	
       @Output(SleuthStreamProcessor.SLEUTH_SAVE)
       MessageChannel sleuthSave();
}

 

  • Receptor
public interface SleuthStreamProcessor {
       String SLEUTH_SAVE = "sleuth-stream-save";
	
       @Input(SleuthStreamProcessor.SLEUTH_SAVE)
       SubscribableChannel sleuthSave();
}

 

Y como se ve en las imágenes, el traceId y el spanId se propaga en la cabecera del mensaje enviado a RabbitMQ:

Zipkin

La segunda pieza de la ecuación es Zipkin. Si con Sleuth se consigue identificar los saltos entre microservicios de una petición en concreto, con Zipkin se consigue un sistema completo de trazabilidad distribuida para microservicios Spring Cloud: generación de trazas con ids únicos, almacenamiento de las mismas y posterior visualización en una interfaz gráfica para su estudio. Abraza la funcionalidad de Spring Cloud Sleuth y la amplía para formar un sistema completo de monitorización de peticiones entre microservicios.

Su objetivo es consultar la salud del ecosistema. Si se quiere tener un procesado a más alto nivel, se debe utilizar en conjunto con algún sistema de agregación de logs como Kibana o Splunk ya que estos sistemas permiten explotar las trazas de forma mucho más intensiva y personalizada.

Implementaciones Zipkin

Como Zipkin se apoya en Sleuth, tiene dos posibles sistemas de envío de trazas al servidor, mediante cabeceras HTTP y con brokers de mensajería. Según Spring Initializr estas son las dependencias publicadas para Zipkin:

trazabilidad-distribuida

  • Cabeceras HTTP: Si se quiere seguir una aproximación con cabeceras HTTP, la estructura de dependencias es la siguiente:

trazabilidad-distribuida

En los microservicios proveedores de trazas, se debe incluir la dependencia con el Zipkin Client (spring-cloud-starter-zipkin) el cual tiene embebida la dependencia con spring-cloud-sleuth-zipkin que compatibiliza las trazas Sleuth con las que espera recibir Zipkin.

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

 

Una vez generadas las trazas, se envían en la cabecera HTTP al Zipkin Server (zipkin-server).

<dependency>
    <groupId>io.zipkin.java</groupId>
    <artifactId>zipkin-server</artifactId>
</dependency>

 

Si no se le especifica ninguna forma de almacenarlas, las deja en memoria. Esta es una mala práctica si se quiere implantar este sistema de trazabilidad en producción, por lo que se debería especificar una base de datos, como puede ser Elasticsearch (zipkin-autoconfigure-storage-elasticsearch).

<dependency>
    <groupId>io.zipkin.java</groupId>
    <artifactId>zipkin-autoconfigure-storage-elasticsearch</artifactId>
    <version>1.2.1</version>
</dependency>

 

Se deben configurar las propiedades de conexión en el zipkin-server:

zipkin:
  storage:
    type: elasticsearch
    elasticsearch:
      cluster: elasticsearch
      hosts: localhost:9300
      index: zipkin
      index-shards: 5
      index-replicas: 1

 

Zipkin crea índices por fecha en Elasticsearch para mejorar su gestión:

Para gestionar el volumen de información que se almacena en elastic, el creador recomienda eliminar periódicamente estos índices con Elastic Curator.

Para que una aplicación Spring Boot se convierta en un Zipkin Server tan solo hace falta incluir la anotación @EnableZipkinServer.

  • Brokers de mensajería: Si por el contrario se desea enviar las trazas al servidor mediante un binder de Spring Cloud Stream el sistema se conforma de la siguiente manera:

trazabilidad-distribuida

Los microservicios de los que se desea obtener las trazas deben contener la dependencia con Sleuth Stream el cual enviará las mismas al broker configurado.

Por su parte, el Zipkin Stream (spring-cloud-sleuth-zipkin-stream) actúa de la misma forma que el Zipkin Server, pero recogiendo las trazas del broker (se debe incluir la dependencia con el mismo) y almacenándolas en memoria si no se especifica nada o en una base de datos si se especifica. 

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-sleuth-zipkin-stream</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

 

Para empezar a trabajar no es necesaria ninguna acción de creación de colas, ya que el propio Sleuth crea automáticamente todo lo necesario. Al ejecutar la aplicación se encarga de gestionarlo.

Con la anotación @EnableZipkinStreamServer dotas a una aplicación Spring Boot de ésta funcionalidad. Cabe destacar que existe un api sobre el que ejecutar consultas al Zipkin Server.

Las peticiones mas importantes son:

curl   http://localhost:9411/api/v1/services
curl   http://localhost:9411/api/v1/traces?serviceName=<service-name>
curl   http://localhost:9411/api/v1/trace/<id>
curl   http://localhost:9411/api/v1/dependencies?endTs=$(date +%s)

 

A continuación se muestra un ejemplo de traza en una llamada entre dos microservicios, donde zipkin-client y zipkin-client-response son los nombres de los microservicios, 994f6a06f0193d35 es el traceId y a079a9da1fcc2960 y 1629c34056ef5226 son los correspondientes spanId. El campo true indica si la traza se exporta o no a Zipkin, es decir, si se envía al Zipkin Server para ser tratada, o solo aparece en los logs.

trazabilidad-distribuida

 

De cara a mejorar el rendimiento y no sobrecargar el sistema de logs, el sistema por defecto envía un 10% de las trazas generadas al Zipkin Server. Si se desea cambiar este porcentaje, se debe modificar la siguiente propiedad: spring.sleuth.sampler.percentage = 0.2

Zipkin UI

Zipkin provee de una interfaz gráfica que agiliza las consultas a la BBDD del Zipkin Server y muestra la información recogida. Para tenerla disponible, simplemente hay que incluir la siguiente dependencia zipkin-autoconfigure-ui en la aplicación.

Zipkin UI permite filtrar las trazas para obtener un desgranado de las mismas. Una vez obtenido el rango de trazas que deseamos estudiar, se puede ver en detalle cada una de las trace y los spans que las componen.

Trace generados en el intervalo:

trazabilidad-distribuida

Spans de uno de los trace:

trazabilidad-distribuida

Annotation (cs, sr, ss, cr) y Tags de la petición:

trazabilidad-distribuida

Además, genera un grafo de la comunicación de los servicios en dicha traza. Haciendo clic en los elementos obtenemos información ampliada.

trazabilidad-distribuida

A la hora de implementar una solución basada en microservicios se deben abordar ciertas peculiaridades que con aplicaciones monolíticas no ocurren, ya que al ser sistemas descentralizados el control de la comunicación entre los diferentes componentes es muy importante.

Al utilizar las herramientas Spring Cloud mencionadas no solo nos aportará la necesaria correlación de logs de una petición entre distintos microservicios, sino que también proporciona una serie de herramientas para poder visualizar la salud de nuestro ecosistema.

4 comentarios

  1. Jorge Alonso dice:

    Muy interesante artículo, Eduardo, lo mejor que he visto en español sobre Sleuth y Zipkin.

    En nuestra empresa hemos utilizado Sleuth y Zipkin a pequeña escala. Pero el verdadero potencial de la trazabilidad distribuida lo obtienes cuando todas las aplicaciones del sistema (o incluso de la compañía) envían información de trazabilidad, de modo que se puede hacer un seguimiento global, de principio a fin, de una petición. En ese sentido, no he encontrado ningún artículo que hable de cómo desplegar Zipkin en producción, en un sistema real, con la necesidad de gestionar un enorme volumen de peticiones. Es decir, no he localizado información de topologías de despliegue reales y a cuántas peticiones se enfrentas esas topologías. ¿Has encontrado algo al respecto?

    • Eduardo Gonzalez dice:

      Buenos días Jorge, muchas gracias por tu comentario.

      El entorno ideal de despliegue tanto de zipkin como del resto de microservicios corporativos sería dentro de contenedores de software como puede ser Docker sobre una infraestructura Cloud ya sea pública (AWS, Google Cloud…) o privada (Openshift…). De esta manera se aprovecha la capacidad de estos sistemas en cuanto a autoescalado y aprovisionamiento sencillo de recursos para hacer frente a grandes volúmenes de peticiones.

      Un saludo

  2. Ricardo Morales dice:

    Que tal, La verdad muy bueno tu post.

    Me gustaría saber si tienes documentación o algún post donde se explique la instalación y puesta en marcha en OpenShift. Muchas gracias.

    Todo el blog es genial.

Escribe un comentario