Spring Boot 3 可观测性实战:Micrometer Tracing 与 OpenTelemetry
一、可观测性不是“多打日志”
可观测性的三根支柱是:
- Logs:某个时间点发生了什么;
- Metrics:系统整体是否异常,例如错误率、延迟分位数、线程池队列长度;
- Traces:一次请求经过了哪些组件,时间消耗在哪里。
三者各自有价值,但关联后才真正高效:
告警:orders.create 的 P95 延迟超过 800ms
↓ 按 service、uri、status 缩小范围
Trace:找到一个 2.4s 的请求
↓ 查看 Span 瀑布图
发现 payment-service HTTP 调用耗时 2.1s
↓ 使用 traceId 搜索日志
定位到下游连接池等待和超时重试
Spring Boot 3 使用 Micrometer Observation 统一表达一次“被观测的操作”。一个 Observation 可以同时产生指标和 Trace Span;Micrometer Tracing 再把追踪数据桥接到 OpenTelemetry。
二、组件之间是什么关系
Spring MVC / RestClient / DataSource / 自定义业务代码
↓
Micrometer Observation
↙ ↘
Micrometer Metrics Micrometer Tracing
↓
OpenTelemetry Bridge
↓ OTLP
OpenTelemetry Collector
↓
Tempo / Jaeger / 其他后端
应用代码优先使用 Micrometer 的 ObservationRegistry 或 Tracer,不要同时手工维护另一套 OpenTelemetry SDK。Spring Boot 会负责自动配置和生命周期管理,OTLP 只是导出协议。
三、添加依赖
<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>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
不要在使用 Spring Boot BOM 时手工拼凑这些依赖的版本。版本不一致很容易引发自动配置缺失、方法找不到或上下文传播失败。
如果只需要 Trace 导出,可以去掉 Prometheus Registry;它在本文中用于演示指标端点。
四、配置 OTLP、采样和 Actuator
spring:
application:
name: order-service
management:
endpoints:
web:
exposure:
include: health,info,prometheus
endpoint:
health:
probes:
enabled: true
tracing:
sampling:
probability: 0.1
propagation:
type: w3c
baggage:
remote-fields: tenant-id
correlation:
fields: tenant-id
otlp:
tracing:
endpoint: http://otel-collector:4318/v1/traces
timeout: 10s
observations:
key-values:
region: cn-east-1
stack: prod
关键点:
spring.application.name会成为服务识别的重要维度;probability: 0.1表示约 10% 的请求创建可导出的 Trace;w3c使用traceparent、tracestate与baggage标准 Header;management.otlp.tracing.endpoint指向 Collector 的 HTTP Trace 接收端点;region、stack这类低基数标签适合作为公共维度。
开发环境可以暂时把采样率设为 1.0,生产环境不要默认全量采样。采样率应根据吞吐、故障定位要求、存储成本和后端限额决定。
Actuator 端点也不应全部公开。env、configprops、heapdump 等端点可能泄露配置或占用大量资源,必须通过独立管理端口、网络策略和鉴权保护。
五、让日志自动带上 TraceId 和 SpanId
Spring Boot 在有效追踪上下文中会把 traceId、spanId 放入 MDC。可以自定义控制台日志格式:
logging:
pattern:
console: >-
%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level
[${spring.application.name:unknown},trace=%X{traceId:-},span=%X{spanId:-}]
[%thread] %logger{36} - %msg%n
示例输出:
2026-07-05 20:16:32.117 INFO
[order-service,trace=7a9f31d8a2f0d9138e1eae74d9c74421,span=0a4fd37b9953a12c]
[http-nio-8080-exec-4] c.e.order.OrderController - create order, skuId=10001
TraceId 是跨系统关联键,SpanId 是当前操作的局部标识。不要自己为每个服务生成新的 TraceId,否则链路会在服务边界断开。
结构化日志系统中,应把 traceId、spanId 保存为独立字段,而不是只嵌入 message,后续检索和聚合会更可靠。
六、跨服务调用必须使用自动配置的 Builder
Spring Boot 会给 RestClient.Builder、WebClient.Builder 和 RestTemplateBuilder 安装观测拦截器。必须注入这些 Builder 创建客户端:
package com.example.order.client;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
@Component
public class InventoryClient {
private final RestClient restClient;
public InventoryClient(RestClient.Builder builder) {
this.restClient = builder
.baseUrl("http://inventory-service:8080")
.build();
}
public InventoryView findBySku(long skuId) {
return restClient.get()
.uri("/api/inventory/{skuId}", skuId)
.retrieve()
.body(InventoryView.class);
}
}
不要这样做:
RestClient restClient = RestClient.create();
直接创建的客户端没有 Spring Boot 定制器,通常不会自动注入 traceparent,于是上游和下游会显示成两条不相关的 Trace。
还要保证代理、网关和服务网格允许追踪 Header 通过。Header 在应用内正确生成,并不代表一定能穿过所有中间层。
七、为关键业务阶段创建自定义 Observation
框架能自动观测 HTTP 请求,但它不知道“库存预占”和“价格计算”哪个阶段更关键。可以围绕业务操作创建 Observation:
package com.example.order.application;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.stereotype.Service;
@Service
public class OrderApplicationService {
private final ObservationRegistry observationRegistry;
private final InventoryClient inventoryClient;
public OrderApplicationService(
ObservationRegistry observationRegistry,
InventoryClient inventoryClient) {
this.observationRegistry = observationRegistry;
this.inventoryClient = inventoryClient;
}
public OrderView create(CreateOrderCommand command) {
return Observation.createNotStarted(
"order.create", observationRegistry)
.contextualName("create order")
.lowCardinalityKeyValue("channel", command.channel())
.highCardinalityKeyValue(
"customer.id", command.customerId())
.observe(() -> doCreate(command));
}
private OrderView doCreate(CreateOrderCommand command) {
inventoryClient.findBySku(command.skuId());
// 校验价格、保存订单、发送领域事件
return new OrderView(1001L, "CREATED");
}
}
低基数与高基数必须分清:
- 低基数:取值集合有限,如
channel=APP|WEB、result=success|failure; - 高基数:取值几乎无限,如用户 ID、订单号、请求 ID。
低基数键会进入指标和 Trace,高基数键只应进入 Trace。把订单号放进 Metrics 标签会制造海量时间序列,显著增加 Prometheus 和后端存储压力。
同一个控制器或 Repository 已经被框架自动观测时,不要再无差别加一层同名 Observation,否则会产生重复 Span。自定义 Observation 应表达框架不知道的业务阶段。
八、什么时候使用注解
如果团队偏好注解,可以启用 Micrometer Observation 注解扫描:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
management:
observations:
annotations:
enabled: true
然后使用 @Observed 或 @NewSpan:
import io.micrometer.observation.annotation.Observed;
@Observed(name = "price.calculate", contextualName = "calculate price")
public Money calculatePrice(PricingCommand command) {
return pricingEngine.calculate(command);
}
注解适合稳定的方法边界;编程式 Observation 更适合动态标签、只观测某个代码片段或精确控制错误记录。两种方式不要在同一边界重复使用。
九、Baggage:少量传播业务上下文
Baggage 可以把租户号等少量字段随 Trace 传播:
management:
tracing:
baggage:
remote-fields: tenant-id
correlation:
fields: tenant-id
remote-fields 允许字段跨网络传播,correlation.fields 把字段放入 MDC,方便日志检索。
但 Baggage 会进入请求 Header 并跨多个服务复制,应遵循三个限制:
- 不放密码、Token、手机号等敏感信息;
- 不放大对象或数量不受控的键值;
- 只传播确实用于诊断或路由的字段。
Baggage 不是业务参数传输协议。下游业务逻辑需要的关键数据仍应在 API 契约中明确声明。
十、异步线程为什么容易丢 Trace
追踪上下文通常绑定在当前执行上下文。把任务提交给自建线程池后,如果没有上下文传播,日志里的 TraceId 会消失。
Spring Framework 提供了上下文传播 TaskDecorator:
package com.example.order.config;
import java.util.concurrent.Executor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "applicationTaskExecutor")
Executor applicationTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(32);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("order-async-");
executor.setTaskDecorator(new ContextPropagatingTaskDecorator());
executor.initialize();
return executor;
}
}
使用 CompletableFuture.supplyAsync(task) 时会进入公共线程池,也可能丢失上下文。应显式传入由应用管理、已配置上下文传播的 Executor。
异步消息还需要在生产端注入上下文、消费端提取上下文。若使用的消息组件没有自动埋点,需要在消息 Header 与 Observation 之间建立清晰的传播约定。
十一、采样不是简单地“保留 10% 日志”
management.tracing.sampling.probability 是头部采样:请求进入系统时决定是否采样。它实现简单、开销可控,但随机 10% 可能刚好丢掉稀有错误。
实践中可分三层:
- 开发环境:短期 100%,方便验证链路;
- 普通生产流量:按成本设置 1%~20% 的头部采样;
- 关键错误或高延迟:在 Collector 侧评估尾部采样,等待 Trace 完整后再决定保留。
尾部采样需要 Collector 暂存更多数据,资源成本和配置复杂度更高。不要在没有容量评估时直接开启全量 Trace。
另外,采样只影响 Trace 是否导出,不应影响业务日志和核心指标的正确性。
十二、验证链路是否真的打通
按以下顺序验证,比直接打开 Trace UI 更快:
- 请求服务后,日志是否出现非空
traceId和spanId; - 下游服务日志是否出现相同 TraceId;
- 抓取或临时记录请求 Header,确认存在
traceparent; - Collector 是否监听正确协议和端口;
- 应用到 Collector 的网络是否畅通;
- Collector 是否成功向后端导出;
- Trace 后端的时间范围、服务名筛选是否正确。
测试请求:
curl -i http://localhost:8080/api/orders/1001
指标端点:
curl http://localhost:8080/actuator/prometheus
如果应用日志有 TraceId,但后端没有 Trace,问题通常在采样、Exporter、Collector 或网络;如果第一个服务有 TraceId、下游换了新的 TraceId,问题通常在客户端创建方式或 Header 传播。
十三、常见问题排查表
| 现象 | 原因与处理 |
|---|---|
| 日志中 TraceId 始终为空 | 检查 Actuator、Tracing Bridge 是否存在,请求是否经过被观测的入口 |
| 下游生成新的 TraceId | 使用自动配置的 HTTP Client Builder,检查网关是否保留 traceparent |
| Collector 收不到数据 | 检查 OTLP HTTP/gRPC 协议、端口、路径和容器网络 |
| Trace 数量远低于请求量 | 检查采样率;这不一定是故障 |
| 指标时间序列暴涨 | 高基数字段被错误放入低基数标签 |
@Async 中 TraceId 消失 |
为线程池配置上下文传播 TaskDecorator |
| 一个请求出现重复 Span | 同一框架边界同时存在自动埋点和自定义注解/Observation |
| Trace 后端能看到链路但日志搜不到 | 日志格式未输出 MDC 字段,或日志采集器没有解析独立字段 |
十四、生产治理清单
- 为每个服务设置稳定且唯一的
spring.application.name; - 使用 W3C Trace Context 作为跨语言默认传播格式;
- HTTP 客户端统一由 Spring Bean 管理,禁止业务代码随意
create(); - 只对关键业务阶段添加自定义 Observation;
- Metrics 标签严格限制为低基数,用户 ID、订单号只进入 Trace 或日志;
- 生产采样率由吞吐、存储成本和排障目标共同决定;
- Collector 部署高可用,并监控自身队列、丢弃量和导出失败;
- TraceId、SpanId 进入结构化日志字段;
- Baggage 不携带凭证和个人敏感信息;
- Actuator 端点实施最小暴露、鉴权和网络隔离;
- 对跨服务、异步线程和消息消费分别做链路连续性测试。
总结
Spring Boot 3 的可观测性主线并不复杂:框架和业务代码通过 Micrometer Observation 产生观测数据,Micrometer Tracing 建立追踪语义,OpenTelemetry Bridge 与 OTLP 负责把 Trace 发送给 Collector。
真正决定方案质量的是工程约束:统一创建 HTTP 客户端、控制标签基数、正确传播异步上下文、合理采样,并把 TraceId 与结构化日志打通。完成这些工作后,排障过程才能从“逐台机器猜问题”变成“从指标定位异常,再沿 Trace 和日志找到根因”。
参考资料
更多推荐




所有评论(0)