ITPub博客

首页 > 数据库 > 数据库开发技术 > 调用链追踪神器:深入理解MDC

调用链追踪神器:深入理解MDC

原创 数据库开发技术 作者:IT枫斗者 时间:2024-12-12 17:16:29 0 删除 编辑



一、引言

在日常开发工作中,排查线上问题时常常会遇到诸如以下困惑:


某次调用异常,具体是哪笔订单的哪个操作导致的?涉及哪些服务?请求来自哪台服务器?


异常操作对数据产生了哪些影响?


某个用户在特定时间段内执行了哪些操作?


随着系统架构的日益复杂,特别是采用了分布式架构后,这些问题变得更加难以追踪和解决。微服务架构、分布式中间件(如消息队列、分布式缓存、对象存储)以及跨区域调用等,都增加了问题排查的难度。

为了有效地解决这些问题,MDC(Mapped Diagnostic Context)成为了日志追踪中的一大利器。


什么是MDC?

MDC(映射诊断上下文) 是 Logback 框架中的一个特性(Logback 是 Spring Boot 默认的日志框架),它允许在日志消息中存储和传播上下文信息。通过MDC,可以在日志事件之间传递键值对,从而在多线程和分布式环境中有效地追踪日志。



二、快速上手MDC

1. 添加依赖

首先,在 pom.xml 中添加 logback-classic 依赖:
<dependency>   <groupId>ch.qos.logback</groupId>   <artifactId>logback-classic</artifactId>   <version>1.3.0</version></dependency>



2. 配置Logback

在项目的 src/main/resources 目录下创建或编辑 logback.xml 文件,配置日志输出格式以包含MDC信息:
<configuration>   <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">       <encoder>           <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [traceId=%X{traceId}] [userId=%X{userId}] %msg%n</pattern>       </encoder>   </appender>   <root level="INFO">       <appender-ref ref="CONSOLE" />   </root></configuration>



3. 在代码中使用MDC

通过在代码中设置和清除MDC的键值对,可以在日志中自动包含这些上下文信息。
import org.slf4j.MDC;import org.springframework.stereotype.Component;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;@Componentpublic class LoggingInterceptor implements HandlerInterceptor {   @Override   public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {       String traceId = generateTraceId();       String userId = request.getHeader("User-ID");       MDC.put("traceId", traceId);       MDC.put("userId", userId);       return true;   }   @Override   public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {       MDC.remove("traceId");       MDC.remove("userId");   }   private String generateTraceId() {       return UUID.randomUUID().toString().replace("-", "");   }}



三、深入理解MDC的应用场景


1. 什么是traceId?

traceId 是用于标识一次具体请求的唯一标识符。通过 traceId,可以将一个请求在系统中经过的所有服务调用链串联起来,实现全局的日志追踪。


traceId 与 spanId 的区别

traceId:用于串联一次请求在系统中经过的所有服务和调用路径。
spanId:用于标识单个服务调用的唯一标识符,记录调用的先后关系。



2. traceId的生成与传递

在分布式系统中,通常由接收请求的第一个服务生成 traceId,然后在后续的服务调用中传递该 traceId,确保整个调用链的可追溯性。


traceId的生成规则

参考阿里巴巴的SkyWalking组件,traceId 通常由以下部分组成:

1.机器标识(8位十六进制):如 1a2b3c4d,代表生成ID的机器。

2.时间戳(13位):如 1701234567890,精确到毫秒。

3.序列号(4位):如 5678,用于同一毫秒内的多次请求。

4.进程ID(5位):如 91011,避免多进程环境下的ID冲突。


示例:1a2b3c4d17012345678905678abcd1



3. 实现MDC最佳实践



前端交互中的traceId

前端在与后端交互时,需要将 traceId 传递给后端,以便在日志中进行关联。

示例:在HTTP响应的Header中添加 X-Trace-Id,在Body中也包含 traceId 字段。

{   "status": "success",   "message": null,   "traceId": "1a2b3c4d17012345678905678abcd1",   "data": {}}



服务间调用中的traceId

在微服务架构中,不同服务之间的调用需要传递 traceId,以便在日志中串联起整个调用链。

  • Feign客户端中添加traceId

