Trazabilidad en microservicios con Spring Cloud Sleuth

Escrito por el , actualizado el .
java planeta-codigo
Enlace permanente Comentarios

Los microservicios son independientes pero se llaman unos a otros, suele ser muy útil para tareas de depuración y de visibilidad de una petición disponer de la traza completa de una petición a lo largo de las llamadas entre varios microservicios. Spring Cloud Sleuth proporciona la infraestructura para que las peticiones salientes envíen un identificativo de correlación de la petición global y para las peticiones entrantes relacionarlo con la petición global.

Spring

Java

En una aplicación distribuida con varios microservicios es imprescindible tener la configuración de forma centralizada que cada microservicio obtiene al iniciarse y disponer de registro y descubrimiento para que los servicios al iniciarse, terminarse, actualizarse o por un fallo se registren o desregistren y obtengan la ubicación de las dependencias que necesitan.

Otra de las funcionalidades esenciales en una aplicación distribuida es la trazabilidad de una petición, desde que entra por el API gateway pasando por las diferentes peticiones que hacen los microservicios por la red o envío de mensajes. Es necesaria la funcionalidad que relacione las trazas de todos los servicios para depuración o consulta en un futuro para dar visibilidad a las acciones que se realizan en el sistema.

¿Como se consigue relacionar las trazas de los microservicios que son independientes? La técnica que se emplea es asignar a cada petición entrante un identificativo, más bien un identificativo para la transacción de forma global y un identificativo para la transacción en cada microservicio que varía en cada comunicación de red.

Cuando un microservicio se comunica con otro envía en su petición el identificativo de la transacción global y el de su transacción. Si un microservicio no recibe estos identificativos los genera. En el protocolo HTTP estos identificativos se envían y reciben a través de las cabeceras. Los identificativos permiten correlacionar todas las trazas que emiten los diferentes procesos de los microservicios de una misma petición en la aplicación, haciendo una búsqueda global por el identificativo global se obtiene el conjunto de trazas que han emitido los microservicios por las que ha transitado una petición.

Para obtener mejor visibilidad de los servicios invocados en una transacción y los tiempos y latencias se puede utilizar Zipkin y Prometheus.

En Java el proyecto Spring Cloud Sleuth proporciona la funcionalidad de trazabilidad. En el esquema se observa como Sleuth envía las cabeceras de un servicio cliente a un servicio servidor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
   Client Span                                                Server Span
┌──────────────────┐                                       ┌──────────────────┐
│                  │                                       │                  │
│   TraceContext   │           Http Request Headers        │   TraceContext   │
│ ┌──────────────┐ │          ┌───────────────────┐        │ ┌──────────────┐ │
│ │ TraceId      │ │          │ X─B3─TraceId      │        │ │ TraceId      │ │
│ │              │ │          │                   │        │ │              │ │
│ │ ParentSpanId │ │ Extract  │ X─B3─ParentSpanId │ Inject │ │ ParentSpanId │ │
│ │              ├─┼─────────>│                   ├────────┼>│              │ │
│ │ SpanId       │ │          │ X─B3─SpanId       │        │ │ SpanId       │ │
│ │              │ │          │                   │        │ │              │ │
│ │ Sampled      │ │          │ X─B3─Sampled      │        │ │ Sampled      │ │
│ └──────────────┘ │          └───────────────────┘        │ └──────────────┘ │
│                  │                                       │                  │
└──────────────────┘                                       └──────────────────┘
sleuth-headers.txt

Sleuth se encarga de propagar las cabeceras del servicio cliente al servicio servidor automáticamente instrumentando los clientes HTTP de RestTemplate, AsyncRestTemplate, WebClient, Apache HttpClient y Netty HttpClient. Para enviar, recibir, obtener y establecer los identificativos de correlación con Sleuth junto con el cliente HTTP de Java hay que hacer la instrumentación manualmente con las clases Tracing y Tracer si no está entre los soportados como en el caso del cliente HTTP que se añadió en Java 11 en el propio JDK con el soporte para HTTP/2.

En la parte servidora Sleuth proporciona un filtro que se encarga de obtener y crear el span de la petición que contiene los identificativos de correlación que con Spring y las dependencias adecuadas se configura automáticamente. Para inyectar y extraer las cabeceras de Sleuth con el cliente HTTP de Java o como en el ejemplo con el de Jersey basta con proporcionar una lambda que realice el añadido o extracción de las cabeceras con la API del cliente.

Ejemplo de microservicio con Spring Boot y Sleuth

Este es el código para instrumentalizar el cliente HTTP de Jersey que utiliza el servicio cliente que invoca al gateway y el código para crear el span en el cliente con los identificativos de correlación y recogerlos en el servicio servidor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package io.github.picodotdev.blogbitix.springcloud.client;

...

@Component
public class ProxyService {

    @Autowired
    private LoadBalancerClient loadBalancer;

    @Autowired
    private Tracing tracing;

    @Autowired
    private Tracer tracer;

    @HystrixCommand(fallbackMethod = "getFallback", commandProperties = {
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "4"),
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "25000")
    })
    public String get() {
        ServiceInstance instance = loadBalancer.choose("proxy");
        URI uri = instance.getUri();
        String resource = String.format("%s%s", uri.toString(), "/service");
        Invocation.Builder builder = ClientBuilder.newClient().target(resource).request();

        Span span = tracer.newTrace().start();
        TraceContext.Injector<Invocation.Builder> injector = tracing.propagation().injector((Invocation.Builder carrier, String key, String value) -> { carrier.header(key, value); });
        injector.inject(span.context(), builder);
        System.out.printf("Proxy Span (traceId: %s, spanId: %s)%n", span.context().traceIdString(), span.context().spanIdString());

        return builder.get().readEntity(String.class);
    }

    private String getFallback() {
        return "Fallback";
    }
}
ProxyService.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package io.github.picodotdev.blogbitix.springcloud.service;

