Spring Boot 链路追踪实战:Micrometer、TraceId 与异步线程上下文传播

一个 HTTP 请求依次调用订单服务、库存服务和支付服务,日志分散在多个容器里;遇到偶发超时,仅凭时间戳很难还原调用链。Spring Boot 的 Micrometer Observation 和 Tracing 能把请求、跨度和日志关联起来,但异步线程传播与敏感数据控制同样重要。

技术栈:Java 17、Spring Boot 3.5.x、Micrometer Tracing、OpenTelemetry、OTLP;以 Spring Boot 3.5 官方文档为准。

一、先区分日志、指标与追踪

  • 日志(Logs):记录具体事件、异常与上下文。
  • 指标(Metrics):统计请求量、延迟分布、错误率。
  • 追踪(Traces):通过 Trace 和 Span 还原跨服务调用关系。

TraceId 用于关联一次逻辑请求,SpanId 表示其中一个操作跨度。有 TraceId 不等于自动采集了完整链路;缺少埋点或传播头时,下游可能成为另一个 Trace。

二、建立最小项目依赖

Spring Boot 3.5 中,Actuator、Micrometer Tracing 桥接与 OTLP 导出是不同的职责。以 Maven 为例:

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-tracing-bridge-otel</artifactId>
    </dependency>
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-exporter-otlp</artifactId>
    </dependency>
</dependencies>

依赖版本交给与 Spring Boot 3.5.x 匹配的 BOM 管理。不要随意混用 Boot 4 的自动配置与 Boot 3 的依赖说明。

开发环境可以使用:

yaml 复制代码
spring:
  application:
    name: order-service

management:
  tracing:
    sampling:
      probability: 1.0
  otlp:
    tracing:
      endpoint: http://localhost:4318/v1/traces

这里的地址假设本地有接受 OTLP HTTP 的 Collector。如果没有启动 Collector,不能声称链路已经成功导出。生产环境应根据成本和调查需求设置采样率,通常不应盲目全量采样。

三、HTTP 调用为什么要使用自动配置的客户端?

Spring Boot 可以为受管理的 HTTP 客户端配置追踪相关拦截器。自己直接 new 一个客户端,可能绕开自动配置,导致传播头没有发送。

以 RestClient 为例,注入 Spring 提供的 Builder:

java 复制代码
@Configuration
class HttpClientConfiguration {

    @Bean
    RestClient inventoryClient(RestClient.Builder builder) {
        return builder
                .baseUrl("http://inventory-service:8080")
                .build();
    }
}

业务 Service:

java 复制代码
@Service
class InventoryGateway {

    private final RestClient inventoryClient;

    InventoryGateway(RestClient inventoryClient) {
        this.inventoryClient = inventoryClient;
    }

    StockResponse getStock(long productId) {
        return inventoryClient.get()
                .uri("/api/stock/{id}", productId)
                .retrieve()
                .body(StockResponse.class);
    }
}

record StockResponse(long productId, int available) {}

在实际工程中,还要配置连接与响应超时、错误状态映射和重试边界;链路追踪不负责解决网络可靠性。

如何验证跨服务传播?

在两个服务都启用追踪的前提下:

  1. 向订单服务发起请求。
  2. 检查订单服务与库存服务日志的 TraceId。
  3. 在追踪后端查看父子 Span 关系。
  4. 故意使库存服务返回 500,观察错误记录是否关联同一 Trace。

预期:受正确配置的同步 HTTP 调用可以传递追踪上下文。实际是否成功取决于客户端构造方式、传播协议和采样配置,本文没有运行这一组实验。

四、自定义业务跨度:ObservationRegistry

默认 HTTP Span 只能告诉我们"请求慢",无法定位具体的业务计算阶段。可使用 Micrometer Observation 包裹关键操作:

java 复制代码
@Service
class PriceService {

    private final ObservationRegistry registry;

    PriceService(ObservationRegistry registry) {
        this.registry = registry;
    }

    BigDecimal calculate(String channel, BigDecimal price) {
        return Observation.createNotStarted(
                        "order.price.calculate", registry)
                .lowCardinalityKeyValue("channel", channel)
                .observe(() -> price.multiply(
                        new BigDecimal("0.95")));
    }
}

这里 channel 应来自有限枚举或固定白名单。如果它包含用户自由输入,会造成指标标签高基数,导致存储与查询成本增加。

低基数和高基数标签的区别

字段 推荐方式 原因
请求方法 GET/POST 低基数 取值有限
业务类型 低基数 枚举可控
用户 ID 不放低基数标签 每个用户产生新序列
订单号 必要时仅用于受控 Trace 唯一值过多,且可能敏感
Token / 密码 不记录 敏感凭据

Micrometer 官方文档区分低基数与高基数 KeyValue:低基数会进入指标与追踪,高基数通常仅用于追踪。仍要检查日志、后端索引和数据保留策略。

五、@Async 之后 TraceId 为什么丢失?

异步执行会切换线程,而追踪上下文通常依赖当前执行上下文。直接把任务交给不受管理的线程池,可能失去父 Span。

Spring Boot 官方建议对 AsyncTaskExecutor 配置 ContextPropagatingTaskDecorator。

java 复制代码
@Configuration
@EnableAsync
class AsyncConfiguration {