import feign.RequestInterceptor;import feign.RequestTemplate;import org.slf4j.MDC;import org.springframework.stereotype.Component;@Componentpublic class FeignTraceInterceptor implements RequestInterceptor {   @Override   public void apply(RequestTemplate template) {       String traceId = MDC.get("traceId");       if (traceId != null) {           template.header("X-Trace-Id", traceId);       }   }}
  • Dubbo服务消费者获取traceId

import com.alibaba.dubbo.common.extension.Activate;import com.alibaba.dubbo.rpc.*;@Activate(group = {Constants.CONSUMER}, order = -99999)public class DubboTraceConsumerFilter implements Filter {   @Override   public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {       String traceId = MDC.get("traceId");       if (traceId == null) {           traceId = UUID.randomUUID().toString().replace("-", "");       }       RpcContext.getContext().setAttachment("traceId", traceId);       return invoker.invoke(invocation);   }}
  • Dubbo服务提供者设置traceId

import com.alibaba.dubbo.common.extension.Activate;import com.alibaba.dubbo.rpc.*;import org.slf4j.MDC;@Activate(group = {Constants.PROVIDER}, order = -99999)public class DubboTraceProviderFilter implements Filter {   @Override   public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {       String traceId = invocation.getAttachment("traceId");       if (traceId == null) {           traceId = UUID.randomUUID().toString().replace("-", "");       }       MDC.put("traceId", traceId);       try {           return invoker.invoke(invocation);       } finally {           MDC.remove("traceId");       }   }}



消息队列与任务调度中的traceId

在消息队列(如RocketMQ)和任务调度(如XXL-JOB)中,也需要传递 traceId,以便在消费端进行日志追踪。

  • RocketMQ消费者消费消息时设置traceId

import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;import org.apache.rocketmq.spring.core.MessageListener;import org.slf4j.MDC;import org.springframework.stereotype.Component;@Component@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-consumer-group")public class OrderMessageListener implements MessageListener<String> {   @Override   public void onMessage(String message) {       String traceId = MDC.get("traceId");       if (traceId == null) {           traceId = UUID.randomUUID().toString().replace("-", "");           MDC.put("traceId", traceId);       }       try {           // 业务处理       } finally {           MDC.remove("traceId");       }   }}
  • XXL-JOB任务中使用traceId

