Spring Cloud Sleuth链路日志不完整导致追踪失败的排查方法

Spring Cloud Sleuth 链路日志不完整导致追踪失败的排查方法

大家好,今天我们来聊聊在使用 Spring Cloud Sleuth 进行分布式链路追踪时,经常遇到的一个问题:链路日志不完整,导致追踪失败。这个问题可能会让你花费大量时间去排查,所以掌握一些排查思路和方法非常重要。

1. 理解链路追踪的基本原理

在深入排查之前,我们需要先理解 Sleuth 的基本工作原理。Sleuth 的核心是利用 TraceIdSpanId 来关联一次请求在不同服务之间的调用链。

  • TraceId: 代表一次完整的请求链路,它在整个调用链中保持不变。
  • SpanId: 代表一次独立的调用,例如一个 HTTP 请求、一个数据库查询等。每个 Span 都有一个父 SpanId,用于表示调用关系。根 Span (Root Span) 没有父 SpanId。

Sleuth 通过 Spring Cloud Context 组件传递这些 ID。Context 组件会自动将 TraceId 和 SpanId 注入到 HTTP Header、消息队列的 Message Header 等,从而在服务之间传播。

当链路日志不完整时,通常意味着某些 Span 丢失了,导致无法还原完整的调用链。

2. 链路日志不完整的常见原因

链路日志不完整的原因有很多,但可以归纳为以下几类:

  • 服务没有正确配置 Sleuth: 这是最常见的原因。如果服务没有正确引入 Sleuth 依赖、没有正确配置 application.propertiesapplication.yml 文件,或者配置有误,Sleuth 就无法正常工作。
  • Context 传递失败: 在服务调用过程中,TraceId 和 SpanId 没有正确传递。这可能是由于使用了不支持 Context 传递的框架、或者配置了错误的 Context 传播策略。
  • 异步调用丢失 Context: 在使用线程池、消息队列等异步方式进行调用时,如果没有正确地传递 Context,会导致异步线程无法获取到 TraceId 和 SpanId,从而产生新的没有关联的链路。
  • 采样率设置过低: Sleuth 默认的采样率是 10%,这意味着只有 10% 的请求会被追踪。如果采样率设置过低,可能会导致关键请求没有被追踪到。
  • 日志丢失: 日志配置不当,导致链路日志没有被正确地收集和存储。
  • 异常未处理: 如果服务在处理请求时发生了未捕获的异常,Sleuth 可能会无法记录 Span 的结束时间,导致 Span 不完整。
  • 自定义组件集成不当: 如果自定义了组件,例如自定义的 HTTP 客户端或消息队列消费者,并且没有正确地集成 Sleuth,会导致这些组件产生的 Span 无法关联到主链路。

3. 排查步骤与方法

现在,我们来详细介绍排查链路日志不完整的步骤和方法。

步骤 1: 检查 Sleuth 依赖和配置

首先,确保所有参与链路追踪的服务都正确引入了 Sleuth 依赖。通常,只需要在 pom.xmlbuild.gradle 文件中添加以下依赖:

<!-- Maven -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

<!-- Gradle -->
implementation 'org.springframework.cloud:spring-cloud-starter-sleuth'

然后,检查 application.propertiesapplication.yml 文件中是否包含了必要的配置。以下是一些常用的 Sleuth 配置:

配置项 描述 默认值
spring.sleuth.sampler.probability 采样率,取值范围为 0 到 1,1 表示全部采样,0 表示不采样。 0.1 (10%)
spring.sleuth.sampler.percentage 采样率,取值范围为 0 到 100,100 表示全部采样,0 表示不采样。此属性优先级高于 spring.sleuth.sampler.probability,如果同时设置,会优先使用此属性。 null (不设置)
spring.sleuth.log.slf4j.enabled 是否启用 Sleuth 的日志集成。 true
spring.sleuth.integration.enabled 是否启用 Sleuth 的集成。 true
spring.sleuth.propagation.type 上下文传播类型。常用的类型有 B3 (默认) 和 W3C B3
spring.sleuth.baggage.enabled 是否启用 Baggage。Baggage 允许在链路中传递自定义的元数据。 false
spring.sleuth.baggage.remote-fields 需要远程传播的 Baggage 字段列表。 空列表
spring.sleuth.trace-id128 是否使用 128 位的 TraceId。默认情况下,Sleuth 使用 64 位的 TraceId。 false

一个典型的 application.yml 配置如下:

