MDC日志跟踪
MDC 日志跟踪:多线程环境下的日志上下文管理
在复杂的分布式系统或多线程环境中,一条请求可能经过多个组件、线程甚至服务节点,传统日志往往难以串联整个调用链路。MDC(Mapped Diagnostic Context,映射诊断上下文)通过与线程绑定的上下文信息,为日志添加全局唯一标识(如traceId),实现跨线程、跨服务的日志追踪,是排查分布式问题的关键工具。
MDC 的核心原理
基本概念
MDC 是日志框架(Log4j、Logback、JUL)提供的线程级上下文存储机制,本质是一个与当前线程绑定的哈希表(ThreadLocal<Map<String, String>>),支持在日志中嵌入自定义键值对(如traceId、userId)。
工作机制
- 线程绑定:MDC 通过
ThreadLocal将键值对与当前线程绑定,确保同一线程内的所有日志都能访问这些上下文信息; - 日志输出:在日志格式中通过
%X{key}占位符引用 MDC 中的值(如%X{traceId}输出追踪 ID); - 自动清理:线程结束时需手动清除 MDC 内容,避免线程复用(如线程池)导致的上下文污染。
MDC 的核心 API
MDC 的 API 简单直观,主要包含以下方法(以 SLF4J 为例,不同框架方法一致):
| 方法 | 说明 |
|---|---|
MDC.put(String key, String value) |
向当前线程的 MDC 中添加键值对 |
MDC.get(String key) |
获取当前线程 MDC 中指定键的值 |
MDC.remove(String key) |
移除当前线程 MDC 中指定键的值 |
MDC.clear() |
清空当前线程 MDC 中的所有键值对 |
MDC.getCopyOfContextMap() |
复制当前线程的 MDC 上下文(用于传递给子线程) |
MDC.setContextMap(Map<String, String>) |
为当前线程设置 MDC 上下文(子线程接收父线程上下文时使用) |
MDC 实战:分布式追踪中的traceId实现
在分布式系统中,traceId是串联整个调用链路的唯一标识,通过 MDC 可轻松实现全链路日志追踪。
1. 生成与传递traceId
(1)拦截器中初始化traceId
在 Web 应用中,通过拦截器(如 Spring 的HandlerInterceptor)为每个请求生成traceId并放入 MDC:
import org.slf4j.MDC;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
public class TraceIdInterceptor implements HandlerInterceptor {
// 定义traceId的键名
public static final String TRACE_ID_KEY = "traceId";
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 从请求头获取traceId(若有上游服务传递,则复用)
String traceId = request.getHeader(TRACE_ID_KEY);
// 若没有,则生成新的traceId(UUID确保唯一性)
if (traceId == null || traceId.isEmpty()) {
traceId = UUID.randomUUID().toString().replaceAll("-", "");
}
// 将traceId放入MDC
MDC.put(TRACE_ID_KEY, traceId);
// 将traceId放入响应头,便于下游服务获取
response.setHeader(TRACE_ID_KEY, traceId);
return true;
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// 请求处理完成后,清除MDC中的traceId(避免线程池复用导致的污染)
MDC.remove(TRACE_ID_KEY);
}
}
(2)注册拦截器(Spring Boot 示例)
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
public class WebConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
// 对所有请求生效
registry.addInterceptor(new TraceIdInterceptor()).addPathPatterns("/**");
}
}
2. 配置日志格式输出traceId
在日志配置文件(如 Logback 的logback.xml)中,通过%X{traceId}引用 MDC 中的traceId:
<!-- Logback配置示例 -->
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 日志格式:包含级别、时间、traceId、类名、消息 -->
<pattern>[%level] %d{yyyy-MM-dd HH:mm:ss.SSS} [traceId=%X{traceId}] %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="info">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
输出日志示例:
[INFO] 2024-05-20 10:30:00.123 [traceId=123e4567e89b12d3a45645678901234] com.example.UserController - 用户登录成功
3. 子线程中传递 MDC 上下文
MDC 默认基于ThreadLocal,子线程无法直接继承父线程的 MDC 上下文,需手动传递:
import org.slf4j.MDC;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MdcThreadDemo {
public static void main(String[] args) {
// 父线程设置MDC
MDC.put("traceId", "parent-123");
// 获取父线程的MDC上下文
Map<String, String> parentMdc = MDC.getCopyOfContextMap();
ExecutorService executor = Executors.newFixedThreadPool(1);
executor.submit(() -> {
try {
// 子线程设置父线程的MDC上下文
if (parentMdc != null) {
MDC.setContextMap(parentMdc);
}
// 子线程日志会包含父线程的traceId
System.out.println("子线程日志:" + MDC.get("traceId"));
} finally {
// 子线程结束后清除MDC
MDC.clear();
}
});
executor.shutdown();
// 父线程清除MDC
MDC.clear();
}
}
4. 跨服务传递traceId
在分布式系统中,通过 HTTP 请求头传递traceId,确保跨服务调用时链路连贯:
(1)服务 A 调用服务 B 时,在请求头中添加traceId
import org.slf4j.MDC;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.web.client.RestTemplate;
public class ServiceAClient {
private RestTemplate restTemplate = new RestTemplate();
public void callServiceB() {
HttpHeaders headers = new HttpHeaders();
// 将当前MDC中的traceId放入请求头
headers.add("traceId", MDC.get("traceId"));
HttpEntity<String> request = new HttpEntity<>(headers);
// 调用服务B
restTemplate.postForObject("http://service-b/api", request, String.class);
}
}
(2)服务 B 通过拦截器获取请求头中的traceId(同步骤 1 的TraceIdInterceptor)
MDC 的注意事项
- 线程池复用问题:
线程池中的线程会被复用,若线程结束时未清除 MDC(MDC.clear()),下次复用该线程时会携带旧的上下文信息,导致日志混乱。务必在finally块中清除 MDC。 - 异步任务传递:
子线程或异步任务(如@Async)需手动传递 MDC 上下文(通过getCopyOfContextMap()和setContextMap()),否则无法继承父线程的traceId。 - 性能影响:
MDC 基于ThreadLocal,操作轻量,对性能影响可忽略,但避免存储大量键值对(仅保留必要的追踪信息,如traceId、userId)。 - 框架兼容性:
主流日志框架(Logback、Log4j2)均支持 MDC,SLF4J 提供统一 API,切换日志实现时无需修改 MDC 代码。
MDC 的扩展应用
除了traceId,MDC 还可用于记录其他上下文信息:
userId:记录当前操作的用户 ID,便于定位用户相关问题;requestId:区分同一traceId下的不同请求(如批量操作);serviceName:微服务名,便于跨服务日志筛选。
总结
MDC 通过与线程绑定的上下文机制,为日志添加全局追踪标识(如traceId),完美解决了多线程和分布式系统中的日志串联问题。核心步骤包括:
- 在请求入口(如拦截器)生成并设置
traceId; - 配置日志格式输出
traceId; - 跨线程 / 服务传递 MDC 上下文;
- 线程结束时清除 MDC,避免污染