import com.xxl.job.core.handler.annotation.XxlJob;import org.slf4j.MDC;import org.springframework.stereotype.Component;@Componentpublic class DataSyncJob {      @XxlJob("dataSyncHandler")   public void dataSyncHandler() throws Exception {       String traceId = UUID.randomUUID().toString().replace("-", "");       MDC.put("traceId", traceId);       try {           // 任务处理逻辑       } finally {           MDC.remove("traceId");       }   }}



多线程环境中的traceId传递

由于MDC基于 ThreadLocal 实现,上下文信息是线程私有的,且不具备继承性。因此,在多线程环境中,需手动传递 traceId。

  • 方式一:使用装饰器模式

import org.slf4j.MDC;import org.springframework.core.task.TaskDecorator;import org.springframework.stereotype.Component;import java.util.Map;@Componentpublic class TraceIdTaskDecorator implements TaskDecorator {   @Override   public Runnable decorate(Runnable runnable) {       Map<String, String> context = MDC.getCopyOfContextMap();       return () -> {           if (context != null) {               MDC.setContextMap(context);           }           try {               runnable.run();           } finally {               MDC.clear();           }       };   }}

在 AsyncConfig 中应用装饰器:

import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.task.TaskDecorator;import org.springframework.scheduling.annotation.EnableAsync;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.concurrent.Executor;@Configuration@EnableAsyncpublic class AsyncConfig {      @Bean(name = "asyncExecutor")   public Executor asyncExecutor(TraceIdTaskDecorator decorator) {       ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();       executor.setCorePoolSize(5);       executor.setMaxPoolSize(20);       executor.setQueueCapacity(100);       executor.setThreadNamePrefix("AsyncThread-");       executor.setTaskDecorator(decorator);       executor.initialize();       return executor;   }}
  • 方式二:使用InheritableThreadLocal

public class TraceIdInheritableThreadLocal {   private static final InheritableThreadLocal<String> THREAD_LOCAL = new InheritableThreadLocal<>();   public static void set(String traceId) {       THREAD_LOCAL.set(traceId);   }   public static String get() {       return THREAD_LOCAL.get();   }   public static void clear() {       THREAD_LOCAL.remove();   }}

在主线程中设置 traceId,子线程自动继承:

public class MultiThreadTest {   public static void main(String[] args) {       TraceIdInheritableThreadLocal.set("trace1234567890");              new Thread(() -> {           System.out.println("子线程中的traceId: " + TraceIdInheritableThreadLocal.get());       }).start();              System.out.println("主线程中的traceId: " + TraceIdInheritableThreadLocal.get());   }}


输出结果:

主线程中的traceId: trace1234567890子线程中的traceId: trace1234567890



四、实战中的最佳实践


前端与后端交互中的traceId


在前端与后端的交互中,前端需要接收并传递 traceId,以便后续的日志追踪。

示例:在HTTP响应的Header中添加 X-Trace-Id,在Body中包含 traceId 字段。

{   "status": "success",   "message": null,   "traceId": "2b3c4d5e17022345678906789bcdef2",   "data": {}}



服务间调用中的traceId传递


在微服务架构中,服务之间的调用需要携带 traceId,以便在日志中串联起整个调用链。


  • Feign客户端中添加traceId

import feign.RequestInterceptor;import feign.RequestTemplate;import org.slf4j.MDC;import org.springframework.stereotype.Component;@Componentpublic class FeignTraceInterceptor implements RequestInterceptor {   @Override   public void apply(RequestTemplate template) {       String traceId = MDC.get("traceId");       if (traceId != null) {           template.header("X-Trace-Id", traceId);       }   }}


  • Dubbo服务消费者获取traceId

import com.alibaba.dubbo.common.extension.Activate;import com.alibaba.dubbo.rpc.*;@Activate(group = {Constants.CONSUMER}, order = -99999)public class DubboTraceConsumerFilter implements Filter {   @Override   public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {       String traceId = MDC.get("traceId");       if (traceId == null) {           traceId = UUID.randomUUID().toString().replace("-", "");       }       RpcContext.getContext().setAttachment("traceId", traceId);       return invoker.invoke(invocation);   }}


  • Dubbo服务提供者设置traceId

import com.alibaba.dubbo.common.extension.Activate;import com.alibaba.dubbo.rpc.*;import org.slf4j.MDC;@Activate(group = {Constants.PROVIDER}, order = -99999)public class DubboTraceProviderFilter implements Filter {   @Override   public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {       String traceId = invocation.getAttachment("traceId");       if (traceId == null) {           traceId = UUID.randomUUID().toString().replace("-", "");       }       MDC.put("traceId", traceId);       try {           return invoker.invoke(invocation);       } finally {           MDC.remove("traceId");       }   }}



消息队列与任务调度中的traceId传递


在使用消息队列(如RocketMQ)和任务调度框架(如XXL-JOB)时,同样需要传递 traceId,以便在消费端进行日志追踪。


  • RocketMQ消费者消费消息时设置traceId

import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;import org.apache.rocketmq.spring.core.MessageListener;import org.slf4j.MDC;import org.springframework.stereotype.Component;@Component@RocketMQMessageListener(topic = "notification-topic", consumerGroup = "notification-consumer-group")public class NotificationMessageListener implements MessageListener<String> {   @Override   public void onMessage(String message) {       String traceId = MDC.get("traceId");       if (traceId == null) {           traceId = UUID.randomUUID().toString().replace("-", "");           MDC.put("traceId", traceId);       }       try {           // 业务处理逻辑       } finally {           MDC.remove("traceId");       }   }}


  • XXL-JOB任务中使用traceId

import com.xxl.job.core.handler.annotation.XxlJob;import org.slf4j.MDC;import org.springframework.stereotype.Component;@Componentpublic class DataBackupJob {      @XxlJob("dataBackupHandler")   public void dataBackupHandler() throws Exception {       String traceId = UUID.randomUUID().toString().replace("-", "");       MDC.put("traceId", traceId);       try {           // 任务执行逻辑       } finally {           MDC.remove("traceId");       }   }}




多线程环境中的traceId传递

由于MDC基于 ThreadLocal 实现,上下文信息是线程私有的,且不具备继承性。因此,在多线程环境中,需手动传递 traceId。


  • 使用装饰器模式传递MDC

import org.slf4j.MDC;import org.springframework.core.task.TaskDecorator;import org.springframework.stereotype.Component;import java.util.Map;@Componentpublic class TraceIdTaskDecorator implements TaskDecorator {   @Override   public Runnable decorate(Runnable runnable) {       Map<String, String> contextMap = MDC.getCopyOfContextMap();       return () -> {           if (contextMap != null) {               MDC.setContextMap(contextMap);           }           try {               runnable.run();           } finally {               MDC.clear();           }       };   }}

在异步配置中应用装饰器:

import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.annotation.EnableAsync;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.concurrent.Executor;@Configuration@EnableAsyncpublic class AsyncConfig {      @Bean(name = "asyncExecutor")   public Executor asyncExecutor(TraceIdTaskDecorator decorator) {       ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();       executor.setCorePoolSize(10);       executor.setMaxPoolSize(50);       executor.setQueueCapacity(200);       executor.setThreadNamePrefix("AsyncExecutor-");       executor.setTaskDecorator(decorator);       executor.initialize();       return executor;   }}


  • 使用InheritableThreadLocal传递MDC

public class TraceIdInheritableThreadLocal {   private static final InheritableThreadLocal<String> THREAD_LOCAL = new InheritableThreadLocal<>();   public static void set(String traceId) {       THREAD_LOCAL.set(traceId);   }   public static String get() {       return THREAD_LOCAL.get();   }   public static void clear() {       THREAD_LOCAL.remove();   }}

在主线程中设置 traceId,子线程自动继承:

public class ThreadLocalExample {   public static void main(String[] args) {       TraceIdInheritableThreadLocal.set("trace9876543210");              new Thread(() -> {           System.out.println("子线程中的traceId: " + TraceIdInheritableThreadLocal.get());       }).start();              System.out.println("主线程中的traceId: " + TraceIdInheritableThreadLocal.get());   }}


输出结果:

主线程中的traceId: trace9876543210子线程中的traceId: trace9876543210



五、集成日志查看工具

在实际工作中,日志追踪通常依赖于日志管理工具,如阿里云的SLS(日志服务)。通过traceId,可以在日志管理工具中快速定位调用链,排查问题。


使用SLS查询traceId对应的调用链

  • 接收告警信息:当系统出现异常时,告警信息中包含 traceId。

  • 在SLS中查询:根据 traceId 在SLS中进行日志搜索,快速定位相关的调用链日志,分析问题根源。


示例:

在SLS中输入 traceId=1a2b3c4d17022345678906789bcdef2,即可查看该请求在各个服务中的日志记录,完整还原调用链。



六、总结

在复杂的分布式系统中,确保日志的可追溯性和调用链的完整性,是提升系统可维护性和稳定性的关键。

通过合理使用MDC,结合traceId的生成与传递,可以有效地追踪和排查线上问题。

无论是在微服务架构、消息队列、任务调度还是多线程环境中,MDC都能发挥其强大的作用,帮助开发者轻松应对高复杂度的系统挑战。




👋 大家好,我是枫哥,一名Java后端开发者!我热衷于探索新技术,并在我的微信公众号上分享关于Java 生态和后端开发的知识。
欢迎关注我的公众号,期待与你一起探讨技术的无穷可能!” 目前专注于Java技术分享,覆盖春招、秋招、社招和跳槽相关内容,并提供一对一带徒学习服务。
加入 学徒计划,即可享受内推机会和优质资源,签订协议确保就业无忧。
此外,我们还推出了‘Java跳槽网’, 为你的求职之路提供全方位支持,助你快速找到理想工作





- End -


原文作者:IT枫斗者;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论
大家好,我是枫哥,🌟阿里云技术专家、📝资深面试官、🌹Java跳蚤网课堂创始人。技术交流vx:itfdz666,拥有多年一线研发经验,曾就职过科大讯飞、美团网、平安等公司。看面试实战视频可以关注b站号:IT枫斗者–Java面试。需要看技术文章,可以关注CSDN:IT枫斗者。

注册时间:2020-05-13

  • 博文量
    183
  • 访问量
    81352