告别手写 TraceId!Micrometer 链路追踪在 Spring Boot 中的落地实践

864 阅读7分钟

前言

目前市面上关于 Spring 链路追踪的资料要么过时,要么残缺。在上一篇文章《彻底搞懂微服务 TraceId 传递:ThreadLocal、TTL 与全链路日志追踪实战》中,我们详细讲解了如何通过 TransmittableThreadLocal (TTL) 手动实现 TraceId 的全链路传递。那套方案能够完美解决异步场景下的上下文传递问题,但需要手动编写不少代码。

本文基于真实迁移经验,介绍企业级的解决方案:Spring Boot 3.0.2 + Micrometer Tracing。这是 Spring 官方推荐的链路追踪方案,给出一套覆盖所有调用场景、可直接用于生产的方案。

技术演进:从 Sleuth 到 Micrometer Tracing

为什么不再使用 Spring Cloud Sleuth?

Spring Cloud Sleuth 是 Spring Boot 2.x 时代的链路追踪组件,但从 Spring Boot 3.0 开始,Sleuth 已停止支持。官方将核心功能迁移到了 Micrometer Tracing 项目。

演进路径:

Spring Boot 2.x  →  Spring Cloud Sleuth 3.1.x (最终版本)
                                ↓
Spring Boot 3.x  →  Micrometer Tracing 1.0+ (官方继任者)

Micrometer Tracing 的优势

  1. 原生集成:Spring Boot 3.x 原生支持,无需额外配置
  2. 自动化:TraceId/SpanId 自动生成、传递、注入日志
  3. 零侵入:业务代码无需改动
  4. 标准化:支持 OpenTelemetry、Zipkin、Brave 等多种后端

项目架构

本文通过一个实战项目演示完整的链路追踪方案,项目包含两个微服务:

traceId.jpeg

核心依赖

pom.xml 配置

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.0.2</version>
</parent>

<properties>
    <java.version>17</java.version>
    <spring-cloud.version>2022.0.1</spring-cloud.version>
</properties>

<dependencies>
    <!-- Web 服务 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Actuator (包含 Micrometer) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <!-- Micrometer Tracing (链路追踪核心) -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-tracing-bridge-brave</artifactId>
    </dependency>

    <!-- Context Propagation (异步上下文传递) -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>context-propagation</artifactId>
        <version>1.0.2</version>
    </dependency>

    <!-- OpenFeign (服务调用) -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

    <!-- Feign + Micrometer 集成 -->
    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-micrometer</artifactId>
    </dependency>
</dependencies>

关键点:

  • 不需要 spring-cloud-starter-sleuth(已废弃)
  • micrometer-tracing-bridge-brave 是链路追踪的核心
  • context-propagation 用于异步场景的上下文传递

配置文件

application.yml

spring:
  application:
    name: user-service  # 服务名称,会自动注入日志

server:
  port: 8081

# Micrometer Tracing 配置
management:
  tracing:
    sampling:
      probability: 1.0  # 采样率:1.0 = 100%(开发环境)
                        # 生产环境建议 0.1(10%)

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <springProperty scope="context" name="springAppName" source="spring.application.name"/>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %5p [${springAppName},%X{traceId},%X{spanId}] --- [%15.15t] %-40.40logger{39} : %m%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

日志格式说明:

  • %X{traceId}:自动注入 TraceId(16位十六进制)
  • %X{spanId}:自动注入 SpanId(8位十六进制)
  • ${springAppName}:服务名称

关键实现

1. 异步配置(重点)

异步场景是链路追踪的难点,需要特殊配置才能正确传递 TraceId。

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(25);
        executor.setThreadNamePrefix("async-");
        
        // 关键:配置 TaskDecorator 传递上下文
        executor.setTaskDecorator(new TaskDecorator() {
            @Override
            public Runnable decorate(Runnable runnable) {
                // 在主线程中捕获上下文快照
                ContextSnapshot snapshot = ContextSnapshot.captureAll();
                return () -> {
                    // 在异步线程中恢复上下文
                    try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) {
                        runnable.run();
                    }
                };
            }
        });
        
        executor.initialize();
        return executor;
    }
}

工作原理:

