Appearance
Sleuth 详解:链路追踪
Spring Cloud Sleuth 是 Spring Cloud 生态中的分布式链路追踪组件。它自动为每个请求生成全局唯一的 TraceId 和 SpanId,在服务间调用时自动透传,最终将链路数据上报到 Zipkin 等可视化平台,让你在几十个微服务中快速定位性能瓶颈和故障点。
重要提示: Spring Boot 3.x / Spring Cloud 2022.x 起,Sleuth 已被 Micrometer Tracing 取代。本章以 Sleuth 为主讲解核心概念和原理(两者概念一致),最后一节专讲迁移。如果你用的是 Spring Boot 3.x,详见 Micrometer Tracing 详解。
一、为什么需要链路追踪?
1.1 没有链路追踪的困境
一个请求经过 5 个微服务:
User → Gateway → order-service → inventory-service → DB
→ payment-service → 第三方支付
→ sms-service → 短信网关
用户反馈:"下单很慢,等了 5 秒"
没有链路追踪时:
├── 查 order-service 日志:RT=50ms,正常
├── 查 inventory-service 日志:RT=30ms,正常
├── 查 payment-service 日志:RT=40ms,正常
├── 查 sms-service 日志:RT=20ms,正常
└── 50+30+40+20=140ms,加起来不到 5 秒啊?
问题:你不知道哪个环节在"等待",哪个环节在"并行"
你也不知道这个请求到底经过了哪些服务、每段耗时多少
有链路追踪时:
Gateway ──2ms──→ order-service ──30ms──→ inventory-service
├──4.5s──→ payment-service ← 找到了!
└──20ms──→ sms-service
一眼看出:payment-service 花了 4.5 秒,其他都正常
问题定位:从"不知道从哪查"变成"一眼看到"1.2 链路追踪解决什么问题
| 问题 | 传统方式 | 用链路追踪 |
|---|---|---|
| 请求经过哪些服务? | 猜,或者逐个服务查日志 | 链路图一目了然 |
| 哪个服务慢? | 各服务日志里的 RT 拼起来 | 直接看到每个 Span 的耗时 |
| 调用顺序? | 看代码,或者猜 | 父子 Span 关系清晰展示 |
| 某个 TraceId 的完整日志? | 逐个服务 grep,可能漏 | 一个 TraceId 查出所有日志 |
| 错误发生在哪个环节? | 逐个服务查错误日志 | 链路中红色高亮故障节点 |
二、核心概念
2.1 Trace 和 Span
一次完整的用户请求链路(Trace):
TraceId: a1b2c3d4e5f6 ← 贯穿整个链路的全局唯一 ID
┌──────────────────────────────────────────────────────────────────┐
│ Span1: Gateway (入口) │
│ SpanId: aaa, ParentSpanId: null │
│ Kind: SERVER, Duration: 2ms │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Span2: order-service (中间) │ │
│ │ SpanId: bbb, ParentSpanId: aaa │ │
│ │ Kind: SERVER, Duration: 80ms │ │
│ │ │ │
│ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │
│ │ │ Span3: inventory-svc │ │ Span4: payment-svc │ │ │
│ │ │ SpanId: ccc │ │ SpanId: ddd │ │ │
│ │ │ ParentSpanId: bbb │ │ ParentSpanId: bbb │ │ │
│ │ │ Kind: CLIENT, 30ms │ │ Kind: CLIENT, 20ms │ │ │
│ │ └──────────────────────┘ └──────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘| 概念 | 类比 | 说明 |
|---|---|---|
| TraceId | 一次旅行的"行程单号" | 64 位 / 128 位,贯穿整个请求链路 |
| SpanId | 行程中的每一段"交通工具" | 每个操作单元的唯一标识,64 位 |
| ParentSpanId | 上一段"交通工具"的编号 | 构建 Span 之间的父子调用关系 |
| Trace | 整张"行程单" | 由一组 Span 组成的树状结构 |
2.2 Span 的四种类型(Annotation)
┌──────────────────────────────────────────────────────────────────┐
│ Span 四种 Annotation │
│ │
│ 服务 A (调用方) 服务 B (被调用方) │
│ ────────────── ────────────── │
│ │
│ CS (Client Sent) SR (Server Received) │
│ 发送请求 ──────────────────────────→ 收到请求 │
│ │ │ │
│ │ 网络传输时间 = SR - CS │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ CR (Client Received) SS (Server Sent) │
│ 收到响应 ←────────────────────────── 返回响应 │
│ │
│ 客户端耗时 = CR - CS │
│ 服务端耗时 = SS - SR │
│ 网络耗时 = (CR - CS) - (SS - SR) │
└──────────────────────────────────────────────────────────────────┘这四个时间点让 Zipkin 能画出精确的调用时序图,定位到底是服务慢还是网络慢。
2.3 Span 的 Kind 类型
| Kind | 含义 | 谁产生 |
|---|---|---|
CLIENT | 客户端发起调用 | 调用方(如 Feign 发送请求时) |
SERVER | 服务端接收调用 | 被调用方(如 Controller 收到请求时) |
PRODUCER | 消息生产者 | 发送 MQ 消息 |
CONSUMER | 消息消费者 | 接收 MQ 消息 |
三、工作原理
3.1 TraceId 生成与透传
┌──────────────────────────────────────────────────────────────────┐
│ TraceId 透传全流程 │
│ │
│ 服务 A (order-service) 服务 B (inventory-service) │
│ ──────────────── ──────────────── │
│ │
│ 1. 收到 HTTP 请求 │
│ 请求头中有 X-B3-TraceId 吗? │
│ ├── 有 → 使用请求头中的 TraceId(透传) │
│ └── 没有 → 生成新的 TraceId(入口服务) │
│ │
│ 2. Sleuth 生成当前 SpanId │
│ TraceId: abc123(来自请求头) │
│ SpanId: bbb(新生成) │
│ ParentSpanId: aaa(来自请求头的 X-B3-SpanId) │
│ │
│ 3. 调用 inventory-service │
│ Feign/RestTemplate 自动拦截 │
│ 在请求头中注入: │
│ X-B3-TraceId: abc123 │
│ X-B3-SpanId: ccc ← 新 SpanId │
│ X-B3-ParentSpanId: bbb ← 当前 SpanId 作为父 SpanId │
│ X-B3-Sampled: 1 ← 需要采样 │
│ ───────────────────────────────→ │
│ 4. 收到请求,解析请求头 │
│ TraceId: abc123 │
│ SpanId: ddd (新生成) │
│ ParentSpanId: ccc │
│ │
│ 5. 处理业务逻辑 │
│ │
│ 6. 收到响应,Sleuth 记录 Span 数据 │
│ ←─────────────────────────────── │
│ │
│ 7. 异步上报 Span 到 Zipkin │
└──────────────────────────────────────────────────────────────────┘关键理解:
- 入口服务(Gateway 或第一个收到请求的服务):请求头中没有 TraceId,Sleuth 生成新的
- 中间服务:请求头中有 TraceId,直接使用,保证全链路一个 TraceId
- 透传载体:
X-B3-*系列请求头(B3 是 Zipkin 的传播格式名) - 自动透传:Feign、RestTemplate、WebClient、Gateway 都自动支持,无需手动编码
3.2 内部架构
┌──────────────────────────────────────────────────────────────────┐
│ Sleuth 内部架构 │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Sleuth 核心 │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Tracer │ │ SpanHandler │ │ Reporter │ │ │
│ │ │ (创建Span) │ │ (处理Span) │ │ (上报数据) │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ Brave (Zipkin 客户端库) │ │ │
│ │ │ - 生成 TraceId/SpanId │ │ │
│ │ │ - 管理 Span 生命周期 │ │ │
│ │ │ - 采样决策 │ │ │
│ │ │ - 上报到 Zipkin Server │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ 自动埋点层 │ │
│ │ │ │
│ │ HTTP 通信 消息队列 数据库 定时任务 │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │Feign │ │RabbitMQ│ │JDBC │ │@Scheduled│ │ │
│ │ │RestTpl │ │Kafka │ │Redis │ │@Async │ │ │
│ │ │WebClient│ │Stream │ │MongoDB │ │ │ │ │
│ │ │Gateway │ │ │ │ │ │ │ │ │
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘Sleuth 底层用的是 Brave(Zipkin 的 Java 客户端库),Sleuth 本身是对 Brave 的 Spring 封装和自动配置。
四、快速上手
4.1 依赖
xml
<!-- Spring Boot 2.x -->
<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>4.2 配置
yaml
spring:
application:
name: order-service # 服务名会作为 Span 的 serviceName
sleuth:
sampler:
probability: 1.0 # 采样率:1.0 = 100%,生产建议 0.1~0.3
web:
enabled: true # 自动为 HTTP 请求生成 Span
zipkin:
base-url: http://localhost:9411 # Zipkin Server 地址
sender:
type: web # 通过 HTTP 上报(也可以用 rabbit / kafka)4.3 一条日志看效果
配置 logback 打印 TraceId:
xml
<!-- logback-spring.xml -->
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId:-},%X{spanId:-}]
%-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>日志输出效果:
2024-01-15 10:30:45.123 [http-nio-8081-1] [abc123,aaa] INFO Gateway - 收到请求
2024-01-15 10:30:45.125 [http-nio-8082-1] [abc123,bbb] INFO OrderService - 开始下单
2024-01-15 10:30:45.155 [http-nio-8083-1] [abc123,ccc] INFO InventorySvc - 扣减库存
2024-01-15 10:30:45.165 [http-nio-8084-1] [abc123,ddd] INFO PaymentSvc - 创建支付单
2024-01-15 10:30:45.185 [http-nio-8082-1] [abc123,bbb] INFO OrderService - 下单完成排查问题: 在 ELK 中搜索 TraceId: abc123,这个请求跨 4 个服务的所有日志全部出来,按时间排序就是完整的故事线。
五、采样策略
5.1 采样率配置
yaml
spring:
sleuth:
sampler:
probability: 0.3 # 30% 采样率,生产环境推荐5.2 自定义采样规则
java
@Configuration
public class SamplerConfig {
@Bean
public SamplerFunction<HttpRequest> customSampler() {
return request -> {
// 错误请求 100% 采样
String path = request.path();
if (path.contains("/error")) {
return true;
}
// 健康检查不采样
if (path.contains("/actuator")) {
return false;
}
// 其他按概率
return Math.random() < 0.3;
};
}
}5.3 采样策略选择
| 策略 | 配置 | 适用场景 |
|---|---|---|
| 全量采样 | probability: 1.0 | 开发/测试环境 |
| 概率采样 | probability: 0.3 | 生产环境,QPS 高 |
| 限速采样 | rate: 100 | 每秒最多 100 条 Trace |
| 自适应采样 | 自定义 | 错误全采,正常按比例 |
六、自定义 Span 与 Tag
6.1 手动创建 Span
java
@Service
public class OrderService {
@Autowired
private Tracer tracer;
public Order createOrder(OrderDTO dto) {
// 创建自定义 Span(包裹一段业务逻辑)
Span span = tracer.nextSpan()
.name("createOrder") // Span 名称
.tag("orderId", dto.getId()) // 自定义标签
.tag("amount", String.valueOf(dto.getAmount()))
.start();
try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
// 业务逻辑... 这段代码的耗时就是 Span 的 Duration
return doCreateOrder(dto);
} catch (Exception e) {
span.error(e); // 标记异常
throw e;
} finally {
span.end(); // 结束 Span
}
}
}6.2 注解方式
java
@RestController
public class OrderController {
@GetMapping("/order/{id}")
@NewSpan("getOrder") // 创建新 Span
@SpanTag(key = "orderId", value = "arg[0]") // 将参数作为标签
public Order getOrder(@PathVariable Long id) {
return orderService.getById(id);
}
}6.3 给已有 Span 加标签
java
@Autowired
private Tracer tracer;
public void doSomething(String userId) {
// 给当前 Span 添加标签
tracer.currentSpan().tag("userId", userId);
tracer.currentSpan().tag("bizType", "order");
tracer.currentSpan().event("开始处理订单"); // 添加事件(时间点)
}七、与各种组件集成
7.1 Feign(自动集成,零配置)
java
// 不需要任何配置,Feign 自动透传 TraceId
@FeignClient(name = "inventory-service")
public interface InventoryClient {
@GetMapping("/stock/{productId}")
Inventory getStock(@PathVariable Long productId);
}原理: Feign 的 RequestInterceptor 自动从当前 Span 读取 TraceId/SpanId,写入请求头 X-B3-*。被调用方 Sleuth 自动解析请求头,恢复链路上下文。
7.2 RestTemplate(自动集成,零配置)
java
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate(); // Sleuth 自动注入拦截器
}7.3 WebClient(自动集成,零配置)
java
@Bean
public WebClient webClient() {
return WebClient.builder().build(); // Sleuth 自动注入 ExchangeFilterFunction
}7.4 Gateway(自动集成,零配置)
yaml
spring:
cloud:
gateway:
routes:
- id: order-route
uri: lb://order-service
predicates:
- Path=/order/**
# Sleuth 自动在 Gateway 层创建 Span,并透传 TraceId7.5 RabbitMQ / Kafka
java
// 生产者:Sleuth 自动在消息头中注入 TraceId
@Service
public class OrderProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendOrder(Order order) {
// Sleuth 自动在消息头加入 X-B3-TraceId 等字段
rabbitTemplate.convertAndSend("order.exchange", "order.created", order);
}
}
// 消费者:Sleuth 自动从消息头提取 TraceId,建立新的 Span
@Component
public class OrderConsumer {
@RabbitListener(queues = "order.created.queue")
public void handleOrder(Order order) {
// 当前 Span 的 ParentSpanId 指向生产者的 Span
log.info("收到订单消息: {}", order.getId());
}
}7.6 异步线程
java
@Service
public class OrderService {
@Autowired
private Tracer tracer;
@Async
public void asyncProcess() {
// ❌ 异步线程默认丢失 TraceId
// 日志输出:[ , ] INFO ... (TraceId 为空)
}
@Async
public void asyncProcessFixed() {
// ✅ 手动恢复 TraceId
Span span = tracer.nextSpan().start();
try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
// 日志输出:[abc123,xxx] INFO ... (TraceId 正常)
doWork();
} finally {
span.end();
}
}
}
// 更简单的方式:使用 LazyTraceExecutor
@Configuration
public class AsyncConfig {
@Bean
public Executor asyncExecutor(Tracer tracer) {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
// 包装 ThreadPoolExecutor,自动传递 TraceId
return new LazyTraceExecutor(executor.getThreadPoolExecutor(), tracer);
}
}八、与 Zipkin 集成
8.1 Zipkin Server 部署
bash
# Docker 启动 Zipkin
docker run -d -p 9411:9411 openzipkin/zipkin8.2 上报方式
| 方式 | 配置 | 适用场景 |
|---|---|---|
| HTTP | sender.type: web | 开发环境,简单 |
| RabbitMQ | sender.type: rabbit | 生产环境,异步、不丢数据 |
| Kafka | sender.type: kafka | 大数据量,高吞吐 |
yaml
# RabbitMQ 方式上报(推荐生产使用)
spring:
zipkin:
sender:
type: rabbit
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest8.3 Zipkin 控制台
访问 http://localhost:9411,可以:
- 按服务名搜索:查询某个服务的所有链路
- 按 TraceId 搜索:查询某个请求的完整链路
- 查看调用链拓扑:服务之间的依赖关系图
- 查看单条链路详情:每个 Span 的耗时、标签、异常信息
- 查看耗时分布:P50/P95/P99 延迟
Zipkin 链路详情示例:
Gateway (2ms)
└── order-service (80ms)
├── inventory-service (30ms) ← 正常,绿色
└── payment-service (4.5s) ← 慢,黄色高亮
点击 payment-service 可以看到:
- 请求 URL: POST /payment/create
- 状态码: 200
- 耗时: 4.5s
- 标签: orderId=12345, amount=100.00九、Sleuth vs Micrometer Tracing
9.1 版本对应关系
| Spring Boot | Spring Cloud | 链路追踪组件 |
|---|---|---|
| 2.x | 2021.x | Sleuth |
| 3.x | 2022.x+ | Micrometer Tracing |
9.2 核心区别
| 维度 | Sleuth | Micrometer Tracing |
|---|---|---|
| 底层实现 | Brave(固定) | Brave 或 OpenTelemetry(可选) |
| API 风格 | Sleuth 特有 API | Micrometer Observation API(统一) |
| 配置前缀 | spring.sleuth.* | management.tracing.* |
| 依赖 | spring-cloud-starter-sleuth | micrometer-tracing-bridge-brave |
| 发展方向 | 停止更新 | 持续演进 |
9.3 从 Sleuth 迁移到 Micrometer Tracing
xml
<!-- 旧:Sleuth -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
<!-- 新:Micrometer Tracing -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-reporter-wavefront</artifactId> <!-- 或 zipkin -->
</dependency>yaml
# 旧配置
spring:
sleuth:
sampler:
probability: 1.0
zipkin:
base-url: http://localhost:9411
# 新配置
management:
tracing:
sampling:
probability: 1.0
zipkin:
tracing:
endpoint: http://localhost:9411/api/v2/spans代码迁移:
java
// 旧 Sleuth API
@Autowired
private Tracer tracer;
Span span = tracer.nextSpan().start();
// 新 Micrometer Tracing API
@Autowired
private ObservationRegistry registry;
Observation observation = Observation.createNotStarted("mySpan", registry);
observation.start();核心概念(TraceId、SpanId、透传机制)完全没变,变的是 API 和配置名。 详细迁移指南见 Micrometer Tracing 详解。
十、生产最佳实践
- 日志中必须包含 TraceId:
%X{traceId:-}是排查分布式问题的唯一手段 - 采样率合理配置:生产环境 10%-30%,错误请求必须 100% 采样
- 异步线程必须传递 TraceId:使用
LazyTraceExecutor或手动withSpanInScope - 上报方式选 MQ 而非 HTTP:避免 Span 上报失败影响业务线程
- 自定义 Span 要有意义:不要每个方法都创建 Span,只在关键业务节点创建
- Span 标签要简洁:不要把整个请求体当标签,标签值不宜超过 256 字符
- Zipkin 数据保留策略:通常保留 7 天,不要无限存储
十一、面试要点
Q1:TraceId 和 SpanId 的区别?
TraceId 是全局链路 ID,一次完整的请求链路中所有服务共享同一个 TraceId。SpanId 是链路中每个操作单元的 ID,每个服务调用产生一个新的 SpanId。通过 ParentSpanId 把 Span 串联成树。
Q2:TraceId 如何跨服务透传?
通过 HTTP 请求头 X-B3-TraceId、X-B3-SpanId、X-B3-ParentSpanId 透传。Feign 的 RequestInterceptor 自动从当前 Span 读取并写入请求头,被调用方 Sleuth 自动解析请求头恢复上下文。入口服务如果没有请求头,就生成新的 TraceId。
Q3:采样策略有哪些?
全量采样(probability: 1.0)、概率采样(probability: 0.3)、限速采样(每秒最多 N 条)、自适应采样(错误全采 + 正常按比例)。生产环境推荐概率采样 10%-30%,错误请求 100% 采样。
Q4:异步线程如何保证 TraceId 不丢失?
使用 LazyTraceExecutor 包装线程池,或手动 tracer.withSpanInScope(span) 包裹异步代码。核心原理是将 TraceId 从当前线程的 MDC 复制到异步线程的 MDC。
Q5:Sleuth 和 Micrometer Tracing 有什么关系?
Micrometer Tracing 是 Sleuth 的继任者,Spring Boot 3.x 起取代 Sleuth。核心概念(TraceId、SpanId、透传)不变,底层从固定 Brave 变为可选 Brave/OpenTelemetry,API 从 Sleuth 专属变为 Micrometer Observation 统一 API。
Q6:Zipkin、SkyWalking、Jaeger 怎么选?
| 工具 | 特点 | 适用场景 |
|---|---|---|
| Zipkin | 轻量、Spring 生态原生集成 | 中小团队,单一追踪需求 |
| SkyWalking | 国产、APM 全功能(指标+追踪+日志) | 国内团队,需要全功能 APM |
| Jaeger | CNCF 项目、K8s 原生 | 云原生、K8s 环境 |
Q7:链路追踪对性能有影响吗?
有,但通常可忽略。影响主要来自:生成 TraceId/SpanId(纳秒级)、记录 Span 时间戳(纳秒级)、异步上报(后台线程,不影响业务)。采样率 100% 时 QPS 下降约 1%-3%,生产环境 30% 采样影响更小。真正影响大的是 Span 上报的网络 IO,所以推荐用 MQ 异步上报。
