一、问题背景

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

二、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 实现专属字典生成器

    编写一个密码生成工具,这里我们使用弱密码与个性化数组组合形成一个定制字典,例如收集用户的姓名,昵称,QQ号手机号等资源,然后通过Python对搜集到的数据与弱密码进行结合,从而定制出属于某个人的专属密 ...

  2. Json Schema高性能.net实现库 LateApexEarlySpeed.Json.Schema - 直接从code生成json schema validator

    LateApexEarlySpeed.Json.Schema - Json schema validator generation from code 除了用户手动传入标准的json schema来生 ...

  3. C#使用Tamir.SharpSsh.jsch上传文件异常Algorithm negotiation fail

    环境 服务器:centos6.5 客户端:Windows 前言 项目中有一个exe,安装在客户端,其中有一个功能是将本地产生的文件上传至服务器,这个功能是以服务的方式安装在客户端上.之前一切好使,文件 ...

  4. React Hooks 使用指南

    .markdown-body { line-height: 1.75; font-weight: 400; font-size: 16px; overflow-x: hidden; color: rg ...

  5. 高精度模板 大数减大数 可变数组vector实现

    vector<int> Sub(vector<int>& A, vector<int>& B)//这里默认长数减去短数 { vector<in ...

  6. 24.1 SetUnhandledExceptionFilter未处理异常--《Windows核心编程》

    对于未处理异常,例如异常过滤返回EXCEPTION_CONTINUE_SEARCH,向上搜索,但无法搜索到处理部分,产生未处理异常.Windows提供了 SetUnhandledExceptionFi ...

  7. Java 运算符 - 除法

    1. 除法运算符 Java中的除法运算符是"/"符号,表示将左侧操作数除以右侧操作数. 2. 整数除法 在Java中,整数除法的结果是一个整数,即只保留除法的整数部分,舍去小数部分 ...

  8. 二进制文件转Hex和Wav文件转Hex的Java代码

    二进制文件转Hex 对于需要将二进制数据写入固件的场景(例如mp3文件), 需要将二进制文件表示为byte数组 import java.io.File; import java.io.FileInpu ...

  9. html+css:小米顶部菜单+二级菜单

    1.源码 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF- ...

  10. [BUUCTF][Web][极客大挑战 2019]EasySQL 1

    打开靶机对应的url 界面显示需要输入账号和密码 分别在两个输入框尝试加单引号尝试是否有sql注入的可能,比如 123' 发现两个框可以注入,因为报了个错误信息 You have an error i ...