sequenceDiagram
    autonumber
    participant Main as 主线程(HTTP-Thread)
    participant Decorator as TaskDecorator(任务装饰器)
    participant Snapshot as ContextSnapshot(上下文快照)
    participant Pool as 线程池队列
    participant Async as 异步线程(async-1)
    
    rect rgb(230, 245, 255)
    Note over Main,Snapshot: 阶段1:提交任务时捕获上下文
    Main->>Main: TraceId = abc123 存储在 ThreadLocal
    Main->>Decorator: 提交异步任务
    Decorator->>Snapshot: captureAll() 捕获所有 ThreadLocal 值
    Snapshot-->>Decorator: 返回快照: traceId = abc123
    Decorator->>Pool: 封装后的任务入队
    end
    
    rect rgb(245, 255, 230)
    Note over Pool,Async: 阶段2:执行任务时恢复上下文
    Pool->>Async: 线程池分配线程执行
    Async->>Snapshot: setThreadLocals() 恢复快照到当前线程
    Snapshot->>Async: TraceId = abc123 注入到 ThreadLocal
    Async->>Async: 执行业务逻辑 log.info() 自动带 TraceId
    end
    
    rect rgb(255, 245, 230)
    Note over Async: 阶段3:任务完成后清理
    Async->>Async: finally 块自动清理 ThreadLocal
    Async-->>Main: 任务执行完成
    end

2. OpenFeign 客户端

@FeignClient(name = "product-service", url = "http://localhost:8082")
public interface ProductFeignClient {
    
    @GetMapping("/product/{productId}")
    String getProductInfo(@PathVariable("productId") String productId);
}

自动集成:

  • 引入 feign-micrometer 依赖后,Feign 会自动在 HTTP 请求头中添加 TraceId
  • 无需手动编写拦截器

3. RestTemplate 配置

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.build();
    }
}

自动集成:

  • Spring Boot 3.x 会自动为 RestTemplate 配置链路追踪拦截器
  • 使用 RestTemplateBuilder 构建即可

4. 异步服务

@Service
public class AsyncService {

    private static final Logger log = LoggerFactory.getLogger(AsyncService.class);

    @Autowired
    private RestTemplate restTemplate;

    @Async("taskExecutor")  // 使用配置的线程池
    public void asyncTask(String userId) {
        log.info("【异步任务】开始执行,userId: {}", userId);
        
        // 模拟耗时操作
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        log.info("【异步任务】执行完成,userId: {}", userId);
    }

    @Async("taskExecutor")
    public void asyncCallProductService(String productId) {
        log.info("【异步调用】开始调用商品服务,productId: {}", productId);
        
        // 异步线程中也能正确传递 TraceId
        String result = restTemplate.getForObject(
            "http://localhost:8082/product/" + productId, 
            String.class
        );
        
        log.info("【异步调用】商品服务返回: {}", result);
    }
}

测试验证

测试场景 1:同步调用(OpenFeign)

请求:

curl http://localhost:8081/user/feign/U001

日志输出:

用户服务:

2025-12-14 17:00:00.123  INFO [user-service,abc123def456,abc123de] --- [nio-8081-exec-1] UserController : 【用户服务-Feign】收到请求,userId: U001

商品服务:

2025-12-14 17:00:00.145  INFO [product-service,abc123def456,f456789a] --- [nio-8082-exec-1] ProductController : 【商品服务】收到请求,productId: P001

验证结果:

  • TraceId 完全相同:abc123def456
  • 每个服务有独立的 SpanId

测试场景 2:异步调用

请求:

curl http://localhost:8081/user/async/U003

日志输出:

主线程:

2025-12-14 17:03:47.414  INFO [user-service,693e7d733e60,92fcdb11] --- [nio-8081-exec-1] UserController : 【用户服务-异步】收到请求

异步线程 1:

2025-12-14 17:03:47.419  INFO [user-service,693e7d733e60,92fcdb11] --- [async-1] AsyncService : 【异步任务】开始执行
2025-12-14 17:03:48.423  INFO [user-service,693e7d733e60,92fcdb11] --- [async-1] AsyncService : 【异步任务】执行完成

异步线程 2 调用商品服务:

2025-12-14 17:03:47.419  INFO [user-service,693e7d733e60,92fcdb11] --- [async-2] AsyncService : 【异步调用】开始
2025-12-14 17:03:47.503  INFO [user-service,693e7d733e60,92fcdb11] --- [async-2] AsyncService : 【异步调用】成功

商品服务:

2025-12-14 17:03:47.492  INFO [product-service,693e7d733e60,4a4347a4] --- [nio-8082-exec-1] ProductController : 【商品服务】收到请求

验证结果:

  • 主线程、异步线程 1、异步线程 2、商品服务的 TraceId 完全相同693e7d733e60
  • 异步场景下 TraceId 正确传递

完整调用链路

核心流程图

flowchart TB
    Start([HTTP 请求]) --> Auto[自动生成 TraceId]
    Auto --> MDC[注入 MDC]
    MDC --> Business[业务处理]
    
    Business --> Sync[同步调用]
    Business --> Async[异步调用]
    
    Sync --> Header1[添加 HTTP Header]
    Async --> Snapshot[捕获上下文快照]
    Snapshot --> AsyncThread[异步线程恢复 TraceId]
    AsyncThread --> Header2[添加 HTTP Header]
    
    Header1 --> Downstream[下游服务]
    Header2 --> Downstream
    Downstream --> Extract[提取 TraceId]
    Extract --> End([继续传播])
    
    style Start fill:#4CAF50,stroke:#2E7D32,stroke-width:3px,color:#fff
    style Auto fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#fff
    style MDC fill:#2196F3,stroke:#1565C0,stroke-width:2px,color:#fff
    style Business fill:#9C27B0,stroke:#6A1B9A,stroke-width:2px,color:#fff
    style Snapshot fill:#E91E63,stroke:#C2185B,stroke-width:2px,color:#fff
    style Downstream fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#fff
    style End fill:#607D8B,stroke:#37474F,stroke-width:3px,color:#fff

