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

前言

目前市面上关于 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 项目。

演进路径:

scss 复制代码
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 等多种后端

项目架构

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

核心依赖

pom.xml 配置

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

yaml 复制代码
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 复制代码
<?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。

java 复制代码
@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 客户端

java 复制代码
@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 配置

java 复制代码
@Configuration
public class RestTemplateConfig {

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

自动集成:

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

4. 异步服务

java 复制代码
@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)

请求:

bash 复制代码
curl http://localhost:8081/user/feign/U001

日志输出:

用户服务:

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

商品服务:

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

验证结果:

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

测试场景 2:异步调用

请求:

bash 复制代码
curl http://localhost:8081/user/async/U003

日志输出:

主线程:

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

异步线程 1:

ini 复制代码
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 调用商品服务:

ini 复制代码
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 : 【异步调用】成功

商品服务:

ini 复制代码
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

解决:

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

问题2:日志中没有 TraceId

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

解决:

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

问题3:跨服务 TraceId 不一致

原因: 缺少 feign-micrometer 依赖

解决:

xml 复制代码
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-micrometer</artifactId>
</dependency>

项目结构

bash 复制代码
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. 编译项目

bash 复制代码
mvn clean install

2. 启动商品服务

bash 复制代码
cd product-service
mvn spring-boot:run

3. 启动用户服务

bash 复制代码
cd user-service
mvn spring-boot:run

4. 测试

bash 复制代码
# 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 后再迁移。

相关推荐
捧 花2 小时前
Go Web 中 WebSocket 原理与实战详解
网络·后端·websocket·网络协议·http·golang·web
serendipity_hky2 小时前
【SpringCloud | 第3篇】Sentinel 服务保护(限流、熔断降级)
java·后端·spring·spring cloud·微服务·sentinel
漂亮的小碎步丶2 小时前
【2】Spring Boot自动装配
java·spring boot·后端
Python极客之家2 小时前
基于Django的高校二手市场与社交系统
后端·python·数据挖掘·django·毕业设计
想用offer打牌3 小时前
一站式了解长轮询,SSE和WebSocket
java·网络·后端·websocket·网络协议·系统架构
Vespeng3 小时前
利用周末写一个小工具:多设备预览图生成
后端·开源·go
Li_7695323 小时前
服务架构相关知识及演进
后端·架构
码界奇点3 小时前
基于SpringBoot与Vue3的多租户中后台管理系统设计与实现
java·spring boot·后端·spring·车载系统·毕业设计·源代码管理
x***B4113 小时前
Spring Boot 实战项目如何写进简历?经验分享
经验分享·spring boot·后端