Trazabilidad en microservicios con Spring Cloud Sleuth

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

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 tiempos y latencias se puede utilizar Zipkin, Prometheus junto con Hystrix o Resilience4j también dan visibilidad de los tiempos entre otras cosas.

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 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.

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";
    }
}
 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());
    }
}

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)

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"

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)
    ...
}

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 el comando ./gradle-run.sh.