跳到䞻芁内容

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 层等场景。