Spring Cloud Sleuth 链路日志不完整导致追踪失败的排查方法
大家好,今天我们来聊聊在使用 Spring Cloud Sleuth 进行分布式链路追踪时,经常遇到的一个问题:链路日志不完整,导致追踪失败。这个问题可能会让你花费大量时间去排查,所以掌握一些排查思路和方法非常重要。
1. 理解链路追踪的基本原理
在深入排查之前,我们需要先理解 Sleuth 的基本工作原理。Sleuth 的核心是利用 TraceId 和 SpanId 来关联一次请求在不同服务之间的调用链。
- 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.properties或application.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.xml 或 build.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.properties 或 application.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 请求: 如果使用
RestTemplate或WebClient进行服务调用,Sleuth 会自动将 TraceId 和 SpanId 注入到 HTTP Header 中。检查 HTTP Header 中是否包含了X-B3-TraceId、X-B3-SpanId、X-B3-ParentSpanId等 Header。例如,可以使用
curl命令来查看 HTTP Header:curl -v http://your-service/your-endpoint如果 Header 中没有这些信息,说明 Context 传递失败。可能是
RestTemplate或WebClient没有被正确配置,或者使用了自定义的 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_ID和KafkaHeaders.SPAN_ID是否存在。确保消息生产者和消费者都正确配置了 Sleuth,并且使用了 Spring Cloud Stream 提供的
MessageChannel或MessageHandler来发送和接收消息。 -
自定义组件: 如果使用了自定义的 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.supplyAsync或CompletableFuture.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: 检查日志配置
确保日志配置正确,链路日志能够被正确地收集和存储。检查日志级别是否设置为 DEBUG 或 TRACE,以及日志输出格式是否包含了 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-a、service-b 和 service-c。service-a 调用 service-b,service-b 调用 service-c。在使用 Sleuth 进行链路追踪时,发现 service-c 的 Span 总是丢失。
按照上述排查步骤,我们可以逐步排除问题:
- 检查依赖和配置: 确保
service-c正确引入了 Sleuth 依赖,并且application.yml文件中包含了必要的配置,例如spring.sleuth.sampler.probability=1.0。 - 检查 Context 传递: 使用
curl命令查看service-b调用service-c的 HTTP Header,发现没有X-B3-TraceId和X-B3-SpanId等 Header。 - 检查 RestTemplate 配置: 检查
service-b中使用的RestTemplate是否正确配置,是否使用了RestTemplateBuilder来创建实例,并启用了拦截器。 - 发现问题: 发现
service-b中使用了自定义的RestTemplate,没有集成 Sleuth。 -
解决问题: 将
service-b中的RestTemplate替换为使用RestTemplateBuilder创建的实例,并启用拦截器。@Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder.build(); }重新部署
service-b,再次测试链路追踪,发现service-c的 Span 已经可以正常显示了。
5. 预防措施
除了排查问题,我们还可以采取一些预防措施,以避免链路日志不完整的问题:
- 标准化配置: 制定统一的 Sleuth 配置规范,并在所有服务中应用。
- 代码审查: 在代码审查过程中,检查是否正确使用了 Sleuth API,是否正确处理了异步调用,以及是否正确传递了 Context。
- 自动化测试: 编写自动化测试用例,模拟各种服务调用场景,并验证链路追踪是否正常工作。
- 监控和告警: 监控链路追踪数据的完整性,并设置告警规则,及时发现链路日志不完整的问题。
总结一下:
通过理解链路追踪的原理,分析链路日志不完整的常见原因,并按照步骤进行排查,可以有效地解决 Sleuth 链路日志不完整的问题。同时,采取预防措施可以避免问题的发生。