Jaeger Trace MDC
1. 方案概述
1.1 背景与目标
在分布式系统中,跨服务的请求链路追踪是定位问题的关键手段。本方案旨在实现 Jaeger 分布式追踪与 SLF4J MDC(Mapped Diagnostic Context)的深度集成,使得:
- 日志自动携带 traceId/spanId,便于日志聚合查询
- 支持跨服务的 trace 上下文传递
- 在线程池等异步场景下保持 trace 上下文的准确性
1.2 核心价值
- 问题定位效率提升:通过 traceId 快速关联分布式系统中的所有相关日志
- 零侵入性:业务代码无需手动管理 MDC,自动注入和清理
- 线程安全:支持嵌套 Span 和线程池复用场景
2. 架构设计
2.1 整体架构图
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Controller │───>│ Service │───>│ DAO │ │
│ │ (HTTP Entry) │ │ (Business) │ │ (Database) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └────────────────────┴────────────────────┘ │
│ │ │
├──────────────────────────────┼────────────────────────────────┤
│ Tracing Layer ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ CustomMDCScopeManager (核心组件) │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ ThreadLocal<CustomMDCScope> │ │ │
│ │ │ - 管理 Scope 生命周期 │ │ │
│ │ │ - 维护 Span 栈 │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ CustomMDCScope (Scope 实现) │ │ │
│ │ │ - MDC 快照与恢复 │ │ │
│ │ │ - 支持嵌套 Span │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────┘ │
│ │ │
├──────────────────────────────┼────────────────────────────────┤
│ Logging Layer ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ SLF4J MDC │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ traceId: 04bf92f3577b34da... │ │ │
│ │ │ spanId: 36bd32b7a5712a1a │ │ │
│ │ │ parentId: 00f067aa0ba902b7 │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────┘ │
│ │ │
└──────────────────────────────┼────────────────────────────────┘
▼
┌────────────────────┐
│ Jaeger Collector │
│ (Trace Storage) │
└────────────────────┘
2.2 核心组件说明
2.2.1 CustomMDCScopeManager
职责:实现 OpenTracing 的 ScopeManager 接口,管理 Span 的激活与传播
关键实现:
private final ThreadLocal<CustomMDCScope> tlsScope = new ThreadLocal<>();
@Override
public Scope activate(Span span) {
return new CustomMDCScope(span);
}
@Override
public Span activeSpan() {
CustomMDCScope scope = tlsScope.get();
return scope == null ? null : scope.wrapped;
}
设计要点:
- 使用
ThreadLocal保证线程隔离 - 支持 Scope 的嵌套(通过链表结构维护 previous)
- 实现懒激活:仅在调用
activate()时才注入 MDC
2.2.2 CustomMDCScope
职责:实现 OpenTracing 的 Scope 接口,管理单个 Span 的生命周期
核心机制:快照-注入-恢复(Snapshot-Inject-Restore)
CustomMDCScope(Span span) {
// 1. 保存当前 MDC 快照
this.previousTraceId = MDC.get("traceId");
this.previousSpanId = MDC.get("spanId");
// 2. 建立链表关系(支持嵌套)
this.previous = CustomMDCScopeManager.this.tlsScope.get();
CustomMDCScopeManager.this.tlsScope.set(this);
// 3. 注入新的 trace 上下文
MDC.put("traceId", span.context().toTraceId());
MDC.put("spanId", span.context().toSpanId());
}
@Override
public void close() {
// 4. 恢复到上一层 Scope
CustomMDCScopeManager.this.tlsScope.set(previous);
// 5. 恢复 MDC 到快照状态
restoreMDC("traceId", previousTraceId);
restoreMDC("spanId", previousSpanId);
}
3. 关键流程设计
3.1 接收远程 Trace 上下文的流程
┌─────────────┐
│ HTTP Request│
│ Headers │
│ uber-trace-id: 4bf92f3577b...│
└──────┬──────┘
│
▼
┌──────────────────────────────────┐
│ 1. 解析 HTTP Header │
│ - 提取 traceId (128-bit) │
│ - 提取 parentSpanId (64-bit) │
│ - 提取 flags (采样标记) │
└──────┬───────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ 2. 构造 JaegerSpanContext │
│ long[] parts = splitTraceId() │
│ new JaegerSpanContext( │
│ traceIdHigh, │
│ traceIdLow, │
│ parentSpanId, │
│ parentOfParentId, │
│ flags │
│ ) │
└──────┬───────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ 3. 创建子 Span │
│ tracer.buildSpan("child-span")│
│ .asChildOf(parentContext)│
│ .withTag(...) │
│ .start() │
└──────┬───────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ 4. 激活 Span 到当前线程 │
│ try (Scope scope = │
│ tracer.scopeManager() │
│ .activate(childSpan)) │
│ { │
│ // MDC 自动注入 │
│ // 业务逻辑执行 │
│ } // MDC 自动清理 │
└──────────────────────────────────┘
3.2 嵌套 Span 的 MDC 管理流程
时间线 ────────────────────────────────────────────>
ThreadLocal Stack:
┌─────────────────────────────────────────────────┐
│ null │
└─────────────────────────────────────────────────┘
activate(spanA)
┌─────────────────────────────────────────────────┐
│ ScopeA: {traceId: xxx, spanId: A, previous: null}│
└─────────────────────────────────────────────────┘
MDC: {traceId: xxx, spanId: A}
activate(spanB) // 嵌套调用
┌─────────────────────────────────────────────────┐
│ ScopeB: {traceId: xxx, spanId: B, previous: ScopeA}│
└─────────────────────────────────────────────────┘
MDC: {traceId: xxx, spanId: B} // spanId 更新
// 业务代码执行
log.info("Processing...") // 日志携带 spanId=B
close(ScopeB)
┌─────────────────────────────────────────────────┐
│ ScopeA: {traceId: xxx, spanId: A, previous: null}│
└─────────────────────────────────────────────────┘
MDC: {traceId: xxx, spanId: A} // 恢复到 spanId=A
close(ScopeA)
┌─────────────────────────────────────────────────┐
│ null │
└─────────────────────────────────────────────────┘
MDC: {} // 完全清空
3.3 Trace ID 分割算法
Jaeger 支持 128-bit 的 traceId,但 Java long 仅为 64-bit,因此需要分割:
/**
* 将十六进制 traceId 字符串分割为高64位和低64位
*
* 示例:
* 输入: "04bf92f3577b34da63ce929d0e0e4736" (32个十六进制字符 = 128 bit)
* 输出: [0x04bf92f3577b34da, 0x63ce929d0e0e4736]
*/
public static long[] splitTraceId(String traceIdHex) {
if (traceIdHex.length() <= 16) {
// 仅有低64位,高位为0
return new long[]{0L, parseLong(traceIdHex)};
} else {
// 分割为高64位和低64位
String highHex = traceIdHex.substring(0, traceIdHex.length() - 16);
String lowHex = traceIdHex.substring(traceIdHex.length() - 16);
return new long[]{parseLong(highHex), parseLong(lowHex)};
}
}
4. 配置与集成
4.1 Tracer 初始化配置
static Tracer tracer = new Configuration("order-service")
.withSampler(
new Configuration.SamplerConfiguration()
.withType("const")
.withParam(1) // 采样率 100%
)
.withReporter(
new Configuration.ReporterConfiguration()
.withLogSpans(true) // 开发环境启用日志输出
.withSender(
new Configuration.SenderConfiguration()
.withAgentHost("localhost")
.withAgentPort(6831)
)
)
.getTracerBuilder()
.withScopeManager(new CustomMDCScopeManager()) // 关键:注入自定义管理器
.build();
4.2 Logback 配置
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [traceId=%X{traceId} spanId=%X{spanId}] - %msg%n</pattern>
</encoder>
</appender>
<appender name="JSON" class="ch.qos.logback.core.FileAppender">
<file>logs/app.json</file>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>traceId</includeMdcKeyName>
<includeMdcKeyName>spanId</includeMdcKeyName>
<includeMdcKeyName>parentId</includeMdcKeyName>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="JSON" />
</root>
</configuration>
4.3 Spring Boot 集成(可选)
@Configuration
public class TracingConfig {
@Bean
public Tracer jaegerTracer() {
return new Configuration(
env.getProperty("spring.application.name", "unknown-service")
)
.withSampler(samplerConfig())
.withReporter(reporterConfig())
.getTracerBuilder()
.withScopeManager(new CustomMDCScopeManager())
.build();
}
@Bean
public TracingFilter tracingFilter(Tracer tracer) {
return new TracingFilter(tracer);
}
}
5. 使用场景与最佳实践
5.1 接收上游 Trace 上下文
@RestController
public class OrderController {
@GetMapping("/order/{id}")
public Order getOrder(
@PathVariable String id,
@RequestHeader(value = "uber-trace-id", required = false) String uberTraceId
) {
JaegerSpanContext parentContext = parseUberTraceId(uberTraceId);
Span span = tracer.buildSpan("get-order")
.asChildOf(parentContext) // 关键:链接远程父 Span
.withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER)
.withTag("order.id", id)
.start();
try (Scope scope = tracer.scopeManager().activate(span)) {
log.info("Processing order request"); // 自动携带 traceId/spanId
return orderService.getById(id);
} finally {
span.finish();
}
}
}
5.2 向下游传递 Trace 上下文
public class PaymentClient {
public void processPayment(String orderId) {
Span span = tracer.buildSpan("call-payment-service")
.withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT)
.start();
try (Scope scope = tracer.scopeManager().activate(span)) {
HttpHeaders headers = new HttpHeaders();
// 注入 trace 上下文到 HTTP Header
tracer.inject(
span.context(),
Format.Builtin.HTTP_HEADERS,
new HttpHeadersCarrier(headers)
);
restTemplate.exchange(
"http://payment-service/pay",
HttpMethod.POST,
new HttpEntity<>(paymentRequest, headers),
PaymentResponse.class
);
} finally {
span.finish();
}
}
}
5.3 异步场景处理
@Service
public class AsyncOrderService {
@Autowired
private Tracer tracer;
@Autowired
private ExecutorService executorService;
public void processOrderAsync(String orderId) {
Span parentSpan = tracer.activeSpan(); // 获取当前 Span
executorService.submit(() -> {
// 在新线程中重新激活父 Span
Span asyncSpan = tracer.buildSpan("async-process")
.asChildOf(parentSpan)
.start();
try (Scope scope = tracer.scopeManager().activate(asyncSpan)) {
log.info("Async processing order"); // MDC 正确注入
// 业务逻辑
} finally {
asyncSpan.finish();
}
});
}
}
6. 关键设计决策
6.1 为什么不直接在业务代码中操作 MDC?
问题:手动管理容易遗漏清理,导致线程池复用时 traceId 污染
解决方案:通过 ScopeManager 的生命周期管理,在 activate() 时注入,在 close() 时自动清理
6.2 为什么需要保存 MDC 快照?
场景:嵌套 Span 调用时,需要在子 Span 结束后恢复父 Span 的 MDC
实现:
// 进入子 Span 时
this.previousTraceId = MDC.get("traceId"); // 保存快照
MDC.put("traceId", childSpan.context().toTraceId()); // 覆盖
// 退出子 Span 时
restoreMDC("traceId", previousTraceId); // 恢复快照
6.3 为什么使用 ThreadLocal 而不是 InheritableThreadLocal?
考量:
ThreadLocal:严格线程隔离,适合同步场景InheritableThreadLocal:子线程继承父线程值,但在线程池复用时容易出现上下文泄漏
推荐:异步场景显式传递 Span,而非依赖自动继承
7. 监控与调试
7.1 日志输出示例
21:45:32.123 [http-nio-8080-exec-1] INFO c.w.OrderService [traceId=04bf92f3577b34da63ce929d0e0e4736 spanId=36bd32b7a5712a1a] - Processing order ORD-001
21:45:32.234 [http-nio-8080-exec-1] INFO c.w.PaymentClient [traceId=04bf92f3577b34da63ce929d0e0e4736 spanId=7f3a28b9c4d5e6a1] - Calling payment service
21:45:32.456 [http-nio-8080-exec-1] INFO c.w.OrderService [traceId=04bf92f3577b34da63ce929d0e0e4736 spanId=36bd32b7a5712a1a] - Order processed successfully
7.2 Jaeger UI 查询
在 Jaeger UI 中可以:
- 通过 traceId
04bf92f3577b34da63ce929d0e0e4736查看完整调用链 - 查看每个 Span 的 Tags(如
db.statement,http.status_code) - 查看 Span 之间的父子关系和耗时分布
7.3 常见问题排查
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 日志中 traceId 为空 | 未激活 Span 或 ScopeManager 未正确配置 | 检查 activate() 调用和 Tracer 构建 |
| traceId 在不同请求间串台 | MDC 未正确清理(线程池复用) | 确保 finally 块中调用 MDC.remove() |
| 子 Span 未链接到父 Span | asChildOf() 参数错误 | 验证 parentSpanId 是否正确解析 |
| Jaeger 中看不到 Span | 采样率设置为0或 Reporter 配置错误 | 检查 withParam(1) 和网络连通性 |
8. 性能考量
8.1 性能影响分析
| 操作 | 耗时 | 影响 |
|---|---|---|
| MDC.put() | < 1μs | 可忽略 |
| ThreadLocal.get() | < 1μs | 可忽略 |
| Span.start() | ~10μs | 低 |
| Span.finish() + 上报 | ~100μs | 异步上报,对主流程影响小 |
8.2 优化建议
- 合理控制采样率:生产环境可设置为 0.1(10%)以降低存储成本
- 批量上报:Reporter 配置
withFlushInterval(1000)批量发送 Span - 避免过度 Span:不要为每个数据库查询都创建 Span,控制粒度
9. 扩展方向
9.1 支持响应式编程(Reactor/WebFlux)
public class ReactorScopeManager implements ScopeManager {
@Override
public Scope activate(Span span) {
return new ReactorScope(span);
}
static class ReactorScope implements Scope {
ReactorScope(Span span) {
// 使用 Reactor Context 而非 ThreadLocal
Context.of("span", span);
}
}
}
9.2 集成 Spring Cloud Sleuth
Spring Cloud Sleuth 提供了开箱即用的分布式追踪,可替代本方案:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
9.3 支持 OpenTelemetry
OpenTelemetry 是新一代可观测性标准,建议未来迁移:
OpenTelemetry openTelemetry = AutoConfiguredOpenTelemetrySdk
.initialize()
.getOpenTelemetrySdk();
10. 总结
本方案通过自定义 ScopeManager 实现了 Jaeger Trace 与 SLF4J MDC 的无缝集成,具备以下特点:
✅ 自动化:Scope 生命周期管理,无需手动操作 MDC
✅ 线程安全:ThreadLocal 隔离 + 快照恢复机制
✅ 嵌套支持:链表结构维护多层 Span 关系
✅ 生产可用:已考虑线程池复用、异步场景等边界情况
该方案适用于需要精细控制 trace 上下文传播的微服务架构,特别是需要接收上游 trace 信息的服务网关、BFF 层等场景。