...

@RestController
public class DefaultController {

	@Autowired
	private DefaultConfiguration configuration;

	@Autowired
	private Tracing tracing;

	@Autowired
	private Tracer tracer;

	private Random random;
	private Counter counter;

	public DefaultController(MeterRegistry registry) {
		this.random = new Random();
		this.counter = Counter.builder("service.invocations").description("Total service invocations").register(registry);
	}

	@RequestMapping("/")
	public String home(HttpServletRequest request) throws Exception {
		counter.increment();

		// Timeout simulation
		//Thread.sleep(random.nextInt(2000));

		TraceContext.Extractor<HttpServletRequest> extractor = tracing.propagation().extractor((HttpServletRequest carrier, String key) -> { return carrier.getHeader(key); });
		Span span = tracer.nextSpan(extractor.extract(request));
		System.out.printf("Client Span (traceId: %s, spanId: %s)%n", span.context().traceIdString(), span.context().spanIdString());

		return String.format("Hello world (%s, %s)", request.getRequestURL(), configuration.getKey());
	}
}
DefaultController.java

He utilizado el ejemplo de la serie de artículos sobre Spring Cloud añadiendo el soporte para Spring Cloud Sleuth. La aplicación se compone de un microservicio de configuración (con Spring Cloud Config), otro de registro y descubrimiento (con Eureka), un servicio de API gateway (con Zuul), el servicio de aplicación y un cliente del servicio que envía las peticiones al gateway y este las redirige al servicio de aplicación.

El cliente inicia un span que es enviado al servidor y el servidor obtiene las cabeceras enviadas. El cliente y el servidor son dos procesos distintos del sistema pero se observa que el identificativo global de la transacción traceId se mantiene en ambos y el identificativo de spanId cambia entre el cliente y el servidor.

1
2
3
4
5
Client Span (traceId: 94fcd131178298fd, spanId: 94fcd131178298fd)
...
Service Span (traceId: 94fcd131178298fd, spanId: 6e6380e239f30917)
...
Service response: Hello world (http://archlinux:8080/, value)
System.out

Para iniciar los diferentes microservicios de la aplicación hay que utilizar los siguientes comandos.

1
2
3
4
5
$ ./gradlew discoveryserver:run --args="--port=8761"
$ ./gradlew configserver:run --args="--port=8090"
$ ./gradlew service:run --args="--port=8080"
$ ./gradlew proxy:run --args="--port=8085"
$ ./gradlew client:run --args="--service=proxy"
gradle-run.sh

En los proyectos hay que incluir la dependencia para Sleuth en la herramienta de construcción.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
plugins {
    id 'application'
}

mainClassName = 'io.github.picodotdev.blogbitix.springcloud.client.Main'

dependencies {
    implementation platform("org.springframework.boot:spring-boot-dependencies:2.1.4.RELEASE")
    implementation platform("org.springframework.cloud:spring-cloud-dependencies:Greenwich.SR1")

    // Spring
    def excludeSpringBootStarterLogging = { exclude(group: 'org.springframework.boot', module: 'spring-boot-starter-logging') }
    compile('org.springframework.boot:spring-boot-starter', excludeSpringBootStarterLogging)
    ...
    compile('org.springframework.cloud:spring-cloud-starter-sleuth', excludeSpringBootStarterLogging)
    ...
}
build.gradle
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradle-run.sh


Este artículo forma parte de la serie spring-cloud:

  1. Datos de sesión externalizados con Spring Session
  2. Aplicación Java autocontenida con Spring Boot
  3. Configuración de una aplicación en diferentes entornos con Spring Cloud Config
  4. Información y métricas de la aplicación con Spring Boot Actuator
  5. Registro y descubrimiento de servicios con Spring Cloud y Consul
  6. Aplicaciones basadas en microservicios
  7. Registro y descubrimiento de servicios con Spring Cloud Netflix
  8. Recargar sin reiniciar la configuración de una aplicación Spring Boot con Spring Cloud Config
  9. Almacenar cifrados los valores de configuración sensibles en Spring Cloud Config
  10. Tolerancia a fallos en un cliente de microservicio con Spring Cloud Netflix y Hystrix
  11. Balanceo de carga y resiliencia en un microservicio con Spring Cloud Netflix y Ribbon
  12. Proxy para microservicios con Spring Cloud Netflix y Zuul
  13. Monitorizar una aplicación Java de Spring Boot con Micrometer, Prometheus y Grafana
  14. Exponer las métricas de Hystrix en Grafana con Prometheus de una aplicación Spring Boot
  15. Servidor OAuth, gateway y servicio REST utilizando tokens JWT con Spring
  16. Trazabilidad en microservicios con Spring Cloud Sleuth
  17. Implementar tolerancia a fallos con Resilience4j
  18. Iniciar una aplicación de Spring Boot en un puerto aleatorio
  19. Utilizar credenciales de conexión a la base de datos generadas por Vault en una aplicación de Spring
  20. Microservicios con Spring Cloud, Consul, Nomad y Traefik
  21. Trazabilidad en servicios distribuidos con Sleuth y Zipkin
  22. Configuración de una aplicación con Spring Boot y configuración centralizada con Spring Cloud Config
Comparte el artículo: