MDC全局链路追踪原理与实现

8,600 阅读3分钟

背景

在目前的微服务体系中,服务应用多,调用链复杂,相应的排查问题的难度也随之上升。当应用发生异常时,我们需要快速定位问题日志,这就需要我们对请求链路进行追踪,在请求到达系统时产生一个能够标识整个请求生命周期的ID。

MDC(Mapped Diagnostic Contexts)介绍

MDC是Slf4J类日志系统中实现分布式多线程日志数据传递的重要工具,用户可利用MDC将一些运行时的上下文数据打印出来。目前只有log4j和logback提供原生的MDC支持

源码分析

MDC在线程上下文中了维护一个 Map<String,String> 属性,可以理解为一个线程级别的容器,在logback中,可以通过%X{key}获取MDC上下文中的值 MDC提供的接口

public interface MDCAdapter {
    // 获取当前线程MDC上下文中指定key的值
    void put(String var1, String var2);
	// 往当前线程MDC上下文中
    String get(String var1);
	// 移除当前线程MDC上下文中指定key的键值
    void remove(String var1);
	// 清空MDC上下文
    void clear();
	// 获取MDC上下文
    Map<String, String> getCopyOfContextMap();
	// 设置MDC上下文呢
    void setContextMap(Map<String, String> var1);
}

logback的实现LogbackMDCAdapter

public class LogbackMDCAdapter implements MDCAdapter {
    final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal();
    private static final int WRITE_OPERATION = 1;
    private static final int MAP_COPY_OPERATION = 2;
    final ThreadLocal<Integer> lastOperation = new ThreadLocal();

    public LogbackMDCAdapter() {
    }
    ...

可以看到LogbackMDC声明了类型为ThreadLocal的map。ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本,也就是说ThreadLocal变量在线程之间隔离而在方法或类间能够共享

LogbackMDCAdapter中的put()

  public void put(String key, String val) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key cannot be null");
        } else {
            Map<String, String> oldMap = (Map)this.copyOnThreadLocal.get();
            Integer lastOp = this.getAndSetLastOperation(1);
            if (!this.wasLastOpReadOrNull(lastOp) && oldMap != null) {
                oldMap.put(key, val);
            } else {
                Map<String, String> newMap = this.duplicateAndInsertNewMap(oldMap);
                newMap.put(key, val);
            }

        }
    }

校验并把键值设值到ThreadLocal的容器中

duplicateAndInsertNewMap()

 private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {
        Map<String, String> newMap = Collections.synchronizedMap(new HashMap());
        if (oldMap != null) {
            synchronized(oldMap) {
                newMap.putAll(oldMap);
            }
        }

        this.copyOnThreadLocal.set(newMap);
        return newMap;
    }

创建线程安全的HashMap作为容器,并放到ThreadLocal中

方案与实现

声明过滤器对请求拦截

@Configuration
public class FilterConfig {
 @Bean
 public FilterRegistrationBean registFilter() {
     FilterRegistrationBean registration = new FilterRegistrationBean();
     registration.setFilter(new DyeFilter());
     registration.addUrlPatterns("/*");
     registration.setName("DyeFilter");
     registration.setOrder(1);
     return registration;
 }
}

构建上下文对象,并赋值到MDC

@WebFilter(filterName = "DyeFilter")
public class DyeFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        HttpServletRequest request = (HttpServletRequest) req;
        initGlobalContext(request);
        try {
            chain.doFilter(req, resp);
        } finally {
            ContextUtil.clearContext();
        }
    }

    private static void initGlobalContext(HttpServletRequest servletRequest) {
        GlobalContext context = new GlobalContext();
        String contextStr = servletRequest.getHeader(ContextConstant.REQUEST_CONTEXT);
        if (!StringUtils.isEmpty(contextStr)){
            context = JSON.parseObject(contextStr,GlobalContext.class);
        }else{
            context.setTraceId(UUID.randomUUID().toString());
            context.setClientIp(IpUtil.getClientAddress(servletRequest));
        }
        ContextUtil.setCurrentContext(context);
    }

    @Override
    public void init(FilterConfig config){
    }

    @Override
    public void destroy() {
    }
}

在业务入口构建GlobalContext,在后续的调用链中,将从请求头中获取构建好的GlobalContext

public class ContextUtil {
    private static ThreadLocal<GlobalContext> currentThreadLocal = ThreadLocal.withInitial(GlobalContext::new);

    public static void setCurrentContext(GlobalContext context) {
        currentThreadLocal.set(context);
        String traceId = context.getTraceId();
        if (traceId != null && traceId.length() > 0 && MDC.get(ContextConstant.TRACK_ID) == null) {
            MDC.put(ContextConstant.TRACK_ID, traceId);
        }
    }

    public static GlobalContext getCurrentContext() {
       return currentThreadLocal.get();
    }

    public static void clearContext() {
        MDC.clear();
        currentThreadLocal.remove();
    }
}

获取GlobalContext,并通过请求头透传

public class FeignConfig {
 @Bean
 public RequestInterceptor header(){
     return this::setRequestHeader;
 }
 private void setRequestHeader(RequestTemplate requestTemplate){
     GlobalContext context = ContextUtil.getCurrentContext();
     if (context!=null){
         requestTemplate.header(ContextConstant.REQUEST_CONTEXT, JSONParser.quote(JSON.toJSONString(context)));
     }
 }
}

这里只举例通过Feign调用的方式,服务之间调用还有很多种,主要思路就是把GlobalContext透传到下一个服务

在logback配置文件中配置MDC容器中的变量%X{trackId}

 <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %X{trackId} [%15.15t] %class{39}.%method[%L] : %m%n</pattern>
            <!-- 控制台也要使用UTF-8,不要使用GBK,否则会中文乱码 -->
            <charset>UTF-8</charset>f
        </encoder>
    </appender>l

DEMO地址: github.com/chenxuancod…