首页 > 其他分享 >MDC实现微服务链路追踪

MDC实现微服务链路追踪

时间:2023-02-27 19:11:46浏览次数:29  
标签:traceId MDC 线程 链路 static TraceIdUtil public 追踪

一、问题背景

在微服务架构中,我们没办法快速定位用户在一次请求中对应的所有日志,在排查生产问题的时候会非常困难,那是因为我们在输出的日志的时候没把请求的唯一标示输出到我们的日志中,导致我们没办法根据一个请求或者用户身份标识来做日志的过滤。

二、MDC简介

MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的Map,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

API说明:

clear() => 移除所有MDC
get (String key) => 获取当前线程MDC中指定key的值
getContext() => 获取当前线程MDC的MDC
put(String key, Object o) => 往当前线程的MDC中存入指定的键值对
remove(String key) => 删除当前线程MDC中指定的键值对 。

三、实现方式

由于 MDC 内部使用的是 ThreadLocal 所以只有本线程才有效,子线程和下游的服务 MDC 里的值会丢失,所以方案主要的难点是解决值的传递问题;

1. 工具类

public class TraceIdUtil {
    public static final String TRACE_ID = "traceId";

    public static String getTraceId() {
        String traceId = MDC.get(TRACE_ID);
        return traceId == null ? "" : traceId;
    }

    public static void setTraceId(String traceId) {
        MDC.put(TRACE_ID, traceId);
    }

    public static void remove() {
        MDC.remove(TRACE_ID);
    }

    public static void clear() {
        MDC.clear();
    }

    public static String generateTraceId() {
        return UUID.randomUUID().toString().replace("-", "");
    }

}
  • logback日志,这里的[%X{traceId}] 就是MDC中的,切不可写错key
<property name="console.log.pattern"
          value="%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}) [%X{traceId}] - %msg%n"/>

2. 拦截器

  • 通过拦截器拦截请求,判断请求头中是否存在traceId,如果存在则存入MDC上下文中,不存在则生成traceId存入MDC中.
public class MdcInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果有上层调用就用上层的ID
        String traceId = request.getHeader(TraceIdUtil.TRACE_ID);
        if (StrUtil.isEmpty(traceId)) {
            TraceIdUtil.setTraceId(TraceIdUtil.generateTraceId());
        } else {
            TraceIdUtil.setTraceId(traceId);
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //调用结束后删除
        TraceIdUtil.remove();
    }

}
  • 注册拦截器
@Component
public class WebAppConfigurer implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 可添加多个
        registry.addInterceptor(new MdcInterceptor()).addPathPatterns("/**");
    }
}

3. 请求头传递

  • 这里使用的是openFeign的解决方案,其他的类似,在请求头中塞入traceId
@Component
public class MyFeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        String traceId = TraceIdUtil.getTraceId();
      	// 传递请求头
        if (StrUtil.isNotBlank(traceId)) {
            requestTemplate.header(TraceIdUtil.TRACE_ID, traceId);
        } else {
            requestTemplate.header(TraceIdUtil.TRACE_ID, TraceIdUtil.generateTraceId());
        }

    }
}

4. 线程父子间传递

  • 由于MDC的底层是ThreadLocal,所以会导致子线程拿不到主线程里的数据
public class ThreadMdcUtil {
    public static void setTraceIdIfAbsent() {
        if (MDC.get(TraceIdUtil.TRACE_ID) == null) {
            MDC.put(TraceIdUtil.TRACE_ID, TraceIdUtil.generateTraceId());
        }
    }

    public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                return callable.call();
            } finally {
                MDC.clear();
            }
        };
    }

    public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            //设置traceId
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}
  • 自定义线程池
public class ThreadPoolExecutorMdcWrapper extends ThreadPoolTaskExecutor {
    private static final long serialVersionUID = 3940722618853093830L;

    @Override
    public void execute(Runnable task) {
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public Future<?> submit(Runnable task) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
}
@Configuration
public class ThreadPoolTaskExecutorConfig{
    //最大可用的CPU核数
    public static final int PROCESSORS = Runtime.getRuntime().availableProcessors();
    @Bean
    public ThreadPoolExecutorMdcWrapper getExecutor(){
        ThreadPoolExecutorMdcWrapper executor =new ThreadPoolExecutorMdcWrapper();
        executor.setCorePoolSize(PROCESSORS *2);
        executor.setMaxPoolSize(PROCESSORS * 4);
        executor.setQueueCapacity(50);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("Task-A");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        executor.initialize();
        return executor;
    }
}
  • 单线程的做法(不建议)
public class MDCRunable implements Runnable {
   