详细说明

阶段1:请求进入

  • HTTP 请求到达服务后,Micrometer Tracing 自动生成 TraceId(16位十六进制)
  • TraceId 自动注入到 MDC(Mapped Diagnostic Context),供日志使用

阶段2:业务处理

  • 业务逻辑执行过程中,TraceId 一直存储在 ThreadLocal 中
  • 所有日志输出自动包含 TraceId 和 SpanId

阶段3:调用下游服务

同步调用(OpenFeign / RestTemplate):

  • Micrometer 自动拦截 HTTP 请求
  • 自动在 HTTP Header 中添加 traceparent 字段(W3C 标准)
  • 下游服务接收后自动提取 TraceId

异步调用(@Async):

  • 提交任务时,ContextSnapshot.captureAll() 捕获当前线程的 TraceId
  • 异步线程执行前,setThreadLocals() 恢复 TraceId 到新线程
  • 异步线程中的 HTTP 调用同样自动传递 TraceId

阶段4:链路传播

  • 下游服务从 HTTP Header 提取 TraceId
  • 继续传播到更下游的服务
  • 整个调用链使用相同的 TraceId

方案对比

手动实现(TTL 方案)vs 企业级方案(Micrometer Tracing)

对比项手动实现(TTL)Micrometer Tracing
代码量需要手动编写 Filter、拦截器、上下文管理几乎零代码,只需配置
异步支持需要配置 TtlRunnable 装饰器需要配置 ContextSnapshot
HTTP 调用需要手动实现拦截器自动集成
日志集成需要手动同步 MDC自动注入
维护成本较高,需要理解原理低,Spring 官方维护
扩展性灵活但复杂标准化,易于集成 Zipkin 等
学习曲线陡峭平缓

适用场景

手动实现方案适合:

  • 非 Spring Boot 项目
  • 需要高度定制的场景
  • 学习 ThreadLocal 原理

企业级方案适合:

  • Spring Boot 3.x 新项目
  • 快速落地
  • 标准化要求高的团队

常见问题

问题1:异步任务中 TraceId 丢失

原因: 未配置 TaskDecorator

解决:

executor.setTaskDecorator(runnable -> {
    ContextSnapshot snapshot = ContextSnapshot.captureAll();
    return () -> {
        try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) {
            runnable.run();
        }
    };
});

问题2:日志中没有 TraceId

原因: logback 配置未添加 %X{traceId}

解决:

<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId},%X{spanId}] %m%n</pattern>

问题3:跨服务 TraceId 不一致

原因: 缺少 feign-micrometer 依赖

解决:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-micrometer</artifactId>
</dependency>

项目结构

surfing-tracing-demo/
├── user-service/          # 用户服务 (8081)
│   ├── controller/
│   ├── service/
│   ├── feign/
│   ├── config/
│   │   ├── AsyncConfig.java       # 异步配置
│   │   └── RestTemplateConfig.java
│   └── resources/
│       ├── application.yml
│       └── logback-spring.xml
│   
└── product-service/       # 商品服务 (8082)
    ├── controller/
    ├── service/
    └── resources/
        ├── application.yml
        └── logback-spring.xml

启动步骤

1. 编译项目

mvn clean install

2. 启动商品服务

cd product-service
mvn spring-boot:run

3. 启动用户服务

cd user-service
mvn spring-boot:run

4. 测试

# OpenFeign 调用
curl http://localhost:8081/user/feign/U001

# RestTemplate 调用
curl http://localhost:8081/user/rest/U002

# 异步调用
curl http://localhost:8081/user/async/U003

# 综合测试
curl http://localhost:8081/user/all/U004

总结

通过 Spring Boot 3.0.2 + Micrometer Tracing 实现分布式链路追踪,相比手动实现方案有以下优势:

  1. 开箱即用:引入依赖即可,无需编写大量代码
  2. 自动化程度高:TraceId 生成、传递、注入日志全自动
  3. 标准化:符合 Spring 官方规范,易于维护
  4. 生产就绪:经过大量企业验证

对于新项目,强烈建议使用这套企业级方案。而对于老项目(Spring Boot 2.x),可以继续使用 Spring Cloud Sleuth 3.1.x,等升级到 Spring Boot 3.x 后再迁移。