分布式追踪:OTel、MDC 与异步传播
分布式日志关联的默认解法是 OpenTelemetry 上下文与 W3C Trace Context;MDC 只是把当前上下文投影到日志的桥梁。本文说明两者的分工、异步边界和 Spring Boot 的接入策略。
目录
| 章节 | 说明 |
|---|---|
| 关联模型 | trace、span、request 的职责 |
| 默认方案:OTel 与 W3C | 传播标准和日志关联 |
| MDC 的正确位置 | 生命周期、清理与补充上下文 |
| 异步与消息边界 | Executor、Reactor、Kafka 的传播原则 |
| Spring Boot 接入 | 使用 Micrometer Tracing 的建议 |
| 手工方案的适用范围 | 遗留系统的最小安全实现 |
关联模型
| 标识 | 含义 | 使用边界 |
|---|---|---|
trace_id | 一次端到端因果链路 | 32 位十六进制;跨进程传播 |
span_id | 链路中的一个操作 | 16 位十六进制;用于定位具体 RPC/DB 操作 |
request_id | 网关或产品定义的一次请求标识 | 可独立存在,不要冒充 trace ID |
| baggage | 跨服务的附加上下文 | 只放小型、非敏感、低基数的数据,设置白名单 |
一次请求会共享 trace_id,每个服务/操作产生自己的 span_id。日志中同时输出两者,才能既按整条链路查询,也能定位某个调用。标准格式见 OpenTelemetry Trace API。
默认方案:OTel 与 W3C
新系统应由 OpenTelemetry/Micrometer 自动创建上下文,并用 W3C traceparent 头在 HTTP、RPC 和消息属性中传播。不要自行定义 X-Trace-Id 作为默认协议;若必须兼容旧系统,应在边界做一次明确的转换和输入校验。
sequenceDiagram
participant C as Client
participant G as Gateway
participant O as Order Service
participant P as Payment Service
C->>G: traceparent
G->>O: traceparent(继承 trace,创建子 span)
O->>P: traceparent(继承 trace,创建子 span)
Note over G,P: 每条日志输出 trace_id 与 span_id
传播时不得盲目信任互联网来源的追踪头:入口应限制格式和长度,并将租户、用户身份等安全语义留在认证上下文,而不是由 trace header 决定。
MDC 的正确位置
MDC 是日志框架的线程本地键值上下文,适合在日志 pattern/JSON encoder 中输出 trace_id、span_id、request_id。它不是分布式追踪协议,也不能自行跨线程。
手工使用时必须成对设置和恢复,而不是简单 clear() 覆盖调用方已有上下文:
Map<String, String> previous = MDC.getCopyOfContextMap();
Map<String, String> next = Map.of("request_id", requestId);
try {
MDC.setContextMap(next);
chain.doFilter(request, response);
} finally {
if (previous == null) {
MDC.clear();
} else {
MDC.setContextMap(previous);
}
}
在 Servlet 线程结束、线程池任务结束和消息消费完成时都应清理/恢复。对 OTel 已自动关联的应用,优先使用框架提供的日志关联能力,不要再手动生成第二套 trace ID。
异步与消息边界
| 边界 | 首选方案 | 注意点 |
|---|---|---|
Executor / CompletableFuture | 使用 OTel context-aware executor 或框架自动 instrumentation | 任务提交时捕获上下文,任务完成后恢复原上下文 |
| Reactor / WebFlux | 使用 OTel Reactor instrumentation | MDC 依赖线程本地,不能假设跨 operator 自动可用 |
| Kafka/RabbitMQ | 使用 OTel instrumentation 注入/提取消息 headers | 消费者创建处理 span;处理重试和 DLQ 要保留关联 |
| 定时/批处理任务 | 显式创建 root span 或链接到触发事件 | 不要伪造来自 HTTP 请求的父上下文 |
不要只重写 Executor.execute:submit、invokeAll 等提交路径也会绕过它。TransmittableThreadLocal 也不是“加依赖即可传播 MDC”;它需要相应的上下文 adapter 或任务包装,且不能替代 OTel 在跨服务边界的标准传播。
Spring Boot 接入
Spring Boot 3.x 推荐以 Micrometer Tracing 作为应用层接入,底层可选择 OpenTelemetry 或 Brave bridge;导出至兼容后端后,框架会负责常见 HTTP 客户端、服务端和消息场景的上下文传播。实际能力取决于已启用的 instrumentation,应在集成测试中验证每条边界。
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
日志 encoder 应输出统一 Schema 中的 trace_id、span_id,并避免自动将全部 MDC 键无筛选地写出。Spring Boot 的具体配置与采样方式以官方 Tracing 文档为准。
SkyWalking Java Agent 仍适合作为低侵入的补充方案,但“无需修改一行代码”不等于零配置或覆盖所有库;应确认 agent 插件、异步调用、采样和日志关联字段,再决定是否采用。
手工方案的适用范围
手工 MDC/自定义 header 仅适用于无法接入 OTel 的遗留服务、短期兼容或非常受限的单体系统。此时至少做到:
- 入口生成或校验 ID,长度和字符集受限;不能让外部输入污染日志。
- HTTP、MQ、线程池均有成对的注入/提取与恢复逻辑。
- 统一采用
trace_id字段,不与traceId、tid等并存。 - 明确监控传播失败、丢失率和异常链路,不把 grep 当作唯一排障手段。
参考资料
- OpenTelemetry Trace API
- Spring Boot Tracing
- 日志体系总览(统一日志 Schema)
- 日志采集与告警(在日志平台按 trace 查询)
评论 (0)