一、问题背景

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

二、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,测试线程

MDC实现微服务链路追踪的更多相关文章

  1. 阿里P7架构师详解微服务链路追踪原理

    背景介绍 在微服务横行的时代,服务化思维逐渐成为了程序员的基本思维模式,但是,由于绝大部分项目只是一味地增加服务,并没有对其妥善管理,当接口出现问题时,很难从错综复杂的服务调用网络中找到问题根源,从而 ...

  2. 「Java分享客栈」随时用随时翻:微服务链路追踪之zipkin搭建

    前言 微服务治理方案中,链路追踪是必修课,SpringCloud的组件其实使用很简单,生产环境中真正令人头疼的往往是软件维护,接口在微服务间的调用究竟哪个环节出现了问题,哪个环节耗时较长,这都是项目上 ...

  3. Gokit微服务-服务链路追踪

    https://mp.weixin.qq.com/s/gjKOy4SDpsjUXDC3Q1YdFw Gokit微服务-服务链路追踪 原创: 兮一昂吧 兮一昂吧 2月28日

  4. 服务链路追踪(Spring Cloud Sleuth)

    sleuth:英 [slu:θ] 美 [sluθ] n.足迹,警犬,侦探vi.做侦探 微服务架构是一个分布式架构,它按业务划分服务单元,一个分布式系统往往有很多个服务单元.由于服务单元数量众多,业务的 ...

  5. spring cloud 入门系列八:使用spring cloud sleuth整合zipkin进行服务链路追踪

    好久没有写博客了,主要是最近有些忙,今天忙里偷闲来一篇. =======我是华丽的分割线========== 微服务架构是一种分布式架构,微服务系统按照业务划分服务单元,一个微服务往往会有很多个服务单 ...

  6. Spring Cloud Sleuth+ZipKin+ELK服务链路追踪(七)

    序言 sleuth是spring cloud的分布式跟踪工具,主要记录链路调用数据,本身只支持内存存储,在业务量大的场景下,为拉提升系统性能也可通过http传输数据,也可换做rabbit或者kafka ...

  7. Zipkin和微服务链路跟踪

    https://cloud.tencent.com/developer/article/1082821 Zipkin和微服务链路跟踪 本期分享的内容是有关zipkin和分布式跟踪的内容. 首先,我们还 ...

  8. spring cloud微服务快速教程之(十一) Sleuth(zipkin) 服务链路追踪

    0.前言 微服务架构上众多微服务通过REST调用,可能需要很多个服务协同才能完成一个接口功能,如果链路上任何一个服务出现问题或者网络超时,都会形成导致接口调用失败.随着业务的不断扩张,服务之间互相调用 ...

  9. Spring Cloud Sleuth服务链路追踪(zipkin)(转)

    这篇文章主要讲述服务追踪组件zipkin,Spring Cloud Sleuth集成了zipkin组件. 一.简介 Spring Cloud Sleuth 主要功能就是在分布式系统中提供追踪解决方案, ...

  10. SpringCloud(7)服务链路追踪Spring Cloud Sleuth

    1.简介 Spring Cloud Sleuth 主要功能就是在分布式系统中提供追踪解决方案,并且兼容支持了 zipkin,你只需要在pom文件中引入相应的依赖即可.本文主要讲述服务追踪组件zipki ...

随机推荐

  1. Python下的三维建模和可视化

    本文介绍基于AnyCAD Rapid Py三维图形平台开发Python的三维应用 1 准备工作 1.1 安装 vc_resit 2022 在Windows下,AnyCAD Rapid SDK依赖Vis ...

  2. WebAssembly核心编程[1]:wasm模块实例化的N种方式

    当我们在一个Web应用中使用WebAssembly,最终的目的要么是执行wasm模块的入口程序(通过start指令指定的函数),要么是调用其导出的函数,这一切的前提需要创建一个通过WebAssembl ...

  3. 多进程|基于非阻塞调用的轮询检测方案|进程等待|重新理解挂起|Linux OS

    说在前面 今天给大家带来操作系统中进程等待的概念,我们学习的操作系统是Linux操作系统. 我们今天主要的目标就是认识wait和waitpid这两个系统调用. 前言 那么这里博主先安利一下一些干货满满 ...

  4. Shopee x JuiceFS:ClickHouse 冷热数据分离存储架构与实践

    本文来自 shopee 技术团队 摘要 Shopee ClickHouse 是一款基于开源数据库 ClickHouse 做二次开发.架构演进的高可用分布式分析型数据库.本文将主要介绍 Shopee C ...

  5. Proxmox 7.4 使用vgpu_unlock,为GTX1060开启vGPU支持

    本文在 2021年发布的博客<Proxmox 5.4使用vgpu_unlock,为GTX1060开启vGPU支持>,介绍了 Proxmox VE 5.4 上部署vGPU unlock 的操 ...

  6. 17.3 给内存映射文件指定基地址--《Windows核心编程》

    可以使用 MapViewOfFileEx 函数,建议系统把文件映射到指定的地址. 其他参数与 MapViewOfFile 相同,最后一个参数 pvBaseeAddress 指定目标地址.同 Virtu ...

  7. .NET Core开发实战(第9课:命令行配置提供程序)--学习笔记

    09 | 命令行配置提供程序:最简单快捷的配置注入方法 这一节讲解如何使用命令行参数来作为配置数据源 命令行配置(提供程序的)支持三种格式的命令 1.无前缀的 key=value 模式 2.双中横线模 ...

  8. Windows上同时使用有线网络及无线网络连接配置

    由于公司搬到了新的办公地点,公司内部只有内网,当需要连接互联网查询资料时只能切换网络,非常麻烦.所以为了能够同时连接连接公司内网,又能够访问互联网,这里介绍如何同时连接无线和有线. 有线网络:10.3 ...

  9. Spark相关面试题

    Spark Core面试篇01 一.简答题 1.Spark master使用zookeeper进行HA的,有哪些元数据保存在Zookeeper? 答:spark通过这个参数spark.deploy.z ...

  10. [JVM]关于swap的理解

    关于swap的理解 概念 swap就是内存交换的意思. 计算机内存分为物理内存和虚拟内存.物理内存就是计算机实际内存的大小:虚拟内存是磁盘空间里开辟出一部分,是虚拟出来的内存空间,所以也叫磁盘缓存. ...