本文目录导读:

关于链路追踪中 TraceId 传递到 MDC(Mapped Diagnostic Context,映射诊断上下文),这是实现分布式日志串联的关键步骤,下面我会从核心原理、主流框架实现及最佳实践三个方面来详细说明。
核心原理:为什么要把 TraceId 塞进 MDC?
MDC 是 SLF4J/Logback/Log4j 等日志框架提供的一个线程上下文的容器,它是一个 ThreadLocal 实现的 Map。
- 目的:将 TraceId 存入 MDC 后,在日志输出模式(
PatternLayout)中通过%X{traceId}占位符,自动 在每个日志行中打印 TraceId,无需在每个方法中显式传递。 - 挑战:跨线程(如线程池、异步调用)和跨网络(RPC、HTTP)时,MDC 无法自动传递,需要手动处理。
主流框架中的实现方案
基于 Spring Boot 3.x + Micrometer Tracing(推荐)
这是目前最现代化的方案,Micrometer Tracing 已经内置了 MDC 的支持,并且自动与 OpenTelemetry 集成。
依赖(Maven):
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-integration-test</artifactId>
<scope>test</scope>
</dependency>
配置(application.yml):
# 开启 MDC 自动注入
logging.pattern.level: "%5p [%X{traceId:-},%X{spanId:-}]"
# 或者自定义格式
# logging.pattern.console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{traceId}] %msg%n"
核心原理: TracingFilter 或 ObservationAwareSpanCustomizer 会自动将当前 Trace 的 traceId 和 spanId 放入 MDC。
优点: 零侵入,同步请求全覆盖;异步任务(如 @Async)需要通过 ExecutorService 的 TraceableExecutorService 包装。
基于 Dubbo 微服务(使用 RpcContext)
如果使用 Dubbo 作为 RPC 框架,可以在 Filter 中手动操作 MDC。
Provider 端(服务提供方):
import org.apache.dubbo.rpc.*;
import org.slf4j.MDC;
public class TraceIdProviderFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// 从 RpcContext 中获取上游传递的 TraceId
String traceId = RpcContext.getContext().getAttachment("traceId");
if (traceId == null) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
MDC.put("traceId", traceId);
try {
return invoker.invoke(invocation);
} finally {
MDC.remove("traceId"); // 必须清理,防止内存泄漏
}
}
}
Consumer 端(服务消费方):
public class TraceIdConsumerFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// 将本地的 MDC 传入 RpcContext
String traceId = MDC.get("traceId");
if (traceId != null) {
RpcContext.getContext().setAttachment("traceId", traceId);
}
return invoker.invoke(invocation);
}
}
基于 HTTP RestTemplate / Feign 调用
- RestTemplate: 通过
ClientHttpRequestInterceptor添加 Header。 - Feign: 通过
RequestInterceptor添加 Header。
@Component
public class FeignTraceInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String traceId = MDC.get("traceId");
if (traceId != null) {
template.header("X-Trace-Id", traceId);
}
}
}
Web 端接收 Header 侧(如网关或 Spring MVC Filter):
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TraceIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String traceId = request.getHeader("X-Trace-Id");
if (traceId == null || traceId.isEmpty()) {
traceId = UUID.randomUUID().toString();
}
MDC.put("traceId", traceId);
try {
filterChain.doFilter(request, response);
} finally {
MDC.clear(); // 清理,避免线程池复用导致污染
}
}
}
MDC 跨线程传递(核心难点)
MDC 默认基于 ThreadLocal,子线程或线程池任务无法直接获取父线程的 MDC。
方案 1:使用 MDC.getCopyOfContextMap()
// 提交任务前获取父线程 MDC 上下文
Map<String, String> contextMap = MDC.getCopyOfContextMap();
executorService.submit(() -> {
// 在子线程中设置 MDC
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
try {
// 执行业务逻辑
doWork();
} finally {
MDC.clear(); // 清理子线程 MDC
}
});
方案 2:使用 Hystrix 的 HystrixConcurrencyStrategy 或自定义线程池包装
对于 Spring 的 @Async,可以自定义 AsyncConfigurer 或使用 ThreadPoolTaskExecutor 的 TaskDecorator:
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
// 核心:装饰任务,自动传递 MDC
executor.setTaskDecorator(runnable -> {
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
runnable.run();
} finally {
MDC.clear();
}
};
});
executor.initialize();
return executor;
}
方案 3:使用 TransmittableThreadLocal(TTL,阿里巴巴开源)
TTL 可以自动解决线程池复用导致的问题,无需每次都手动拷贝。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.5</version>
</dependency>
用法:在创建线程池时包装一下 TtlRunnable 即可。
最佳实践总结
| 场景 | 推荐方案 |
|---|---|
| 新项目 Spring Boot 3.x | 使用 Micrometer Tracing,配置 logging.pattern.level,原生支持 MDC。 |
| 传统 Spring Boot + Sleuth | 使用 Spring Cloud Sleuth(已进入维护期),它会自动处理。 |
| Dubbo 微服务 | 使用自定义 RpcContext Filter,上下游通过 attachment 传递。 |
| 异步/线程池 | 使用 TaskDecorator 或 TTL 包裹任务。 |
| 手动配置 | 使用 Filter/Interceptor 在请求入口处设置和清理 MDC。 |
关键注意事项(避坑指南)
- 必须清理 MDC:无论是 Filter 还是线程任务,
finally块中必须调用MDC.clear()或MDC.remove("traceId"),否则线程池复用会导致日志串号。 - 区分 TraceId 与 SpanId:
traceId:全局唯一的流水号,整个调用链不变。spanId:每个服务或步骤的 ID,会变化。- MDC 中建议都放,配合 UI(如 Zipkin、SkyWalking)使用。
- 标准 Header 名称:建议使用 W3C 标准
traceparent(格式:00-traceId-spanId-01)或X-B3-TraceId(Brave 标准),便于与 OpenTelemetry、Zipkin 集成。 - 性能影响:MDC 操作(
put/remove)是轻量级的,但如果日志量极大(如数百万/秒),频繁的put和clear也会有一定开销,可考虑在 Filter 层面只对traceId操作,不操作整个contextMap。
如果涉及具体的框架集成(如 gRPC、RocketMQ 消息队列、@KafkaListener),需根据其拦截器或监听器机制做类似的处理。