1.什么是链路追踪?
简单说:给每个请求分配一个唯一ID(TraceId),这个ID会贯穿整个调用链,方便排查问题。
场景:用户反馈"注册失败",你怎么查?
没有链路追踪:一个个服务翻日志,累死
有链路追踪:拿到 TraceId,一搜全出来
2.核心概念
用户请求
↓
┌─────────────────────────────────────────────────────────┐
│ TraceId: abc123 (整个调用链唯一) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ payslip-api │───▶│ payroll │───▶│ policy │ │
│ │ SpanId: 001 │ │ SpanId: 002 │ │ SpanId: 003│ │
│ │ 耗时: 100ms │ │ 耗时: 800ms │ │ 耗时: 500ms│ │
│ └──────────────┘ └──────────────┘ └────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
TraceId: 唯一标识一次完整请求(从用户发起到返回)
SpanId: 标识调用链中的每一跳(每个服务一个)
Step 1:添加依赖(pom.xml)
java
<!-- Spring Cloud Sleuth 链路追踪 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!-- 可选:Zipkin 可视化(把追踪数据发送到Zipkin服务器) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
Step 2:配置(application.yml)
java
spring:
application:
name: payslip-api # 服务名,会显示在日志中
sleuth:
sampler:
probability: 1.0 # 采样率:1.0=100%全采集,生产环境可以设0.1
# 可选:Zipkin配置
zipkin:
base-url: http://localhost:9411 # Zipkin服务器地址
enabled: true
# 日志格式配置
logging:
pattern:
level: "%5p [${spring.application.name},%X{traceId},%X{spanId}]"
Step 3:日志格式说明
java
# 日志格式
[服务名, TraceId, SpanId] 日志内容
# 实际示例
2025-12-18 10:30:15 INFO [payslip-api,abc123def456,001] 接收到注册请求
2025-12-18 10:30:15 INFO [payslip-api,abc123def456,001] 调用Payroll服务
2025-12-18 10:30:16 INFO [payroll,abc123def456,002] 创建工资单
2025-12-18 10:30:16 INFO [policy,abc123def456,003] 查询税务政策
Step 4:代码中使用
4.1 自动传递(不需要写代码)
Sleuth 会自动处理:
- HTTP请求(RestTemplate、Feign)自动传递 TraceId
- 日志自动带上 TraceId
- 异步任务(@Async)自动传递
4.2 手动获取 TraceId
java
package com.payslip.utils;
import brave.Tracer;
import brave.propagation.TraceContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 链路追踪工具类
*/
@Component
public class TraceUtil {
@Autowired
private Tracer tracer;
/**
* 获取当前 TraceId
*/
public String getTraceId() {
if (tracer.currentSpan() != null) {
return tracer.currentSpan().context().traceIdString();
}
return null;
}
/**
* 获取当前 SpanId
*/
public String getSpanId() {
if (tracer.currentSpan() != null) {
return tracer.currentSpan().context().spanIdString();
}
return null;
}
}
4.3 在日志中使用
java
@RestController
@RequestMapping("/api/user")
@Slf4j
public class UserController {
@Autowired
private TraceUtil traceUtil;
@PostMapping("/register")
public Result register(@RequestBody RegisterForm form) {
// 日志自动带 TraceId(不需要手动加)
log.info("接收到注册请求: mobile={}", form.getMobile());
try {
// 业务逻辑...
userService.register(form);
log.info("注册成功");
return Result.success();
} catch (Exception e) {
// 异常时记录 TraceId,方便排查
log.error("注册失败, TraceId={}, error={}",
traceUtil.getTraceId(), e.getMessage());
return Result.fail("注册失败");
}
}
}
4.4 返回 TraceId 给前端(方便用户反馈)
java
@RestController
@RequestMapping("/api/user")
@Slf4j
public class UserController {
@Autowired
private TraceUtil traceUtil;
@PostMapping("/register")
public Map<String, Object> register(@RequestBody RegisterForm form) {
Map<String, Object> result = new HashMap<>();
// 把 TraceId 返回给前端
result.put("traceId", traceUtil.getTraceId());
try {
userService.register(form);
result.put("code", 0);
result.put("message", "注册成功");
} catch (Exception e) {
result.put("code", -1);
result.put("message", "注册失败: " + e.getMessage());
}
return result;
}
}
// 返回示例:
// {
// "code": -1,
// "message": "注册失败: 手机号已存在",
// "traceId": "abc123def456" ← 用户反馈时带上这个
// }
Step 5:Feign 调用自动传递
Sleuth 会自动在Feign请求中添加追踪 Header:
java
// 你的代码(不需要改动)
@FeignClient(value = "payrollClient", url = "${api.payroll.url}")
public interface PayrollClient {
@PostMapping("/payroll/create")
Result createPayroll(@RequestBody PayrollForm form);
}
// Sleuth 自动添加的 Header(你看不到,但会自动加)
// X-B3-TraceId: abc123def456
// X-B3-SpanId: 002
// X-B3-ParentSpanId: 001
Step 6:追踪完整调用链(实战)
6.1 发起请求
java
curl -X POST http://localhost:8080/api/user/register \
-H "Content-Type: application/json" \
-d '{"mobile":"13800138000","password":"123456","code":"1234"}'
6.2 查看日志
java
# 实时查看日志
tail -f logs/payslip-api.log
# 日志输出示例:
2025-12-18 10:30:15.123 INFO [payslip-api,a]bc123def456,001] 接收到注册请求: mobile=138****8000
2025-12-18 10:30:15.234 INFO [payslip-api,abc123def456,001] 验证手机号格式
2025-12-18 10:30:15.345 INFO [payslip-api,abc123def456,001] 调用Payroll服务创建用户
2025-12-18 10:30:15.456 INFO [payroll,abc123def456,002] 接收到创建用户请求
2025-12-18 10:30:15.567 INFO [payroll,abc123def456,002] 调用Policy服务查询政策
2025-12-18 10:30:15.678 INFO [policy,abc123def456,003] 查询税务政策
2025-12-18 10:30:15.789 INFO [policy,abc123def456,003] 返回政策数据
2025-12-18 10:30:15.890 INFO [payroll,abc123def456,002] 创建用户成功
2025-12-18 10:30:16.001 INFO [payslip-api,abc123def456,001] 注册成功
6.3 根据 TraceId 搜索
java
# 搜索某个 TraceId 的所有日志
grep "abc123def456" logs/*.log
# 结果:
logs/payslip-api.log: [payslip-api,abc123def456,001] 接收到注册请求
logs/payslip-api.log: [payslip-api,abc123def456,001] 调用Payroll服务
logs/payroll.log: [payroll,abc123def456,002] 接收到创建用户请求
logs/payroll.log: [payroll,abc123def456,002] 调用Policy服务
logs/policy.log: [policy,abc123def456,003] 查询税务政策
6.4 分析调用链
java
TraceId: abc123def456
总耗时: 886ms
调用链:
┌─────────────────────────────────────────────────────────┐
│ │
│ payslip-api (SpanId: 001) │
│ ├─ 10:30:15.123 接收请求 │
│ ├─ 10:30:15.234 验证手机号 (+111ms) │
│ ├─ 10:30:15.345 调用Payroll (+111ms) │
│ │ │
│ │ payroll (SpanId: 002) │
│ │ ├─ 10:30:15.456 接收请求 (+111ms) │
│ │ ├─ 10:30:15.567 调用Policy (+111ms) │
│ │ │ │
│ │ │ policy (SpanId: 003) │
│ │ │ ├─ 10:30:15.678 查询政策 (+111ms) │
│ │ │ └─ 10:30:15.789 返回数据 (+111ms) │
│ │ │ │
│ │ └─ 10:30:15.890 创建成功 (+101ms) │
│ │ │
│ └─ 10:30:16.001 注册成功 (+111ms) │
│ │
└─────────────────────────────────────────────────────────┘
耗时分析:
- payslip-api 自身: 222ms
- payroll 服务: 434ms (其中 policy: 222ms)
- 总计: 886ms
瓶颈: payroll 服务耗时最长,需要优化
3.核心知识点
3.1Sleuth 自动处理的场景
java
| 场景 | 是否自动传递 | 说明 |
| RestTemplate | ✅ | 自动添加Header |
| Feign | ✅ | 自动添加Header |
| @Async | ✅ | 自动传递上下文 |
| 线程池 | ❌ | 需要手动处理 |
| MQ消息 | ❌ | 需要手动传递 |
3.2手动传递(线程池场景)
java
@Service
public class AsyncService {
@Autowired
private Tracer tracer;
public void asyncTask() {
// 获取当前Span
Span currentSpan = tracer.currentSpan();
// 在新线程中使用
executorService.submit(() -> {
try (Tracer.SpanInScope ws = tracer.withSpanInScope(currentSpan)) {
// 这里的日志会带上正确的 TraceId
log.info("异步任务执行中...");
}
});
}
}
3.3日志配置(logback-spring.xml)
java
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 定义日志格式,包含 TraceId -->
<property name="LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %5p [${springAppName},%X{traceId},%X{spanId}] %logger{36} - %msg%n"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/${springAppName}.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/${springAppName}.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
4.实际排查问题流程
java
1. 用户反馈:"注册失败了"
↓
2. 让用户提供 TraceId(或从日志中找)
TraceId: abc123def456
↓
3. 搜索所有服务日志
grep "abc123def456" /var/log/*/app.log
↓
4. 找到错误日志
[policy,abc123def456,003] ERROR 查询政策失败: Connection refused
↓
5. 定位问题
Policy 服务连接数据库失败
↓
6. 解决问题
重启 Policy 服务 / 检查数据库连接
5.常见问题
5.1日志没有 TraceId
java
检查:
- 是否添加了 sleuth 依赖
- 日志格式是否配置了 %X{traceId}
- 是否在 Spring 管理的 Bean 中打印日志
5.2Feign 调用没有传递 TraceId
java
检查:
- 是否使用 @FeignClient(不是手动 HttpClient)
- Sleuth 版本是否兼容
5.3采样率设置
java
spring:
sleuth:
sampler:
probability: 0.1 # 生产环境设 10%,避免性能影响