0%

MDC日志跟踪

MDC 日志跟踪:多线程环境下的日志上下文管理

在复杂的分布式系统或多线程环境中,一条请求可能经过多个组件、线程甚至服务节点,传统日志往往难以串联整个调用链路。MDC(Mapped Diagnostic Context,映射诊断上下文)通过与线程绑定的上下文信息,为日志添加全局唯一标识(如traceId),实现跨线程、跨服务的日志追踪,是排查分布式问题的关键工具。

MDC 的核心原理

基本概念

MDC 是日志框架(Log4j、Logback、JUL)提供的线程级上下文存储机制,本质是一个与当前线程绑定的哈希表(ThreadLocal<Map<String, String>>,支持在日志中嵌入自定义键值对(如traceIduserId)。

工作机制

  • 线程绑定: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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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";

@Override
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;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// 请求处理完成后,清除MDC中的traceId(避免线程池复用导致的污染)
MDC.remove(TRACE_ID_KEY);
}
}
(2)注册拦截器(Spring Boot 示例)
1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 对所有请求生效
registry.addInterceptor(new TraceIdInterceptor()).addPathPatterns("/**");
}
}

2. 配置日志格式输出traceId

在日志配置文件(如 Logback 的logback.xml)中,通过%X{traceId}引用 MDC 中的traceId

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 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>

输出日志示例:

1
[INFO] 2024-05-20 10:30:00.123 [traceId=123e4567e89b12d3a45645678901234] com.example.UserController - 用户登录成功

3. 子线程中传递 MDC 上下文

MDC 默认基于ThreadLocal,子线程无法直接继承父线程的 MDC 上下文,需手动传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 的注意事项

  1. 线程池复用问题
    线程池中的线程会被复用,若线程结束时未清除 MDC(MDC.clear()),下次复用该线程时会携带旧的上下文信息,导致日志混乱。务必在finally块中清除 MDC
  2. 异步任务传递
    子线程或异步任务(如@Async)需手动传递 MDC 上下文(通过getCopyOfContextMap()setContextMap()),否则无法继承父线程的traceId
  3. 性能影响
    MDC 基于ThreadLocal,操作轻量,对性能影响可忽略,但避免存储大量键值对(仅保留必要的追踪信息,如traceIduserId)。
  4. 框架兼容性
    主流日志框架(Logback、Log4j2)均支持 MDC,SLF4J 提供统一 API,切换日志实现时无需修改 MDC 代码。

MDC 的扩展应用

除了traceId,MDC 还可用于记录其他上下文信息:

  • userId:记录当前操作的用户 ID,便于定位用户相关问题;
  • requestId:区分同一traceId下的不同请求(如批量操作);
  • serviceName:微服务名,便于跨服务日志筛选。

总结

MDC 通过与线程绑定的上下文机制,为日志添加全局追踪标识(如traceId),完美解决了多线程和分布式系统中的日志串联问题。核心步骤包括:

  1. 在请求入口(如拦截器)生成并设置traceId
  2. 配置日志格式输出traceId
  3. 跨线程 / 服务传递 MDC 上下文;
  4. 线程结束时清除 MDC,避免污染

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10