跳到主要内容

Jaeger Trace MDC

Simple Demo Code

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 中可以:

  1. 通过 traceId 04bf92f3577b34da63ce929d0e0e4736 查看完整调用链
  2. 查看每个 Span 的 Tags(如 db.statement, http.status_code
  3. 查看 Span 之间的父子关系和耗时分布

7.3 常见问题排查

问题现象可能原因排查方法
日志中 traceId 为空未激活 Span 或 ScopeManager 未正确配置检查 activate() 调用和 Tracer 构建
traceId 在不同请求间串台MDC 未正确清理(线程池复用)确保 finally 块中调用 MDC.remove()
子 Span 未链接到父 SpanasChildOf() 参数错误验证 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 优化建议

  1. 合理控制采样率:生产环境可设置为 0.1(10%)以降低存储成本
  2. 批量上报:Reporter 配置 withFlushInterval(1000) 批量发送 Span
  3. 避免过度 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 层等场景。