spring:
  application:
    name: your-service-name
  sleuth:
    sampler:
      probability: 1.0 # 全部采样
    propagation:
      type: B3 # 使用 B3 传播格式
    trace-id128: true # 使用 128 位 TraceId

确保 spring.sleuth.sampler.probability 设置为足够高的值 (例如 1.0) 以保证所有请求都被采样。

步骤 2: 检查 Context 传递

Sleuth 依赖于 Spring Cloud Context 组件来传递 TraceId 和 SpanId。要确保 Context 传递正常工作,需要检查以下几点:

  • HTTP 请求: 如果使用 RestTemplateWebClient 进行服务调用,Sleuth 会自动将 TraceId 和 SpanId 注入到 HTTP Header 中。检查 HTTP Header 中是否包含了 X-B3-TraceIdX-B3-SpanIdX-B3-ParentSpanId 等 Header。

    例如,可以使用 curl 命令来查看 HTTP Header:

    curl -v http://your-service/your-endpoint

    如果 Header 中没有这些信息,说明 Context 传递失败。可能是 RestTemplateWebClient 没有被正确配置,或者使用了自定义的 HTTP 客户端,没有集成 Sleuth。

    对于 RestTemplate,确保使用了 RestTemplateBuilder 来创建实例,并启用了拦截器:

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.build();
    }

    对于 WebClient, 可以使用 WebClient.Builder

    @Bean
    public WebClient webClient(WebClient.Builder builder) {
        return builder.build();
    }
  • 消息队列: 如果使用消息队列进行服务调用,Sleuth 会自动将 TraceId 和 SpanId 注入到 Message Header 中。检查 Message Header 中是否包含了相应的 Header。

    例如,如果使用 Spring Cloud Stream 和 Kafka,可以检查 KafkaHeaders.TRACE_IDKafkaHeaders.SPAN_ID 是否存在。

    确保消息生产者和消费者都正确配置了 Sleuth,并且使用了 Spring Cloud Stream 提供的 MessageChannelMessageHandler 来发送和接收消息。

  • 自定义组件: 如果使用了自定义的 HTTP 客户端或消息队列消费者,需要手动集成 Sleuth,将 TraceId 和 SpanId 注入到 HTTP Header 或 Message Header 中。

    可以使用 TraceContext 对象来获取当前的 TraceId 和 SpanId:

    @Autowired
    private Tracer tracer;
    
    public void yourCustomMethod() {
        TraceContext traceContext = tracer.currentSpan().context();
        String traceId = traceContext.traceId();
        String spanId = traceContext.spanId();
    
        // 将 TraceId 和 SpanId 注入到 HTTP Header 或 Message Header 中
    }

步骤 3: 处理异步调用

在使用线程池、消息队列等异步方式进行调用时,需要特别注意 Context 的传递。因为异步线程无法自动获取到主线程的 Context。

  • @Async 注解: 如果使用 @Async 注解进行异步调用,可以使用 org.springframework.cloud.sleuth.instrument.async.TraceableExecutorService 来包装线程池,从而将 Context 传递到异步线程。

    @Configuration
    public class AsyncConfig {
    
        @Bean(name = "asyncExecutor")
        public Executor asyncExecutor(Tracer tracer) {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setCorePoolSize(5);
            executor.setMaxPoolSize(10);
            executor.setQueueCapacity(25);
            executor.setThreadNamePrefix("AsyncThread-");
            executor.initialize();
            return new TraceableExecutorService(executor, tracer);
        }
    }
    
    @Async("asyncExecutor")
    public void yourAsyncMethod() {
        // 异步执行的代码
    }
  • CompletableFuture: 如果使用 CompletableFuture 进行异步调用,可以使用 CompletableFuture.supplyAsyncCompletableFuture.runAsync 方法,并传入 TraceContext 对象:

    @Autowired
    private Tracer tracer;
    
    public CompletableFuture<String> yourAsyncMethod() {
        TraceContext traceContext = tracer.currentSpan().context();
        return CompletableFuture.supplyAsync(() -> {
            // 异步执行的代码
            return "result";
        });
    }
  • 手动传递 Context: 如果使用了其他异步框架,需要手动获取当前的 TraceId 和 SpanId,并将它们传递到异步线程中。在异步线程中,可以使用 Tracer.nextSpan() 方法创建一个新的 Span,并将 TraceId 和 SpanId 设置到新的 Span 中。

步骤 4: 检查采样率

确保 spring.sleuth.sampler.probability 设置为足够高的值,以保证所有重要的请求都被采样。如果采样率设置过低,可能会导致关键请求没有被追踪到。

