1. 分布式链路追踪到底在追什么
微服务架构下,一个前端请求打过来,背后可能要经过网关、认证服务、订单服务、库存服务、支付服务、消息队列、缓存层,最后才落库返回。这条链路上任何一个环节慢了、挂了、超时了,用户端看到的只是一个笼统的“系统繁忙”或者“请求超时”。你打开日志平台,每个服务都在刷自己的日志,时间戳密密麻麻,但没有一个统一的标识能把它们串起来。这就是分布式链路追踪要解决的核心问题:给一次完整的请求调用过程打上唯一标记,让所有参与的服务在日志里都带上这个标记,从而把散落在各处的日志拼成一条完整的调用链。
SpringCloud Sleuth 就是 Spring Cloud 生态里干这件事的组件。它的定位很清晰:不重复造轮子,而是做“埋点标准化”和“上下文透传”。底层它兼容了 Google Dapper 论文提出的 Trace/Span 模型,同时对接了 Zipkin、ELK 等后端存储和展示系统。你不需要在每个服务里手写 traceId 的生成和传递逻辑,Sleuth 通过自动配置和拦截器把这些脏活累活全包了。
这篇文章适合谁看?如果你正在维护一套 Spring Cloud 微服务系统,遇到过“日志查不到完整链路”“性能瓶颈定位靠猜”“服务间调用关系理不清”这类问题,那 Sleuth 就是你该认真啃下来的东西。我会从设计思路、核心概念、实操配置、踩坑排查几个维度,把我在实际项目里积累的经验完整倒出来。不管你是刚接触微服务的新手,还是已经用过 Sleuth 但没深究原理的老手,都能从中找到可以直接抄作业的内容。
2. 核心概念拆解与设计思路
2.1 Trace 与 Span:一次请求的身份证和行程单
Trace 和 Span 是链路追踪的两个基石概念,理解它们之间的关系,后面所有配置和排查都会顺理成章。
Trace代表一次完整的请求调用链路。你可以把它理解成一张“行程单编号”。用户点击下单按钮那一刻,系统生成一个全局唯一的 TraceId,这个 ID 会跟着请求走完全部服务,直到最终响应返回。不管中间经过多少个服务、多少次数据库查询、多少次远程调用,TraceId 始终不变。
Span代表链路中的一个工作单元。一次远程调用是一个 Span,一次数据库查询是一个 Span,甚至一个耗时的本地方法也可以是一个 Span。每个 Span 有自己的 SpanId,同时记录父 Span 的 ParentSpanId,这样就能还原出调用树形结构。Span 还记录开始时间、结束时间、标签(Tags)和事件(Logs),这些数据是后续做性能分析的核心素材。
用一个生活化类比:Trace 是你这次出差的总行程编号,Span 是你行程中的每一段——打车去机场是一段,候机是一段,飞行是一段,到达后打车去酒店又是一段。每段都有开始和结束时间,有各自的属性(比如航班号、出租车牌),而 ParentSpanId 就是“这段行程是在哪段行程里发生的”。
注意:TraceId 和 SpanId 的生成规则在不同版本里略有差异。早期 Sleuth 使用 64 位随机数,后来为了兼容更多后端系统,默认改为 128 位。如果你要对接某些只支持 64 位的旧系统,需要手动调整配置。
2.2 为什么选 Sleuth 而不是自己写拦截器
有人可能会想:不就是生成一个 ID 然后塞到 HTTP Header 里传下去吗,我自己写个 Filter 不就完了?理论上可行,但实际落地时会遇到一堆麻烦事。
第一,上下文透传的覆盖面。你的服务可能用 RestTemplate 调别人,也可能用 Feign,还可能用 WebClient、消息队列、线程池异步任务。每种通信方式透传 TraceId 的方式都不一样。自己写就要为每种场景写适配代码,漏一个就断链。Sleuth 通过自动配置把这些场景全覆盖了,包括 Hystrix 线程隔离、MQ 消息头、gRPC 等。
第二,采样策略。全量采集链路数据在高并发下对存储和网络的压力非常大。Sleuth 内置了采样器,支持按比例采样、自定义采样规则,还能和配置中心联动动态调整。自己实现一套完整的采样逻辑,工作量不小。
第三,与日志框架的集成。Sleuth 会自动把 TraceId 和 SpanId 注入到 MDC(Mapped Diagnostic Context)里,你只需要在 logback 或 log4j2 的 pattern 里加上%X{traceId}和%X{spanId},日志里就自动带上链路信息了。这个集成度是自己写拦截器很难低成本达到的。
第四,生态兼容性。Sleuth 生成的链路数据格式是标准化的,可以直接推给 Zipkin、Jaeger 等后端。如果你自己定义格式,后续想换展示系统就要重写解析逻辑。
所以选 Sleuth 的核心逻辑是:用成熟的标准化方案,换取开发效率和生态兼容性。当然代价是引入了一个额外依赖,并且需要理解它的自动配置逻辑,否则出了问题不好排查。
2.3 Sleuth 与 Zipkin 的分工边界
很多人会把 Sleuth 和 Zipkin 混为一谈,其实它们职责分明。
Sleuth 负责“生产数据”:在服务内部生成 TraceId/SpanId,在服务间传递上下文,把链路信息写入日志和上报队列。它不负责存储和展示。
Zipkin 负责“消费数据”:接收 Sleuth 上报的 Span 数据,存储到后端(内存、MySQL、Elasticsearch 等),并提供 UI 界面做链路查询和依赖分析。
你可以只用 Sleuth 不接 Zipkin,这样链路信息只存在于日志里,排查时靠 grep TraceId 来串联。也可以接 Zipkin,获得可视化的调用链和耗时瀑布图。实际项目中,我建议至少把 Sleuth 的日志集成做好,Zipkin 按需接入。因为日志是最可靠的兜底手段,Zipkin 服务本身也可能出问题。
3. 实操配置与核心环节实现
3.1 依赖引入与版本对齐
Sleuth 的依赖引入本身很简单,但版本对齐是第一个容易踩的坑。Spring Cloud 的版本管理是 BOM 模式,Sleuth 的版本必须和 Spring Cloud 主版本匹配。
以 Maven 为例,在父工程的 dependencyManagement 里引入 Spring Cloud BOM:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2022.0.4</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>然后在需要追踪的服务模块里引入 Sleuth:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency>如果要对接 Zipkin,再加一个:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-zipkin</artifactId> </dependency>提示:从 Spring Cloud 2022.0.0 开始,Sleuth 的替代方案是 Micrometer Tracing。如果你用的是较新版本,官方推荐迁移到 Micrometer Tracing + Brave 或 OTel 桥接。但大量存量项目仍在用 Sleuth,本文以 Sleuth 为主,迁移思路在后续章节会提到。
版本对齐的检查方法:启动服务后看日志里有没有INFO [appname,traceId,spanId,exportable]这种格式的输出。如果没有,大概率是版本冲突或者自动配置没生效。可以用mvn dependency:tree排查是否有多个 sleuth 版本共存。
3.2 日志格式改造:让 TraceId 出现在每一行
Sleuth 默认会把 TraceId 和 SpanId 放进 MDC,但你的日志 pattern 如果不引用它们,日志里就看不到。所以第二步是改 logback-spring.xml。
<configuration> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-},%X{spanId:-}] %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE"/> </root> </configuration>关键点是%X{traceId:-}和%X{spanId:-}。:-表示如果 MDC 里没有这个 key,就显示空字符串,避免日志里出现null。
改完之后,同一次请求经过的所有服务,日志里都会带上相同的 TraceId。排查问题时,你只需要在日志平台搜索这个 TraceId,就能看到完整的调用过程。这个体验提升是巨大的——以前要在几个服务的日志文件之间来回切换对时间戳,现在一条搜索搞定。
注意:异步线程和线程池里的日志默认不会带上 TraceId,因为 MDC 是 ThreadLocal 的。Sleuth 提供了
LazyTraceExecutor和TraceableExecutorService来包装线程池,但需要你手动配置。这个坑后面会详细讲。
3.3 采样率配置:平衡数据量和排查需求
默认情况下,Sleuth 的采样率是 0.1,也就是 10% 的请求会被采集上报。这个默认值是为了避免高并发下把 Zipkin 打爆。但在实际排查问题时,10% 的采样率意味着你复现一个问题后,有 90% 的概率链路数据没被采集到,非常尴尬。
我的建议是分环境配置:
spring: sleuth: sampler: probability: 1.0开发测试环境设为 1.0,全量采集,方便调试。生产环境根据流量和存储成本调整,一般 0.01 到 0.1 之间。如果生产环境出了疑难问题需要全量追踪,可以通过配置中心动态调整采样率,临时开到 1.0,问题定位后再调回去。
还有一种更精细的做法是自定义采样器,比如只采集耗时超过阈值的请求,或者只采集特定接口的请求。实现Sampler接口即可:
@Bean public Sampler customSampler() { return new Sampler() { @Override public boolean isSampled(Span span) { // 自定义采样逻辑 return true; } }; }不过自定义采样器要小心,如果逻辑太复杂会拖慢主流程。采样判断是在请求入口处执行的,性能敏感。
3.4 服务间透传:HTTP、MQ、线程池三种场景
Sleuth 对 HTTP 调用的透传是自动的。无论是 RestTemplate、Feign 还是 WebClient,只要是通过 Spring Cloud 管理的客户端,TraceId 都会自动塞进请求头。请求头名称默认是X-B3-TraceId、X-B3-SpanId、X-B3-ParentSpanId、X-B3-Sampled,这是 B3 传播格式。
消息队列场景需要额外注意。以 RocketMQ 为例,Sleuth 不会自动把链路信息塞进消息体或消息属性里。你需要在发送消息时手动注入,消费时手动提取。常见做法是:
// 发送端 Message message = new Message(); message.putUserProperty("traceId", tracer.currentSpan().context().traceId()); message.putUserProperty("spanId", tracer.currentSpan().context().spanId()); // 消费端 String traceId = message.getUserProperty("traceId"); Span span = tracer.nextSpan().name("consume").start(); try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) { // 业务逻辑 } finally { span.end(); }线程池场景是另一个高频断链点。如果你在业务代码里用了@Async或者手动提交任务到线程池,子线程里 MDC 是空的,TraceId 就丢了。解决方案是用 Sleuth 提供的包装类:
@Bean public Executor traceableExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(20); executor.setQueueCapacity(100); executor.initialize(); return new LazyTraceExecutor(beanFactory, executor); }这样提交到该线程池的任务会自动继承父线程的链路上下文。实测下来,这个包装对性能影响很小,但能避免大量断链问题。
4. 常见问题与排查技巧实录
4.1 链路断了的五种典型情况和排查路径
链路断裂是使用 Sleuth 时最高频的问题。表现是:A 服务日志里有 TraceId,B 服务日志里也有 TraceId,但两者不一样,说明上下文没传过去。我整理了一个排查表:
| 断链场景 | 典型表现 | 排查方法 | 解决方案 |
|---|---|---|---|
| HTTP 客户端未托管 | RestTemplate 手动 new | 检查客户端是否由 Spring 管理 | 用 @Bean 注入 RestTemplate |
| 异步线程 | 子线程日志无 TraceId | 检查是否用了 @Async 或线程池 | 用 LazyTraceExecutor 包装 |
| MQ 消息 | 消费端 TraceId 与生产端不同 | 检查消息属性里是否有 traceId | 手动注入和提取 |
| 网关过滤 | 网关后第一个服务 TraceId 为空 | 检查网关是否透传了 B3 头 | 配置网关的转发规则 |
| 跨版本调用 | 新老服务 TraceId 格式不一致 | 对比两边的 Sleuth 版本 | 统一版本或做格式兼容 |
排查的基本手法是:在 A 服务发请求的地方打日志,打印出当前 TraceId 和请求头;在 B 服务收请求的地方打日志,打印出收到的请求头和生成的 TraceId。对比两边就能定位是发送端没带、传输中丢了、还是接收端没解析。
4.2 Zipkin 服务端连不上或数据不显示
Zipkin 数据不显示通常有三个原因。
第一,上报地址配错了。Sleuth 默认上报到http://localhost:9411/api/v2/spans。如果你的 Zipkin 部署在别的机器上,要显式配置:
spring: zipkin: base-url: http://zipkin-host:9411 sender: type: web第二,采样率太低。前面说过,默认 0.1 的采样率意味着大部分请求不会上报。调试时先开到 1.0 确认链路通了,再调回去。
第三,Zipkin 存储后端没配好。Zipkin 默认用内存存储,重启就丢数据。生产环境要配 Elasticsearch 或 MySQL。如果存储后端连不上,Zipkin 服务本身可能启动正常,但数据写不进去,UI 上就看不到。
实操心得:我习惯在开发环境把 Zipkin 的日志级别调到 DEBUG,这样能看到它有没有收到上报数据。如果 Zipkin 日志里没有收到 span 的记录,问题就在生产端;如果收到了但 UI 不显示,问题就在存储或查询端。
4.3 性能开销评估与优化建议
引入 Sleuth 会带来一定的性能开销,主要来自三个方面:TraceId 生成、上下文传递、数据上报。根据我的实测,在常规业务系统里,Sleuth 带来的额外延迟在 1% 到 3% 之间,大部分场景可以接受。
如果对性能极其敏感,可以做以下优化:
- 降低采样率,减少上报数据量
- 使用异步上报方式,避免阻塞主流程
- 关闭不必要的集成,比如不需要 MQ 追踪就排除相关自动配置
- 升级到 Micrometer Tracing,它的性能比 Sleuth 有优化
spring: sleuth: async: enabled: true开启异步上报后,Span 数据会先进入队列,由后台线程批量发送,对主流程的影响更小。但要注意队列满了会丢数据,需要监控队列深度。
4.4 与日志平台的集成技巧
Sleuth 的 TraceId 只有和日志平台结合才能发挥最大价值。以常见的 ELK 方案为例,你需要确保 Logstash 或 Filebeat 采集日志时,把 TraceId 解析成独立字段,这样在 Kibana 里才能按 TraceId 聚合查询。
Logstash 的 grok 配置示例:
filter { grok { match => { "message" => "\[%{DATA:traceId},%{DATA:spanId}\]" } } }解析出 traceId 字段后,在 Kibana 里搜索traceId: "abc123"就能看到这条链路上所有服务的日志,按时间排序就是完整的调用顺序。这个体验比在 Zipkin 里看瀑布图更接地气,因为日志里有具体的业务参数和异常堆栈。
注意:如果日志量很大,给 traceId 建索引会占用额外存储。建议只保留最近 7 到 15 天的 traceId 索引,历史日志归档时去掉这个字段。
5. 从 Sleuth 到 Micrometer Tracing 的迁移思路
Spring Cloud 2022.0.0 之后,Sleuth 不再更新,官方推荐迁移到 Micrometer Tracing。这不是简单的换个依赖,核心概念和 API 都有变化。
Micrometer Tracing 是一个门面(Facade),底层可以桥接 Brave 或 OpenTelemetry。如果你原来用 Sleuth + Zipkin,迁移时可以选择 Micrometer Tracing + Brave + Zipkin,这样后端存储不用换。
迁移的主要工作包括:
- 依赖替换:
spring-cloud-starter-sleuth换成micrometer-tracing-bridge-brave - 配置项调整:
spring.sleuth.*换成management.tracing.* - 日志 pattern 调整:MDC key 从
traceId/spanId变成traceId/spanId(大部分兼容,但格式可能不同) - 手动埋点 API 替换:
Tracer接口的方法签名有变化
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing-bridge-brave</artifactId> </dependency> <dependency> <groupId>io.zipkin.reporter2</groupId> <artifactId>zipkin-reporter-brave</artifactId> </dependency>迁移过程中最容易出问题的是手动埋点的代码。如果你原来在业务代码里直接用了 Sleuth 的Tracer做自定义 Span,迁移时需要重写这部分逻辑。建议在迁移前先梳理清楚项目里有哪些地方做了手动埋点,评估工作量。
我的建议是:新项目直接用 Micrometer Tracing,老项目如果不急着升级 Spring Cloud 版本,Sleuth 继续用也没问题,它足够稳定。等有明确的升级需求时再迁移,不要为了追新而引入不必要的风险。
6. 几个容易被忽略的细节
第一个细节是网关层的处理。Spring Cloud Gateway 作为入口,需要确保它把 B3 请求头透传到下游。默认情况下 Gateway 会透传所有请求头,但如果你在网关里做了请求头过滤或重写,可能会把 B3 头丢掉。检查方法是看网关日志里的 TraceId 和下游第一个服务的 TraceId 是否一致。
第二个细节是Feign 的 Hystrix 隔离策略。如果 Hystrix 用的是线程隔离模式,Feign 调用会在独立线程里执行,MDC 不会自动传递。需要配置 Hystrix 的并发策略为信号量模式,或者用 Sleuth 提供的 Hystrix 集成。不过 Hystrix 已经停止维护,新项目建议用 Resilience4j,它对 Sleuth 的兼容性更好。
第三个细节是数据库连接池的监控。Sleuth 默认不会为 JDBC 查询创建 Span。如果你需要追踪慢 SQL,要额外引入spring-cloud-sleuth-datasource或者用 P6Spy 配合。不过 JDBC 层面的 Span 数量会非常多,采样率要调低,否则 Zipkin 会被 SQL Span 淹没。
第四个细节是TraceId 的长度。128 位的 TraceId 在日志里占 32 个字符,如果日志量巨大,这个开销不可忽视。可以在配置里改成 64 位:
spring: sleuth: trace-id128: false但要注意,改成 64 位后碰撞概率会上升,超大规模系统慎用。
这些细节在官方文档里往往一笔带过,但实际项目中每一个都可能让你排查半天。我的经验是,接入 Sleuth 的前两周,每天花十分钟检查一下链路完整性,把断链问题尽早暴露出来,后面就省心了。