    @Bean("businessExecutor")
    ThreadPoolTaskExecutor businessExecutor() {
        ThreadPoolTaskExecutor executor =
                new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(8);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("business-");
        executor.setTaskDecorator(
                new ContextPropagatingTaskDecorator());
        executor.initialize();
        return executor;
    }
}

业务方法:

java 复制代码
@Service
class AuditService {

    @Async("businessExecutor")
    public CompletableFuture<Void> writeAudit(long orderId) {
        // 在此调用真正的审计持久化组件
        return CompletableFuture.completedFuture(null);
    }
}

需要注意两个边界:

  • @Async 的代理调用机制与 @Transactional 类似,同类内部自调用通常不能触发异步代理。
  • 上面仅演示上下文传播,并未实现可靠审计。重要审计或消息不能仅靠内存线程池;进程退出时可能丢任务,需持久化队列或 Outbox。

手动线程池与 CompletableFuture

如果业务自行创建 Executors.newFixedThreadPool(),不能假设 Boot 自动为其装饰上下文。应使用受管理执行器,或者在任务提交处显式使用 Micrometer Context Propagation 提供的上下文快照机制。

异步传播还需要测试:父请求完成后,子任务是否仍能正确建立跨度,以及取消和异常时是否释放上下文。

六、为什么日志有 TraceId,追踪后端却没有?

按以下顺序排查:

  1. 采样率:当前请求是否被采样?
  2. Exporter:是否引入并配置了 OTLP 导出?
  3. 网络:Collector 的 4318 HTTP 端口是否可达?
  4. 路径 :使用 HTTP 导出时是否包含 /v1/traces?
  5. 服务名称 :spring.application.name 是否正确?
  6. 跨服务协议:双方传播协议是否兼容?
  7. 自定义客户端:是否绕开了 Spring 的自动配置?
  8. 异步线程:是否丢失上下文?
  9. 后端:Collector 是否真的把数据转发给存储或 UI?

不要因为日志里有 TraceId 就直接推断 Collector 已收到 Span。日志关联与追踪导出是两个不同环节。

七、测试思路:先验证上下文,再验证后端

单元层面

可以对自定义 Observation 包裹的业务逻辑做普通单元测试,确保异常仍向外传播,而不是被观测代码吞掉。

java 复制代码
@Test
void calculationShouldKeepBusinessResult() {
    ObservationRegistry registry =
            ObservationRegistry.create();

    PriceService service = new PriceService(registry);
    BigDecimal result = service.calculate(
            "WEB", new BigDecimal("100.00"));

    assertEquals(0,
            new BigDecimal("95.0000").compareTo(result));
}

这个测试只证明计算结果与包装行为,不证明 Trace 已成功导出。

集成层面

建议在隔离测试环境启动两个 Spring Boot 服务和 Collector,分别检查:

  • HTTP 入站 Span 与出站 Span 是否属于同一 Trace;
  • 异步任务是否继承必要的上下文;
  • 异常是否被正确记录;
  • 关闭 Collector 后业务是否仍按设计处理;
  • 采样率变化是否符合预期;
  • 日志与 Trace 是否包含不该出现的敏感字段。

常见误区

误区一:追踪能替代业务日志。 Trace 更擅长描述调用关系,业务决策仍需要结构化日志或审计记录。

误区二:把用户 ID 放到每个指标标签。 这会产生高基数时序数据。

误区三:生产环境直接 100% 采样。 可能带来不必要的成本和开销。

误区四:把所有异常都当成网络问题。 追踪只提供证据,根因还需结合 SQL、线程池和下游日志分析。

总结

Spring Boot 链路追踪真正需要打通的是:自动配置的 HTTP 客户端、业务 Observation、异步上下文传播、OTLP 导出和可观察的故障排查流程。

先在隔离环境验证 TraceId 能跨服务延续,再逐步配置采样、标签与保留策略。不要用"日志里有 TraceId"代替端到端验证。

官方参考资料

相关推荐
计算机毕设定制辅导-无忧学长24 分钟前
《基于Spring Boot的迷游哈游戏综合服务平台的开发》
java·vue.js·spring boot·mysql·迷游哈游戏综合服务平台
非优秀程序员9 小时前
第07章 孙德良的阳谋|《互联网产品恩仇录》
javascript·后端
非优秀程序员9 小时前
第06章 浙江帮的内战|《互联网产品恩仇录》
后端
非优秀程序员9 小时前
第08章 马云的反击|《互联网产品恩仇录》
后端
非优秀程序员9 小时前
题记|《互联网产品恩仇录》
后端·程序员·创业
非优秀程序员10 小时前
第04章 邮箱大战,163 与 QQ 的暗战|《互联网产品恩仇录》
java·vue.js·架构
知已难寻10 小时前
Docker 入门:容器技术与 Docker 基础
java·运维·服务器·docker·云原生·容器
文丑颜不良啊12 小时前
LeetCode2481~2500题目解析
java·leetcode
Flynn Liang13 小时前
同一需求三套写法:用 Spring AI、LangChain4j、AgentScope Java 各实现一个带工具调用的 RAG 助手(实战对比)
java·人工智能·大模型·架构设计·spring ai·langchain4j·agentscope