步骤 5: 检查日志配置

确保日志配置正确,链路日志能够被正确地收集和存储。检查日志级别是否设置为 DEBUGTRACE,以及日志输出格式是否包含了 TraceId 和 SpanId。

例如,如果使用 Logback,可以配置如下的日志格式:

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{traceId:-},%X{spanId:-}] %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

步骤 6: 处理异常

确保所有异常都被正确地捕获和处理。如果服务在处理请求时发生了未捕获的异常,Sleuth 可能会无法记录 Span 的结束时间,导致 Span 不完整。

可以使用 try-catch 语句来捕获异常,并在 catch 块中记录异常信息:

try {
    // 可能抛出异常的代码
} catch (Exception e) {
    // 记录异常信息
    logger.error("An error occurred: {}", e.getMessage(), e);
    // 设置 Span 的错误信息
    tracer.currentSpan().error(e);
    throw e; // 重新抛出异常
}

步骤 7: 使用 Sleuth API 手动创建和管理 Span

在某些情况下,Sleuth 无法自动创建 Span,例如在执行一些耗时的后台任务时。这时,可以使用 Sleuth API 手动创建和管理 Span。

@Autowired
private Tracer tracer;

public void yourMethod() {
    try (Span span = tracer.nextSpan().name("your-operation").start()) {
        // 设置 Span 的标签
        span.tag("your-tag", "your-value");

        // 执行你的代码
        yourCode();

    } catch (Exception e) {
        // 记录异常信息
        tracer.currentSpan().error(e);
        throw e;
    } finally {
        // Span 会在 try-with-resources 语句块结束时自动结束
    }
}

步骤 8: 使用工具进行分析

在排查链路日志不完整的问题时,可以使用一些工具来分析链路数据,例如 Zipkin 或 Jaeger。这些工具可以帮助你可视化链路,找到丢失的 Span,并分析问题的根源。

将日志数据导入到这些工具中,然后根据 TraceId 过滤链路,查看是否存在缺失的 Span。

步骤 9: 检查中间件的版本兼容性

确保使用的 Spring Cloud Sleuth 版本与 Spring Boot、Spring Cloud 以及其他中间件的版本兼容。版本不兼容可能会导致 Sleuth 无法正常工作。

可以参考 Spring Cloud 的官方文档,了解各个组件的版本兼容性。

4. 一个完整的排查案例

假设我们有一个微服务架构,包含了三个服务:service-aservice-bservice-cservice-a 调用 service-bservice-b 调用 service-c。在使用 Sleuth 进行链路追踪时,发现 service-c 的 Span 总是丢失。

按照上述排查步骤,我们可以逐步排除问题:

  1. 检查依赖和配置: 确保 service-c 正确引入了 Sleuth 依赖,并且 application.yml 文件中包含了必要的配置,例如 spring.sleuth.sampler.probability=1.0
  2. 检查 Context 传递: 使用 curl 命令查看 service-b 调用 service-c 的 HTTP Header,发现没有 X-B3-TraceIdX-B3-SpanId 等 Header。
  3. 检查 RestTemplate 配置: 检查 service-b 中使用的 RestTemplate 是否正确配置,是否使用了 RestTemplateBuilder 来创建实例,并启用了拦截器。
  4. 发现问题: 发现 service-b 中使用了自定义的 RestTemplate,没有集成 Sleuth。
  5. 解决问题:service-b 中的 RestTemplate 替换为使用 RestTemplateBuilder 创建的实例,并启用拦截器。

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.build();
    }

    重新部署 service-b,再次测试链路追踪,发现 service-c 的 Span 已经可以正常显示了。

5. 预防措施

除了排查问题,我们还可以采取一些预防措施,以避免链路日志不完整的问题:

  • 标准化配置: 制定统一的 Sleuth 配置规范,并在所有服务中应用。
  • 代码审查: 在代码审查过程中,检查是否正确使用了 Sleuth API,是否正确处理了异步调用,以及是否正确传递了 Context。
  • 自动化测试: 编写自动化测试用例,模拟各种服务调用场景,并验证链路追踪是否正常工作。
  • 监控和告警: 监控链路追踪数据的完整性,并设置告警规则,及时发现链路日志不完整的问题。

总结一下:

通过理解链路追踪的原理,分析链路日志不完整的常见原因,并按照步骤进行排查,可以有效地解决 Sleuth 链路日志不完整的问题。同时,采取预防措施可以避免问题的发生。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注