SpringCloud-Sleuth链路追踪实战

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 会自动处理:

  1. HTTP请求(RestTemplate、Feign)自动传递 TraceId
  2. 日志自动带上 TraceId
  3. 异步任务(@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%,避免性能影响
相关推荐
SUPER52662 小时前
本地开发环境_spring-ai项目启动异常
java·人工智能·spring
冷崖2 小时前
原子锁操作
c++·后端
moxiaoran57532 小时前
Spring AOP开发的使用场景
java·后端·spring
一线大码6 小时前
Gradle 基础篇之基础知识的介绍和使用
后端·gradle
Java猿_6 小时前
Spring Boot 集成 Sa-Token 实现登录认证与 RBAC 权限控制(实战)
android·spring boot·后端
小王师傅666 小时前
【轻松入门SpringBoot】actuator健康检查(上)
java·spring boot·后端
码事漫谈7 小时前
C++高并发编程核心技能解析
后端
码事漫谈7 小时前
C++与浏览器交织-从Chrome插件到WebAssembly,开启性能之门
后端
IT 行者7 小时前
Spring Security 6.x 迁移到 7.0 的完整步骤
java·spring·oauth2