一 MDC与TraceID的介绍
MDC
(Mapped Diagnostic Context)和TraceID
都是用于在分布式系统中跟踪和诊断日志的重要概念,它们之间有一定的关系。
-
MDC(Mapped Diagnostic Context): MDC是一个在Java应用程序中广泛使用的机制,它允许你在一个线程中设置一些上下文信息,并在整个线程的执行过程中保持不变。MDC通常用于在日志中传递诊断信息,如请求ID、用户ID、IP地址等,在多线程环境下,确保日志输出的信息与实际业务操作相关联。MDC信息是线程局部的,不同的线程之间不会互相影响,因此适用于多线程并发环境。
-
TraceID: TraceID是在分布式系统中用于跟踪请求的唯一标识符。在分布式系统中,一个请求可能涉及多个微服务之间的调用,这些调用可能会经过多个不同的服务节点。通过在请求的头部或上下文中添加TraceID,可以将整个请求链路中的所有日志都关联起来,形成一个完整的请求追踪。TraceID是一个全局唯一的标识符,它能够帮助开发人员在日志中快速定位和追踪请求的执行过程。
关系:
- MDC和TraceID都涉及到在日志中传递关键信息,帮助跟踪和诊断请求的执行过程。
- 通常情况下,TraceID是在请求的入口处生成,并在整个请求链路中传递。当请求进入一个新的服务节点时,会从上游节点获取TraceID,并继续传递下去。
- MDC通常用于在单个应用程序内部的日志跟踪,它是线程局部的,不同的请求线程之间不会互相干扰。
- 在分布式系统中,MDC通常用于将TraceID设置到当前线程的MDC上下文中,以便在整个请求处理过程中,不同的业务逻辑都能够打印出TraceID,并保持日志的一致性。
- TraceID是全局唯一的,可以用于追踪整个分布式系统中的请求链路,而MDC用于在当前线程范围内传递诊断信息。
二 结合使用的方法
结合使用MDC和TraceID可以实现在分布式系统中的日志追踪和诊断。下面是结合使用MDC和TraceID的方法:
-
生成和传递TraceID: 在分布式系统中的请求入口处,例如API网关或服务的控制器层,生成一个全局唯一的TraceID,并将它添加到请求头部或上下文中。然后,将这个TraceID传递给其他微服务节点。在Spring Boot应用程序中,你可以使用
Filter
或Interceptor
来实现这一点,拦截请求并将TraceID设置到MDC中。 -
将TraceID设置到MDC: 在Spring Boot应用程序中,你可以使用自定义的MDC装饰器(如上面提到的
MdcTaskDecorator
),在处理请求之前,将请求头部或上下文中的TraceID设置到MDC中。这样,在整个请求处理过程中,MDC中就会包含TraceID的信息。 -
在日志中输出TraceID: 在应用程序的业务逻辑中,在日志输出的地方,可以通过MDC获取TraceID,并将其包含在日志中。这样,你可以在日志中快速定位和追踪特定请求的执行过程,跨越不同的服务节点。
-
传递TraceID给其他服务: 在服务节点之间的调用中,将MDC中的TraceID继续传递给其他服务。这可以通过Spring Cloud Sleuth等分布式跟踪工具自动实现,或者通过手动传递Header的方式。
总的来说,结合使用MDC和TraceID可以实现在分布式系统中的日志追踪和诊断。TraceID在请求的入口处生成并传递,MDC将TraceID设置到当前线程的上下文中,在整个请求处理过程中保持不变。在日志输出时,从MDC中获取TraceID并将其包含在日志中,这样就可以在整个请求链路中追踪和诊断请求的执行过程,方便开发人员快速定位和解决问题。
三 详细案例
假设我们有一个分布式系统,包含两个微服务:ServiceA
和ServiceB
。ServiceA
是一个API网关,负责接收外部请求并将请求转发给ServiceB
进行处理。我们希望在整个请求处理过程中,能够跟踪请求的执行过程,并在日志中包含TraceID,以便于诊断和调试问题。
-
生成和传递TraceID: 在
ServiceA
的控制器层,我们可以生成一个全局唯一的TraceID,并将它添加到请求头部。在Spring Boot中,可以使用javax.servlet.Filter
来拦截请求,并在请求头部添加TraceID。public class TraceIdFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String traceId = UUID.randomUUID().toString(); HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setHeader("X-Trace-Id", traceId); chain.doFilter(httpRequest, httpResponse); } }
-
将TraceID设置到MDC: 在
ServiceA
中的控制器层,我们可以使用自定义的MDC装饰器,将请求头中的TraceID设置到MDC中,以便在整个请求处理过程中使用。public class MdcTaskDecorator implements TaskDecorator { @Override public Runnable decorate(Runnable runnable) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String traceId = request.getHeader("X-Trace-Id"); MDC.put("traceId", traceId); return runnable; } }
-
在日志中输出TraceID: 在
ServiceA
和ServiceB
中的业务逻辑中,在日志输出的地方,我们可以通过MDC获取TraceID,并将其包含在日志中。@RestController public class ServiceAController { private static final Logger LOGGER = LoggerFactory.getLogger(ServiceAController.class); @Autowired private ServiceBClient serviceBClient; @GetMapping("/process") public String processRequest() { LOGGER.info("Received request with TraceID: {}", MDC.get("traceId")); // 处理请求... return serviceBClient.processRequest(); } } @Service public class ServiceBClient { private static final Logger LOGGER = LoggerFactory.getLogger(ServiceBClient.class); public String processRequest() { LOGGER.info("Processing request with TraceID: {}", MDC.get("traceId")); // 处理请求... return "Processed"; } }
-
传递TraceID给其他服务: 在
ServiceA
调用ServiceB
的时候,我们可以使用Feign或RestTemplate等方式将MDC中的TraceID传递给ServiceB
。@FeignClient(name = "serviceB", url = "${serviceB.url}") public interface ServiceBClient { @GetMapping("/process") String processRequest(); }
通过以上步骤,我们就实现了在分布式系统中的日志追踪和诊断。请求经过ServiceA
的时候,生成一个全局唯一的TraceID,并将其添加到请求头部。在ServiceA
的控制器层,使用MdcTaskDecorator
将请求头部的TraceID设置到MDC中。在日志输出时,可以通过MDC获取TraceID并将其包含在日志中。同时,在调用ServiceB
的时候,可以通过Feign或RestTemplate将MDC中的TraceID传递给ServiceB
,从而实现整个请求链路的追踪。这样,在日志中就可以快速定位和追踪特定请求的执行过程,方便开发人员诊断和调试问题。
四 扩展
在Java中,MDC允许你在一个线程中设置一些上下文信息(比如请求ID、用户ID等),然后这些信息可以在整个线程的执行过程中保持不变,也可以在子线程中继续使用。这在多线程环境下很有用,可以确保日志中打印的信息与实际业务操作相关联。
这是一个实现了TaskDecorator
接口的自定义类MdcTaskDecorator
,用于将MDC(Mapped Diagnostic Context)信息从父线程传递到子线程,确保多线程环境下MDC信息的正确传递。
public class MdcTaskDecorator implements TaskDecorator {
// 从父线程获取MDC的上下文信息,并保存在map中
Map<String, String> map = MDC.getCopyOfContextMap();
@Override
public Runnable decorate(Runnable runnable) {
return () -> {
try {
// 将父线程的MDC上下文信息设置到当前线程
MDC.setContextMap(map);
// 执行任务
runnable.run();
} finally {
// 清除当前线程的MDC上下文信息
MDC.clear();
}
};
}
}
解释:
-
MdcTaskDecorator
实现了TaskDecorator
接口,用于实现装饰任务的逻辑。 -
Map<String, String> map = MDC.getCopyOfContextMap();
从父线程中获取MDC的上下文信息,并将其保存在map
变量中。这样,可以确保在装饰的子线程中能够使用相同的MDC上下文信息。 -
public Runnable decorate(Runnable runnable) { ... }
实现TaskDecorator
接口的decorate
方法,该方法用于装饰任务。 -
MDC.setContextMap(map);
在装饰任务之前,将父线程的MDC上下文信息设置到当前线程,确保子线程可以访问到父线程的MDC信息。 -
runnable.run();
执行任务,也就是传入的runnable
对象所代表的任务。 -
MDC.clear();
在任务执行完毕后,清除当前线程的MDC上下文信息,防止对后续任务产生干扰。
这样,在使用线程池等多线程执行任务时,如果你在父线程中设置了MDC上下文信息,通过使用MdcTaskDecorator
,可以确保子线程能够正确继承和使用父线程的MDC信息。这样在日志输出等情况下,能够保持MDC信息的一致性。