一个 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) {}
在实际工程中,还要配置连接与响应超时、错误状态映射和重试边界;链路追踪不负责解决网络可靠性。
如何验证跨服务传播?
在两个服务都启用追踪的前提下:
- 向订单服务发起请求。
- 检查订单服务与库存服务日志的 TraceId。
- 在追踪后端查看父子 Span 关系。
- 故意使库存服务返回 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,追踪后端却没有?
按以下顺序排查:
- 采样率:当前请求是否被采样?
- Exporter:是否引入并配置了 OTLP 导出?
- 网络:Collector 的 4318 HTTP 端口是否可达?
- 路径 :使用 HTTP 导出时是否包含
/v1/traces? - 服务名称 :
spring.application.name是否正确? - 跨服务协议:双方传播协议是否兼容?
- 自定义客户端:是否绕开了 Spring 的自动配置?
- 异步线程:是否丢失上下文?
- 后端: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"代替端到端验证。