网关 + MDC 过滤器方案通过在网关层统一生成 TraceId,再通过过滤器在各服务间透传,实现全链路日志追踪。以下是具体实现步骤,包含网关生成、服务接收、跨线程传递等关键细节:
一、网关层:生成并传递 TraceId(以 Spring Cloud Gateway 为例)
作用:所有请求进入系统的第一个入口生成 TraceId,确保全链路唯一。
1. 添加依赖(若未引入)
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2. 编写全局过滤器
java
import org.slf4j.MDC;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.UUID;
@Configuration
public class TraceIdGatewayConfig {
@Bean
public GlobalFilter traceIdFilter() {
return (exchange, chain) -> {
// 1. 生成 TraceId(去掉 UUID 中的横线,更简洁)
String traceId = UUID.randomUUID().toString().replace("-", "");
// 2. 放入 MDC,供网关自身日志使用
MDC.put("traceId", traceId);
// 3. 写入请求头,传递给下游服务
ServerWebExchange modifiedExchange = exchange.mutate()
.request(exchange.getRequest()
.mutate()
.header("X-Trace-Id", traceId) // 约定 header 键为 X-Trace-Id
.build())
.build();
// 4. 执行后续过滤链,完成后清除 MDC(避免 Netty 线程复用导致的污染)
return chain.filter(modifiedExchange)
.doFinally(signalType -> MDC.clear());
};
}
// 设置过滤器优先级(最高,确保第一个执行)
@Bean
public Ordered traceIdFilterOrder() {
return () -> Ordered.HIGHEST_PRECEDENCE;
}
}
二、下游服务:接收并透传 TraceId(所有业务服务通用)
作用:从请求头获取 TraceId,放入当前线程的 MDC,确保日志能打印;同时在调用其他服务时传递下去。
1. 编写 Servlet 过滤器(Spring Boot Web 服务)
java
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
@Component
public class TraceIdServletFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 1. 从请求头获取 TraceId,若没有则生成(应对直接访问服务的场景)
String traceId = httpRequest.getHeader("X-Trace-Id");
if (traceId == null || traceId.isEmpty()) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
// 2. 放入 MDC,供当前服务日志使用
MDC.put("traceId", traceId);
try {
// 3. 执行后续逻辑
chain.doFilter(request, response);
} finally {
// 4. 清除 MDC,避免 Tomcat 线程池复用导致的脏数据
MDC.clear();
}
}
}
2. 配置日志模板(logback-spring.xml)
在日志格式中加入 [%X{traceId}]
,示例:
xml
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId}] %logger{50} - %msg%n</pattern>
</encoder>
</appender>
此时服务日志会自动带上 TraceId,形如:
2025-07-31 15:00:00.123 [http-nio-8080-exec-1] INFO [a1b2c3d4e5f67890] com.example.Service - 处理订单逻辑
三、跨服务调用:确保 TraceId 透传(关键!)
当服务 A 调用服务 B 时,需手动将当前 MDC 中的 TraceId 放入请求头,否则链路会中断。
1. RestTemplate 调用场景
java
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.web.client.RestTemplate;
import java.io.IOException;
import java.util.Collections;
// 配置 RestTemplate 拦截器,自动添加 TraceId 头
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(Collections.singletonList(new ClientHttpRequestInterceptor() {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
// 从 MDC 获取 TraceId,添加到请求头
String traceId = MDC.get("traceId");
if (traceId != null) {
request.getHeaders().add("X-Trace-Id", traceId);
}
return execution.execute(request, body);
}
}));
return restTemplate;
}
}
2. OpenFeign 调用场景
java
import feign.RequestInterceptor;
import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
@Bean
public RequestInterceptor traceIdInterceptor() {
return template -> {
// 从 MDC 获取 TraceId,添加到 Feign 请求头
String traceId = MDC.get("traceId");
if (traceId != null) {
template.header("X-Trace-Id", traceId);
}
};
}
}
四、跨线程场景:解决异步任务 TraceId 丢失
当使用线程池、CompletableFuture 等异步操作时,子线程默认无法继承父线程的 MDC 数据,需手动传递:
1. 线程池包装类(推荐)
java
import org.slf4j.MDC;
import java.util.Map;
import java.util.concurrent.*;
public class TraceIdThreadPoolExecutor extends ThreadPoolExecutor {
// 构造方法复用父类
public TraceIdThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void execute(Runnable command) {
// 获取父线程 MDC 上下文
Map<String, String> context = MDC.getCopyOfContextMap();
super.execute(() -> {
try {
// 子线程设置 MDC 上下文
if (context != null) {
MDC.setContextMap(context);
}
command.run();
} finally {
// 清除子线程 MDC
MDC.clear();
}
});
}
}
2. 使用方式
java
// 用包装类创建线程池,替代原生 ThreadPoolExecutor
ExecutorService executor = new TraceIdThreadPoolExecutor(
5, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>()
);
// 提交任务时,子线程日志会自动带上 TraceId
executor.submit(() -> {
log.info("异步处理任务..."); // 日志中包含父线程的 TraceId
});
五、验证与效果
- 发起请求:通过网关访问任意接口(如
http://网关地址/order/create
)。 - 查看日志:在网关、服务 A、服务 B 的日志中搜索同一个 TraceId,确认链路贯通。
- 异常排查:当出现问题时,只需 grep 某个 TraceId,即可获取全链路的日志上下文。
核心要点总结
- 网关生成:确保入口唯一,避免分布式环境下 TraceId 冲突。
- 过滤器透传 :所有服务必须添加过滤器,且日志模板包含
%X{traceId}
。 - 跨服务调用:RestTemplate/Feign 必须加拦截器,否则调用下游时 TraceId 丢失。
- 异步场景:线程池需包装,手动传递 MDC 上下文,否则子线程日志无 TraceId。
场景分类 | 具体场景 | 核心问题 | 解决方案 | 侵入性 | 适用范围 |
---|---|---|---|---|---|
跨服务调用 | HTTP调用(Feign) | 服务间请求头未自动携带TraceId | 配置Feign拦截器,自动从MDC获取并设置X-Trace-Id 头 |
低 | Spring Cloud微服务 |
RPC调用(Dubbo) | 分布式调用中上下文丢失 | 实现Dubbo过滤器,通过Attachment传递TraceId,Provider端自动设置MDC | 低 | Dubbo微服务 | |
消息队列(Kafka/RabbitMQ) | 消息生产/消费链路断裂 | 生产者拦截器添加TraceId到消息头,消费者拦截器读取并设置MDC | 低 | 基于消息队列的异步通信 | |
异步场景 | 线程池(ThreadPool) | 子线程无法继承父线程MDC | 自定义线程池,提交任务时捕获父线程MDC,子线程执行前恢复 | 中 | 所有线程池异步任务 |
@Async注解 | Spring异步方法默认不传递MDC | 使用自定义线程池(同上)作为@Async的执行器 | 低 | Spring框架异步方法 | |
CompletableFuture | 链式调用中线程切换导致TraceId丢失 | 封装CompletableFuture工具类,自动传递MDC上下文 | 低 | Java原生异步编程 | |
基础场景 | 网关入口 | 全链路TraceId生成源头不统一 | 网关全局过滤器生成唯一TraceId,写入请求头并设置MDC | 低 | 有网关的分布式系统 |
单体服务内部 | 单个服务内日志无统一TraceId | 过滤器从请求头(或生成)TraceId,设置到MDC,日志模板包含%X{traceId} |
低 | 单体服务或微服务节点 | |
特殊场景 | 定时任务(Quartz) | 无请求触发,无初始TraceId | 任务执行时自动生成TraceId并设置MDC | 低 | 定时任务、后台任务 |
跨语言调用(如Java→Python) | 不同语言间上下文传递格式不统一 | 约定X-Trace-Id 头,Python端通过中间件(如Flask/Django过滤器)接收并记录 |
中 | 多语言混合架构 |
通过这套方案,可在10分钟内实现轻量级全链路追踪,无需引入复杂组件,适合中小团队快速落地。