    private Map<String, String> copyOfContextMap;

    private Runnable runnable;

    public MDCRunable(Runnable runnable) {
        this.copyOfContextMap = MDC.getCopyOfContextMap();
        this.runnable = runnable;
    }

    @Override
    public void run() {
        if (!copyOfContextMap.isEmpty()) {
            MDC.setContextMap(copyOfContextMap);
        }
        try {
            runnable.run();
        } finally {
            if (!copyOfContextMap.isEmpty()) {
                MDC.clear();
            }
        }
    }
}

5. 测试结果

  • 上游日志
2023-02-27 18:58:05 [http-nio-8099-exec-2] INFO  c.s.c.controller.ConsumerController [65f8173c73f945d99ea5b0ab209164fd] - consumer-打印日志2
2023-02-27 18:58:05 [DefaultAsync-1] INFO  c.s.c.controller.ConsumerController [65f8173c73f945d99ea5b0ab209164fd] - consumer-thread-01,测试线程
2023-02-27 18:58:05 [pool-9-thread-1] INFO  c.s.c.controller.ConsumerController [65f8173c73f945d99ea5b0ab209164fd] - consumer-mdc-thread
  • 下游日志
2023-02-27 18:58:05 [http-nio-8089-exec-1] INFO  c.s.f.p.c.ProviderController [65f8173c73f945d99ea5b0ab209164fd] - provider-测试日志
2023-02-27 18:58:05 [DefaultAsync-1] INFO  c.s.f.p.c.ProviderController [65f8173c73f945d99ea5b0ab209164fd] - provider-thread-02,测试线程

标签:traceId,MDC,线程,链路,static,TraceIdUtil,public,追踪
From: https://www.cnblogs.com/sun2020/p/17161509.html

相关文章

  • Spring Boot 实现日志链路追踪,无需引入组件,让日志定位更方便!
    来源:blog.csdn.net/qq_35387940/article/details/125062368前言从文章标题就知道,这篇文章是介绍些什么。这是我一位朋友的问题反馈:好像是的,确实这种现象是普遍存在的......
  • 华为交换机配置链路聚合(手工模式链路聚合和lacp模式聚合)UBUNTU 20.04 服务器配置多网
    一.华为交换机配置链路聚合(Eth-Trunk),是将多个物理接口捆绑为一个逻辑接口,实现增加链路带宽、提高可靠性、提供负载分担的目的。链路聚合两种模式:手工负载分担模式(默认模式,默......
  • IS-95前向链路系统误码率matlab仿真,包括扩频调制,匹配滤波,RAKE接收
    1.算法描述       前向链路指由基站发往移动台的无线通信链路,也称作下行链路。IS-95系统前向链路最多可以有64个同时传输的信道,它们是在PN序列上再采用正交的Walsh......
  • linux-路由追踪
    [root@masterdeployment]#yum-yinstallmtr[root@masterdeployment]#mtr36.134.8.76......
  • 实时渲染路径追踪概述
    大家好,本文是对Real-TimePathTracingandBeyond和它的视频:HPG2022Monday,Day1(enhanced)学习的总结,并结合我的理解进行了一些延伸得益于下面的技术进步,使得通常应......
  • 第三章 数据链路层
    一、数据链路层概述链路就是从一个结点到相邻节点的一段物理线路,中间没有其他任何的交换节点数据链路是指把实现通信协议的硬件和软件加到链路上,就构成了数据链路数据......
  • 搜索EE场景排序链路升级
    作者:京东零售吕豪背景EE(Explore&Exploit)模块是搜索系统中改善生态、探索商品的重要链路,其目标是缓解数据马太效应导致模型对商品排序丰富性、探索性不足,带来的系统非最优......
  • m基于LDPC+QPSK通信链路误码率matlab仿真
    1.算法描述       LDPC(Low-densityParity-check,低密度奇偶校验)码是由Gallager在1963年提出的一类具有稀疏校验矩阵的线性分组码(linearblockcodes),然而......
  • 搜索EE场景排序链路升级
    作者:京东零售吕豪背景EE(Explore&Exploit)模块是搜索系统中改善生态、探索商品的重要链路,其目标是缓解数据马太效应导致模型对商品排序丰富性、探索性不足,带来的系统非......
  • git临时忽略已追踪文件
    背景参考使用git,日常开发时经常会修改一些项目中已经追踪的配置文件,但是并不想提交更改。通常需要在每次提交时排除掉对应的文件,很麻烦。并且稍不注意就会提交到远......