在复杂业务系统中,一个用户请求可能会贯穿多个服务、调用数十个方法、涉及多线程异步处理——排查问题时,日志被不同请求穿插得杂乱无章,想要找到某条请求的完整链路日志如同大海捞针。
TraceId 日志链路追踪正是为解决这个痛点而生:通过为每一次请求分配唯一的 TraceId,将该请求在全链路中的所有日志串联起来,让排查问题从“拼凑日志”变成“精准定位”。本文将基于 SpringBoot 实现从基础到进阶的 TraceId 链路追踪,覆盖同步请求、异步线程、定时任务、微服务调用等全场景,提供生产级可直接落地的方案。
一、核心原理:TraceId 如何打通全链路?
1. 核心组件与流程
TraceId 链路追踪的核心是“ThreadLocal + MDC + 拦截器/AOP”,流程如下:
- 请求入口拦截:通过拦截器(Web 请求)或 AOP(定时任务)为每次请求生成唯一 TraceId;
- 上下文存储:将 TraceId 存入 MDC(Mapped Diagnostic Context,日志诊断上下文),MDC 底层基于 ThreadLocal,确保线程隔离;
- 日志打印:通过日志框架(Logback/Log4j2)配置,自动在日志中打印 TraceId;
- 链路传递:同步调用时,ThreadLocal 自动传递 TraceId;异步调用/跨服务调用时,手动传递 TraceId;
- 资源清理:请求结束后,清除 MDC 中的 TraceId,避免内存泄漏。
2. MDC 核心 API 说明
MDC 是 SLF4J 提供的日志上下文工具,核心 API 简单易用:
| API 方法 | 作用 | 示例 |
|---|---|---|
MDC.put(String key, String value) |
向当前线程的 MDC 存入键值对 | MDC.put("traceId", UUID.randomUUID().toString()) |
MDC.get(String key) |
获取当前线程 MDC 中指定 key 的值 | String traceId = MDC.get("traceId") |
MDC.getCopyOfContextMap() |
复制当前线程的 MDC 上下文(用于异步传递) | Map<String, String> context = MDC.getCopyOfContextMap() |
MDC.setContextMap(Map) |
为当前线程设置 MDC 上下文 | MDC.setContextMap(context) |
MDC.clear() |
清除当前线程的 MDC 上下文 | MDC.clear() |
二、基础实现:Web 请求 TraceId 链路追踪
先实现最基础的 Web 请求链路追踪,让每个 HTTP 请求的所有同步日志都携带 TraceId。
1. 依赖准备(SpringBoot 2.x/3.x 通用)
无需额外依赖,SpringBoot 自带的 spring-boot-starter-web 和 spring-boot-starter-logging 已满足需求,若使用 Lombok 可简化日志声明:
<dependencies>
<!-- SpringBoot Web 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 日志依赖(默认集成 Logback) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Lombok 简化日志 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2. 日志配置:自动打印 TraceId
修改 Logback 配置文件(src/main/resources/logback-spring.xml),在日志格式中添加 %X{traceId}(MDC 中 TraceId 的 key):
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!-- 日志存储路径 -->
<property name="LOG_PATH" value="${user.home}/logs/trace-demo" />
<!-- 日志输出格式(核心:添加 [traceId:%X{traceId:-NONE}]) -->
<property name="LOG_PATTERN" value="[traceId:%X{traceId:-NONE}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n" />
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 按天滚动文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_PATH}/app-%d{yyyy-MM-dd}.log</FileNamePattern>
<MaxHistory>30</MaxHistory> <!-- 保留 30 天日志 -->
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>10MB</MaxFileSize> <!-- 单个日志文件最大 10MB -->
</triggeringPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 日志输出级别 -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>
%X{traceId:-NONE}表示:若 MDC 中存在 traceId 则打印,否则打印 NONE;-
配置文件路径需在
application.yml中指定(可选,SpringBoot 会自动扫描classpath:logback-spring.xml):logging: config: classpath:logback-spring.xml server: port: 8080
3. 自定义拦截器:生成并传递 TraceId
通过 SpringMVC 拦截器,在请求入口生成 TraceId 并存入 MDC,请求结束后清除:
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
/**
* Web 请求 TraceId 拦截器
*/
@Slf4j
public class TraceIdInterceptor implements HandlerInterceptor {
// TraceId 的 key(需与日志配置中的 %X{traceId} 一致)
public static final String TRACE_ID_KEY = "traceId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 1. 优先从请求头获取 TraceId(支持前端/网关传递,便于跨服务追踪)
String traceId = request.getHeader(TRACE_ID_KEY);
// 2. 若请求头未携带,生成唯一 TraceId(UUID 去除横线,缩短长度)
if (!StringUtils.hasText(traceId)) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
// 3. 存入 MDC
MDC.put(TRACE_ID_KEY, traceId);
// 4. 可选:将 TraceId 写入响应头,便于前端排查
response.setHeader(TRACE_ID_KEY, traceId);
log.info("请求入口:URI={}, Method={}, TraceId={}",
request.getRequestURI(), request.getMethod(), traceId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) {
// 5. 请求结束后清除 MDC,避免 ThreadLocal 内存泄漏(Tomcat 线程池复用)
log.info("请求结束:URI={}, TraceId={}", request.getRequestURI(), MDC.get(TRACE_ID_KEY));
MDC.clear();
}
}
4. 注册拦截器
通过 WebMvcConfigurer 注册拦截器,确保拦截所有 Web 请求:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/**
* SpringMVC 配置
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private TraceIdInterceptor traceIdInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(traceIdInterceptor)
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/error", "/health"); // 排除健康检查、错误页等无需追踪的路径
}
}
5. 测试:同步请求链路追踪
编写测试接口和服务,验证 TraceId 传递:
// 测试控制器
@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {
@Resource
private TestService testService;
@GetMapping("/sync")
public String syncTest(String name) {
log.info("控制器接收请求:name={}", name);
String result = testService.process(name);
log.info("控制器返回结果:result={}", result);
return result;
}
}
// 测试服务
@Service
@Slf4j
public class TestService {
public String process(String name) {
log.info("服务层处理:name={}", name);
String data = queryData(name);
log.info("服务层处理完成:data={}", data);
return "Hello, " + data;
}
private String queryData(String name) {
log.info("数据查询:name={}", name);
// 模拟数据库查询
return name + "_" + System.currentTimeMillis();
}
}
测试结果(日志片段):
[traceId:672e16b742f54119b28a6112bd21ccb0] 2024-11-11 15:30:00.123 [http-nio-8080-exec-1] INFO c.example.trace.controller.TestController - 请求入口:URI=/test/sync, Method=GET, TraceId=672e16b742f54119b28a6112bd21ccb0
[traceId:672e16b742f54119b28a6112bd21ccb0] 2024-11-11 15:30:00.124 [http-nio-8080-exec-1] INFO c.example.trace.controller.TestController - 控制器接收请求:name=zhangsan
[traceId:672e16b742f54119b28a6112bd21ccb0] 2024-11-11 15:30:00.125 [http-nio-8080-exec-1] INFO c.example.trace.service.TestService - 服务层处理:name=zhangsan
[traceId:672e16b742f54119b28a6112bd21ccb0] 2024-11-11 15:30:00.126 [http-nio-8080-exec-1] INFO c.example.trace.service.TestService - 数据查询:name=zhangsan
[traceId:672e16b742f54119b28a6112bd21ccb0] 2024-11-11 15:30:00.127 [http-nio-8080-exec-1] INFO c.example.trace.service.TestService - 服务层处理完成:data=zhangsan_1731329400126
[traceId:672e16b742f54119b28a6112bd21ccb0] 2024-11-11 15:30:00.128 [http-nio-8080-exec-1] INFO c.example.trace.controller.TestController - 控制器返回结果:result=Hello, zhangsan_1731329400126
[traceId:672e16b742f54119b28a6112bd21ccb0] 2024-11-11 15:30:00.129 [http-nio-8080-exec-1] INFO c.example.trace.interceptor.TraceIdInterceptor - 请求结束:URI=/test/sync, TraceId=672e16b742f54119b28a6112bd21ccb0
所有日志通过同一个 TraceId 串联,清晰可见请求的完整链路。
三、进阶场景 1:异步线程 TraceId 传递
同步请求的 TraceId 依赖 ThreadLocal 自动传递,但异步线程(线程池、@Async)会切换线程,导致 MDC 中的 TraceId 丢失——需手动将父线程的 MDC 上下文传递给子线程。
1. 核心工具类:封装线程任务,传递 MDC 上下文
import org.slf4j.MDC;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
/**
* MDC 线程上下文传递工具类
*/
public final class MdcThreadUtil {
private static final String TRACE_ID_KEY = TraceIdInterceptor.TRACE_ID_KEY;
/**
* 生成 TraceId
*/
public static String generateTraceId() {
return UUID.randomUUID().toString().replace("-", "");
}
/**
* 为线程池任务包装 MDC 上下文(Runnable 无返回值)
*/
public static Runnable wrap(Runnable task) {
// 复制父线程的 MDC 上下文
Map<String, String> parentContext = MDC.getCopyOfContextMap();
return () -> {
try {
// 子线程设置 MDC 上下文
if (parentContext != null) {
MDC.setContextMap(parentContext);
} else {
// 若父线程无上下文,生成默认 TraceId
MDC.put(TRACE_ID_KEY, generateTraceId());
}
task.run(); // 执行任务
} finally {
// 清除子线程 MDC,避免内存泄漏
MDC.clear();
}
};
}
/**
* 为线程池任务包装 MDC 上下文(Callable 有返回值)
*/
public static <T> Callable<T> wrap(Callable<T> task) {
Map<String, String> parentContext = MDC.getCopyOfContextMap();
return () -> {
try {
if (parentContext != null) {
MDC.setContextMap(parentContext);
} else {
MDC.put(TRACE_ID_KEY, generateTraceId());
}
return task.call();
} finally {
MDC.clear();
}
};
}
}
2. 自定义线程池:自动传递 MDC 上下文
重写 Spring 线程池,对提交的任务自动进行 MDC 包装:
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
/**
* 支持 MDC 上下文传递的线程池
*/
public class MdcThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
@Override
public void execute(Runnable task) {
// 包装任务,传递 MDC 上下文
super.execute(MdcThreadUtil.wrap(task));
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return super.submit(MdcThreadUtil.wrap(task));
}
@Override
public Future<?> submit(Runnable task) {
return super.submit(MdcThreadUtil.wrap(task));
}
}
3. 配置线程池 Bean
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import java.util.concurrent.Executor;
/**
* 异步线程池配置
*/
@EnableAsync
@Configuration
public class AsyncConfig {
@Bean("asyncExecutor")
public Executor asyncExecutor() {
MdcThreadPoolTaskExecutor executor = new MdcThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 核心线程数
executor.setMaxPoolSize(10); // 最大线程数
executor.setQueueCapacity(100); // 队列容量
executor.setKeepAliveSeconds(60); // 空闲线程存活时间
executor.setThreadNamePrefix("async-"); // 线程名前缀
executor.initialize();
return executor;
}
}
4. 测试:异步线程 TraceId 传递
@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {
@Resource
private TestService testService;
@GetMapping("/async")
public String asyncTest(String name) {
log.info("控制器接收请求:name={}", name);
// 调用异步方法
testService.asyncProcess(name);
log.info("控制器返回(异步任务已提交)");
return "Async task submitted";
}
}
@Service
@Slf4j
public class TestService {
@Async("asyncExecutor") // 指定自定义线程池
public void asyncProcess(String name) {
log.info("异步服务层处理:name={}", name);
try {
// 模拟耗时操作
Thread.sleep(2000);
} catch (InterruptedException e) {
log.error("异步处理异常", e);
Thread.currentThread().interrupt();
}
log.info("异步服务层处理完成:name={}", name);
}
}
测试结果(日志片段):
[traceId:89a3f2d17c4e43b88e9d3578a9123456] 2024-11-11 15:35:00.456 [http-nio-8080-exec-2] INFO c.example.trace.controller.TestController - 请求入口:URI=/test/async, Method=GET, TraceId=89a3f2d17c4e43b88e9d3578a9123456
[traceId:89a3f2d17c4e43b88e9d3578a9123456] 2024-11-11 15:35:00.457 [http-nio-8080-exec-2] INFO c.example.trace.controller.TestController - 控制器接收请求:name=lisi
[traceId:89a3f2d17c4e43b88e9d3578a9123456] 2024-11-11 15:35:00.458 [http-nio-8080-exec-2] INFO c.example.trace.controller.TestController - 控制器返回(异步任务已提交)
[traceId:89a3f2d17c4e43b88e9d3578a9123456] 2024-11-11 15:35:00.459 [async-1] INFO c.example.trace.service.TestService - 异步服务层处理:name=lisi
[traceId:89a3f2d17c4e43b88e9d3578a9123456] 2024-11-11 15:35:02.460 [async-1] INFO c.example.trace.service.TestService - 异步服务层处理完成:name=lisi
[traceId:89a3f2d17c4e43b88e9d3578a9123456] 2024-11-11 15:35:02.461 [http-nio-8080-exec-2] INFO c.example.trace.interceptor.TraceIdInterceptor - 请求结束:URI=/test/async, TraceId=89a3f2d17c4e43b88e9d3578a9123456
异步线程的日志与主线程的 TraceId 一致,链路完整贯通。
四、进阶场景 2:定时任务 TraceId 追踪
定时任务(@Scheduled)不经过 Web 拦截器,因此需要通过 AOP 为其生成 TraceId,确保定时任务的日志也能被追踪。
1. 定时任务线程池配置
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.util.concurrent.Executors;
/**
* 定时任务线程池配置
*/
@EnableScheduling
@Configuration
public class SchedulerConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
// 配置定时任务线程池(5个线程)
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
}
}
2. AOP 切面:为定时任务生成 TraceId
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.MDC;
import org.springframework.context.annotation.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 定时任务 TraceId AOP 切面
*/
@Aspect
@Configuration
public class SchedulerTraceAspect {
private static final Logger log = LoggerFactory.getLogger(SchedulerTraceAspect.class);
private static final String TRACE_ID_KEY = TraceIdInterceptor.TRACE_ID_KEY;
// 切点:所有标注 @Scheduled 的方法
@Pointcut("@annotation(org.springframework.scheduling.annotation.Scheduled)")
public void schedulerPointcut() {}
@Around("schedulerPointcut()")
public Object aroundScheduler(ProceedingJoinPoint joinPoint) throws Throwable {
String traceId = null;
try {
// 生成 TraceId
traceId = MdcThreadUtil.generateTraceId();
MDC.put(TRACE_ID_KEY, traceId);
log.info("定时任务开始:task={}, TraceId={}",
joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName(),
traceId);
// 执行定时任务
return joinPoint.proceed();
} finally {
// 清除 MDC
log.info("定时任务结束:task={}, TraceId={}",
joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName(),
traceId);
MDC.clear();
}
}
}
3. 测试:定时任务 TraceId
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* 测试定时任务
*/
@Service
public class TestScheduler {
private static final Logger log = LoggerFactory.getLogger(TestScheduler.class);
// 每1分钟执行一次
@Scheduled(cron = "0 0/1 * * * ?")
public void testTask() {
log.info("执行定时任务,当前时间:{}", new Date());
// 模拟业务逻辑
doBusiness();
}
private void doBusiness() {
log.info("定时任务业务处理中...");
}
}
测试结果(日志片段):
[traceId:2d4f7a9c3b6e418d9c7a1234567890ab] 2024-11-11 15:40:00.001 [pool-2-thread-1] INFO c.example.trace.aspect.SchedulerTraceAspect - 定时任务开始:task=com.example.trace.scheduler.TestScheduler.testTask, TraceId=2d4f7a9c3b6e418d9c7a1234567890ab
[traceId:2d4f7a9c3b6e418d9c7a1234567890ab] 2024-11-11 15:40:00.002 [pool-2-thread-1] INFO c.example.trace.scheduler.TestScheduler - 执行定时任务,当前时间:Mon Nov 11 15:40:00 CST 2024
[traceId:2d4f7a9c3b6e418d9c7a1234567890ab] 2024-11-11 15:40:00.003 [pool-2-thread-1] INFO c.example.trace.scheduler.TestScheduler - 定时任务业务处理中...
[traceId:2d4f7a9c3b6e418d9c7a1234567890ab] 2024-11-11 15:40:00.004 [pool-2-thread-1] INFO c.example.trace.aspect.SchedulerTraceAspect - 定时任务结束:task=com.example.trace.scheduler.TestScheduler.testTask, TraceId=2d4f7a9c3b6e418d9c7a1234567890ab
定时任务的所有日志通过独立的 TraceId 串联,便于排查定时任务相关问题。
五、高阶场景:微服务间 TraceId 传递
在微服务架构中,请求会跨多个服务(如服务 A → 服务 B → 服务 C),需要将 TraceId 通过 HTTP 请求头传递,实现全链路贯通。
1. 微服务调用工具:RestTemplate 传递 TraceId
通过拦截器为 RestTemplate 添加 TraceId 请求头:
import org.slf4j.MDC;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
/**
* RestTemplate 配置(微服务调用传递 TraceId)
*/
@Configuration
public class RestTemplateConfig {
private static final String TRACE_ID_KEY = TraceIdInterceptor.TRACE_ID_KEY;
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
// 添加拦截器,传递 TraceId
List<ClientHttpRequestInterceptor> interceptors = Collections.singletonList(
new ClientHttpRequestInterceptor() {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
// 从 MDC 获取 TraceId,添加到请求头
String traceId = MDC.get(TRACE_ID_KEY);
if (traceId != null) {
HttpHeaders headers = request.getHeaders();
headers.add(TRACE_ID_KEY, traceId);
}
return execution.execute(request, body);
}
}
);
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
}
2. 微服务接收端:复用 Web 拦截器
微服务 B/C 只需复用前面实现的 TraceIdInterceptor,即可从请求头中获取 TraceId 并存入 MDC——无需额外开发,实现跨服务 TraceId 传递。
3. 测试:微服务间 TraceId 贯通
-
服务 A 调用服务 B:
@RestController @Slf4j public class ServiceAController { @Resource private RestTemplate restTemplate; @GetMapping("/call-service-b") public String callServiceB() { log.info("服务 A 准备调用服务 B"); // 调用服务 B(TraceId 通过请求头传递) String result = restTemplate.getForObject("http://service-b/test/echo", String.class); log.info("服务 A 调用服务 B 完成,结果:{}", result); return result; } } -
服务 B 接收并响应:
@RestController @Slf4j public class ServiceBController { @GetMapping("/test/echo") public String echo() { log.info("服务 B 接收请求,TraceId={}", MDC.get(TraceIdInterceptor.TRACE_ID_KEY)); return "Hello from Service B"; } }
测试结果(日志片段):
-
服务 A 日志:
[traceId:4e7a9d3b6f8e429c8d7b1234567890cd] 2024-11-11 15:45:00.789 [http-nio-8080-exec-3] INFO c.example.trace.controller.ServiceAController - 服务 A 准备调用服务 B [traceId:4e7a9d3b6f8e429c8d7b1234567890cd] 2024-11-11 15:45:00.901 [http-nio-8080-exec-3] INFO c.example.trace.controller.ServiceAController - 服务 A 调用服务 B 完成,结果:Hello from Service B -
服务 B 日志:
[traceId:4e7a9d3b6f8e429c8d7b1234567890cd] 2024-11-11 15:45:00.850 [http-nio-8081-exec-1] INFO c.example.trace.controller.ServiceBController - 服务 B 接收请求,TraceId=4e7a9d3b6f8e429c8d7b1234567890cd跨服务的 TraceId 完全一致,实现全链路追踪。
六、生产环境优化与避坑指南
1. 内存泄漏防护
- 必须在请求结束/线程执行完成后调用
MDC.clear():Tomcat 线程池、自定义线程池会复用线程,未清除的 MDC 会导致 TraceId 串流(不同请求共用同一个 TraceId); - 异步线程的 MDC 清除必须放在
finally块中:即使任务执行异常,也能确保 MDC 被清除。
2. TraceId 生成优化
- 避免使用过长的 TraceId:UUID 去除横线后长度为 32 位,足够唯一且不占用过多日志空间;
- 支持前端传递 TraceId:便于前端排查问题(如用户反馈问题时,前端传递 TraceId 给后端,直接定位日志)。
3. 日志检索优化
- 日志文件按 TraceId 相关维度滚动:如按天滚动 + 按服务名区分文件,便于日志检索工具(ELK、SkyWalking)快速查询;
- 集成日志检索工具:将日志导入 ELK,通过 TraceId 快速筛选出全链路日志,排查问题效率翻倍。
4. 特殊场景处理
- 异常链路:全局异常处理器中需保留 TraceId,在异常日志中打印,便于定位问题根源;
- 第三方服务调用:调用第三方服务(如支付、短信)时,将 TraceId 作为请求参数或请求头传递,便于第三方反馈问题时关联日志;
- 消息队列:发送消息时,将 TraceId 存入消息头;消费消息时,从消息头中提取 TraceId 存入 MDC,打通“服务 → 消息队列 → 消费服务”的链路。
七、总结:TraceId 链路追踪的核心价值
TraceId 链路追踪看似简单,却能解决复杂系统排查问题的“痛点”——它将分散的日志串联成完整链路,让问题定位从“数小时”缩短到“分钟级”。本文实现的方案覆盖:
- 基础场景:Web 同步请求;
- 进阶场景:异步线程、定时任务;
- 高阶场景:微服务跨服务调用;
- 生产级保障:内存泄漏防护、日志检索优化。
在实际落地时,可根据业务复杂度灵活扩展(如添加 SpanId 支持更细粒度的链路追踪、集成 SkyWalking 等分布式追踪工具)。记住:日志链路追踪不是“银弹”,但它是排查问题的“必备工具”,能让你在复杂系统中快速找到问题根源,大幅提升运维效率。
除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接
文章评论