使用 MDC 实现日志链路跟踪,包教包会!
在微服务环境中,我们经常使用 Skywalking、Spring Cloud Sleut 等去实现整体请求链路的追踪,但是这个整体运维成本高,架构复杂,本次我们来使用 MDC 通过 Log 来实现一个轻量级的会话事务跟踪功能,需要的朋友可以参考一下。
1.1 应用效果图
我们知道了 MDC 的好处后,其实在用户从第一时间调用请求时候,我们其实可以将请求增加 traceid 一并返回,这样用户反馈时候,我们直接用 traceid 就可以全链路追踪到所有请求的情况了,做到信息的闭环。
请求效果图:
LOGBOOK 效果图:
2、关键思路
2.1 MDC
日志追踪目标是每次请求级别的,也就是说同一个接口的每次请求,都应该有不同的 traceId。每次接口请求,都是一个单独的线程,所以自然我们很容易考虑到通过 ThreadLocal 实现上述需求。考虑到 log4j 本身已经提供了类似的功能 MDC,所以直接使用 MDC 进行实现。
关于 MDC 的简述
MDC(Mapped Diagnostic Context)是一个映射,用于存储运行上下文的特定线程的上下文数据。因此,如果使用 log4j 进行日志记录,则每个线程都可以拥有自己的 MDC,该 MDC 对整个线程是全局的。属于该线程的任何代码都可以轻松访问线程的 MDC 中存在的值。
API 说明
-
clear()
=> 移除所有 MDC -
get (String key)
=> 获取当前线程 MDC 中指定 key 的值 -
getContext()
=> 获取当前线程 MDC 的 MDC -
put(String key, Object o)
=> 往当前线程的 MDC 中存入指定的键值对 -
remove(String key)
=> 删除当前线程 MDC 中指定的键值对
3、目标
-
需要一个全服务唯一的 id,即 traceId,如何保证?
-
traceId 如何在服务内部传递?
-
traceId 如何在服务间传递?
-
traceId 如何在多线程中传递?
4、实现方式
4.1 需要一个全服务唯一的 id,即 traceId,如何保证?
使用最简单的 uuid 即可。复杂的话可以配置 Redis、雪花算法等方式。本次分享选最简单 uuid 生成 traceId 的方式。
4.2 traceId 如何在服务间传递?
1)在 XML 的日志格式中添加 %X{traceId}
配置。
<appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender"><layout class="org.apache.log4j.PatternLayout"><param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} [%X{traceId}] [%p] %l[%t]%n%m%n" /></layout>
</appender>
2)新增拦截器,拦截所有请求,从 header 中获取 traceId 然后放到 MDC 中,如果没有获取到,则直接用 UUID 生成一个。
@Slf4j
@Component
public class LogInterceptor implements HandlerInterceptor {private static final String TRACE_ID = "traceId";@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,Exception arg3) throws Exception {}@Overridepublic void postHandle(HttpServletRequest request,HttpServletResponse response, Object handler, ModelAndView arg3) throws Exception {}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {String traceId = request.getHeader(TRACE_ID);if (StringUtils.isEmpty(traceId)) {MDC.put(TRACE_ID, UUID.randomUUID().toString());} else {MDC.put(TRACE_ID, traceId);}return true;}
}
3)配置拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {@Resourceprivate LogInterceptor logInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(logInterceptor).addPathPatterns("/**");}
}
4.3 traceId 如何在服务间传递?
封装 HTTP 工具类,把 traceId 加入头中,带到下一个服务。
@Slf4j
public class HttpUtils {public static String get(String url) throws URISyntaxException {RestTemplate restTemplate = new RestTemplate();MultiValueMap<String, String> headers = new HttpHeaders();headers.add("traceId", MDC.get("traceId"));URI uri = new URI(url);RequestEntity<?> requestEntity = new RequestEntity<>(headers, HttpMethod.GET, uri);ResponseEntity exchange = restTemplate.exchange(requestEntity, String.class);if (exchange.getStatusCode().equals(HttpStatus.OK)) {log.info("send http request success");}return exchange.getBody();}
}
4.4 traceId 如何在多线程中传递?
Spring 项目也使用到了很多线程池,比如 @Async 异步调用,Zookeeper 线程池、 Kafka 线程池等。不管是哪种线程池都大都支持传入指定的线程池实现,拿 @Async 举例:
原理为:
MDC 底层使用 ThreadLocal 来实现,那根据 ThreadLocal 的特点,它是可以让我们在同一个线程中共享数据的,但是往往我们在业务方法中,会开启多线程来执行程序,这样的话 MDC 就无法传递到其他子线程了。这时,我们需要使用额外的方法来传递存在 ThreadLocal 里的值。
MDC 提供了一个叫 getCopyOfContextMap 的方法,很显然,该方法就是把当前线程 ThreadLocal 绑定的Map获取出来,之后就是把该 Map 绑定到子线程中的ThreadLocal 中了。
改造 Spring 的异步线程池,包装提交的任务。
@Slf4j
@Component
public class TraceAsyncConfigurer implements AsyncConfigurer {@Overridepublic Executor getAsyncExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(8);executor.setMaxPoolSize(16);executor.setQueueCapacity(100);executor.setThreadNamePrefix("async-pool-");executor.setTaskDecorator(new MdcTaskDecorator());executor.setWaitForTasksToCompleteOnShutdown(true);executor.initialize();return executor;}@Overridepublic AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {return (throwable, method, params) -> log.error("asyc execute error, method={}, params={}", method.getName(),Arrays.toString(params));}public static class MdcTaskDecorator implements TaskDecorator {@Overridepublic Runnable decorate(Runnable runnable) {Map<String, String> contextMap = MDC.getCopyOfContextMap();return () -> {if (contextMap != null) {MDC.setContextMap(contextMap);}try {runnable.run();} finally {MDC.clear();}};}}
}public class MDCLogThreadPoolExecutor extends ThreadPoolExecutor {public MDCLogThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue workQueue) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);}@Overridepublic void execute(Runnable command) {super.execute(MDCLogThreadPoolExecutor.executeRunable(command, MDC.getCopyOfContextMap()));}@Overridepublic Future<?> submit(Runnable task) {return super.submit(MDCLogThreadPoolExecutor.executeRunable(task, MDC.getCopyOfContextMap()));}@Overridepublic Future submit(Callable callable) {return super.submit(MDCLogThreadPoolExecutor.submitCallable(callable, MDC.getCopyOfContextMap()));}public static Runnable executeRunable(Runnable runnable, Map<String, String> mdcContext) {return new Runnable() {@Overridepublic void run() {if (mdcContext == null) {MDC.clear();} else {MDC.setContextMap(mdcContext);}try {runnable.run();} finally {MDC.clear();}}};}private static Callable submitCallable(Callable callable, Map<String, String> context) {return () -> {if (context == null) {MDC.clear();} else {MDC.setContextMap(context);}try {return callable.call();} finally {MDC.clear();}};}
}
接下来需要对 ThreadPoolTaskExecutor
的方法进行重写:
package com.example.demo.common.threadpool;import com.example.demo.common.constant.Constants;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;/*** MDC线程池* 实现内容传递* @author wangbo* @date 2021/5/13*/
@Slf4jpublic
class MdcTaskExecutor extends ThreadPoolTaskExecutor {@Overridepublic <T> Future<T> submit(Callable<T> task) {log.info("mdc thread pool task executor submit");Map<String, String> context = MDC.getCopyOfContextMap();return super.submit(() -> {T result;if (context != null) {// 将父线程的MDC内容传给子线程MDC.setContextMap(context);} else {// 直接给子线程设置MDCMDC.put(Constants.LOG_MDC_ID, UUID.randomUUID().toString().replace("-", ""));}try {// 执行任务result = task.call();} finally {try {MDC.clear();} catch (Exception e) {log.warn("MDC clear exception", e);}}return result;});}@Overridepublic void execute(Runnable task) {log.info("mdc thread pool task executor execute");Map<String, String> context = MDC.getCopyOfContextMap();super.execute(() -> {if (context != null) {// 将父线程的MDC内容传给子线程MDC.setContextMap(context);} else {// 直接给子线程设置MDCMDC.put(Constants.LOG_MDC_ID, UUID.randomUUID().toString().replace("-", ""));}try {// 执行任务task.run();} finally {try {MDC.clear();} catch (Exception e) {log.warn("MDC clear exception", e);}}});}
}
然后使用自定义的重写子类 MdcTaskExecutor
来实现线程池配置:
/*** 线程池配置* * @author wangbo* @date 2021/5/13*/
@Slf4j
@Configurationpublic
class ThreadPoolConfig {/** * 异步任务线程池 * 用于执行普通的异步请求,带有请求链路的MDC标志 */@Beanpublic Executor commonThreadPool() {log.info("start init common thread pool"); // ThreadPoolTaskExecutorexecutor = new ThreadPoolTaskExecutor();MdcTaskExecutor executor = new MdcTaskExecutor();// 配置核心线程数executor.setCorePoolSize(10);// 配置最大线程数executor.setMaxPoolSize(20);// 配置队列大小executor.setQueueCapacity(3000);// 配置空闲线程存活时间executor.setKeepAliveSeconds(120);// 配置线程池中的线程的名称前缀executor.setThreadNamePrefix("common-thread-pool-");// 当达到最大线程池的时候丢弃最老的任务executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());// 执行初始化executor.initialize();return executor;}/*** 定时任务线程池* 用于执行自启动的任务执行,父线程不带有MDC标志,不需要传递,直接设置新的MDC* 和上面的线程池没啥区别,只是名字不同*/@Beanpublic Executor scheduleThreadPool() {log.info("start init schedule thread pool");MdcTaskExecutor executor = new MdcTaskExecutor();executor.setCorePoolSize(10);executor.setMaxPoolSize(20);executor.setQueueCapacity(3000);executor.setKeepAliveSeconds(120);executor.setThreadNamePrefix("schedule-thread-pool-");executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());executor.initialize();return executor;}
}
5、扩展点
5.1 JSF 接口日志追踪的应用
项目中也运用到了大量的 JSF 接口,我们其实可以按照上述的思路进行服务间的传递。
调用端:
// todo 不能在filter里面这么用
RpcContext.getContext().setAttachment("user", "zhanggeng");
RpcContext.getContext().setAttachment(".passwd", "11112222");
// "."开头的对应上面的hide=truexxxService.yyy();// 再开始调用远程方法
// 重要:下一次调用要重新设置,之前的属性会被删除
RpcContext.getContext().setAttachment("user", "zhanggeng");
RpcContext.getContext().setAttachment(".passwd", "11112222");
// "."开头的对应上面的hide=truexxxService.zzz();
// 再开始调用远程方法
Provider 端:
1.filter 中直接获取,包括标记为 hidden 的参数。通过 Rpccontext 无法获取。
String consumerToken = (String) invocation.getAttachment(".passwd");
2.服务端业务代码中直接获取。
String user = RpcContext.getContext().getAttachment("user");
提示:调用链中的隐式传参。
❝注意:在调用链例如 A–>B–>C,A和B都要隐私传参的时候,由于是同一个线程,会出现数据污染。例如 A 发参数 P1 给 B,B 收到请求拿到 P1 同时要发参数 P2 给 C,那么 C 会直接拿到 P1、P2。这种情况,就要求 B 收到 P1,然后设置 P2 调用 C 之前,要求自己清空上下文数据(
❞RpcContext.getContext().clearAttachments();
)
5.2 接口返回值应用
我们知道了 MDC 的好处后,其实在用户从第一时间调用请求时候,我们其实可以将有误的请求增加 traceid 一并返回。这样用户反馈时候,我们直接用 traceid 就可以全链路追踪到所有请求的情况了,做到信息的闭环。
效果图:
6、备注
各位知道了日志追踪的原理,其实很多应用场景可以继续补充,例如 MQ,JD 的其他中间件也可以应用相同原理进行追踪。
其实,当了解了底层的原理后,我们其实就可以了解到 JD 监控中间件 PFinder 监控等中间件是如何做的了。
本次由于时间情况,就不进行扩展了,各位可以线下去了解 Skywalking 分布式链路追踪系统,就可以知道,万变不离其宗。
最后说一句(求关注!别白嫖!)
如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。
关注公众号:woniuxgg,在公众号中回复:笔记 就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!
相关文章:

使用 MDC 实现日志链路跟踪,包教包会!
在微服务环境中,我们经常使用 Skywalking、Spring Cloud Sleut 等去实现整体请求链路的追踪,但是这个整体运维成本高,架构复杂,本次我们来使用 MDC 通过 Log 来实现一个轻量级的会话事务跟踪功能,需要的朋友可以参考一…...

【成都信息工程大学】只考程序设计!成都信息工程大学计算机考研考情分析!
成都信息工程大学(Chengdu University of Information Technology),简称“成信大”,由中国气象局和四川省人民政府共建,入选中国首批“卓越工程师教育培养计划”、“2011计划”、“中西部高校基础能力建设工程”、四川…...

将单列数据帧转换成多列数据帧
文章目录 1. 查看数据文件2. 读取数据文件得到单例数据帧3. 将单列数据帧转换成多列数据帧 在本次实战中,我们的目标是将存储在HDFS上的以逗号分隔的文本文件student.txt转换为结构化的Spark DataFrame。首先,使用spark.read.text读取文件,得…...

信息学奥赛初赛天天练-20-完善程序-vector数组参数引用传递、二分中值与二分边界应用的深度解析
PDF文档公众号回复关键字:20240605 1 2023 CSP-J 完善程序1 完善程序(单选题,每小题 3 分,共计 30 分) 原有长度为 n1,公差为1等升数列,将数列输到程序的数组时移除了一个元素,导致长度为 n 的开序数组…...

推荐系统学习 一
参考:一文看懂推荐系统:召回08:双塔模型——线上服务需要离线存物品向量、模型更新分为全量更新和增量更新_数据库全量更新和增量更新流程图-CSDN博客 一文看懂推荐系统:概要01:推荐系统的基本概念_王树森 小红书-CSD…...
分库分表详解
文章目录 分库分表概述分库分表详解分库分表的策略分库分表的注意事项常用的分库分表中间件mysql单表达到多少数据量需要分库分表数据库分库分表缺点分表要停服吗,不停服怎么做 分库分表概述 分库分表是数据库架构设计中的一种常见策略,尤其是在面对大规…...
【java前端课堂】04_类的继承
类的继承 在Java中,继承是面向对象编程的四大基本特性之一,它允许我们根据一个已有的类来定义一个新的类,这个新的类继承了原有类的特性(属性和方法),并可以添加新的特性或修改原有特性。这样,…...
React nginx配置,一个端口代理多个项目(转发后找不到CSS,JS及图片资源问题解决)
场景: nginx 配置负载均衡,甲方只提供一个端口,一个域名地址 方法: 一个端口一个域名匹配多个应用 方法一: 依靠设备浏览器区分: 使用UserAgent头来识别用户的客户端, CDN监测vary头的信息,如果内容不一致…...

Unity协程详解
什么是协程 协程,即Coroutine(协同程序),就是开启一段和主程序异步执行的逻辑处理,什么是异步执行,异步执行是指程序的执行并不是按照从上往下执行。如果我们学过c语言,我们应该知道࿰…...

【iOS】UI学习(二)
目录 前言UIViewContorllerUIViewContorller基础UIViewContorller使用 定时器和视图移动UISwitch控件UIProgressView和UISlider总结 前言 本篇博客是笔者在学习UI部分内容时的成果和遇到的一些问题,既是我自己的学习笔记,也希望对你有帮助~ …...

React路由(React笔记之五)
本文是结合实践中和学习技术文章总结出来的笔记(个人使用),如有雷同纯属正常((✿◠‿◠)) 喜欢的话点个赞,谢谢! React路由介绍 现在前端的项目一般都是SPA单页面应用,不再是以前多个页面多套HTML代码项目了,应用内的跳转不需要刷新页面就能完成页面跳转靠的就是路由系统 R…...

调用讯飞星火API实现图像生成
目录 1. 作者介绍2. 关于理论方面的知识介绍3. 关于实验过程的介绍,完整实验代码,测试结果3.1 API获取3.2 代码解析与运行结果3.2.1 完整代码3.2.2 运行结果 3.3 界面的编写(进阶) 4. 问题分析5. 参考链接 1. 作者介绍 刘来顺&am…...

reduce过滤递归符合条件的数据
图片展示 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> </head><…...

Go微服务: 基于rocketmq:5.2.0搭建RocketMQ环境,以及示例参考
概述 参考最新官方文档:https://rocketmq.apache.org/zh/docs/quickStart/03quickstartWithDockercompose以及:https://rocketmq.apache.org/zh/docs/deploymentOperations/04Dashboard综合以上两个文档来搭建环境 搭建RocketMQ环境 1 ) 基于 docker-c…...

Wpf 使用 Prism 开发MyToDo应用程序
MyToDo 是使用 WPF ,并且塔配Prism 框架进行开发的项目。项目中进行了前后端分离设计,客户端所有的数据均通过API接口获取。适合新手入门学习WPF以及Prism 框架使用。 首页统计以及点击导航到相关模块功能待办事项增删改查功能备忘录增删改查功能登录注册…...

vue-Dialog 自定义title样式
展示结果 vue代码 <el-dialog :title"title" :visible.sync"classifyOpen" width"500px" :showClose"false" class"aboutDialog"> <el-form :model"classifyForm" :rules"classifyRules">…...
数据库主键设计
文章目录 前言1. 自增ID(Auto-Increment)2. GUID (Globally Unique Identifier)3. 雪花算法(Snowflake)处理时钟回拨的方法1. 简单等待2. 配置时钟回拨安全窗口3. 使用不同的机器 ID 小结稳定的雪花算法实现方案示例实现1. 定义雪…...

小熊家务帮day13-day14 门户管理(ES搜索,Canal+MQ同步,索引同步)
目录 1 服务搜索1.1 需求分析1.2 技术方案1.2.1 使用Elasticsearch进行全文检索(为什么数据没有那么多还要用ES?)1.2.2 索引同步方案1.2.2.1 Canal介绍1.2.2.1 Canal工作原理 1 服务搜索 1.1 需求分析 服务搜索的入口有两处: 在…...
Android8.1高通平台修改默认输入法
需求 安卓8.1 SDK原生的输入法只能打英文, 需要替换成中文输入法. 以高通平台为例, 其它平台也适用. 查看设备当前默认输入法 adb shell settings list secure | grep input 可以看到当前默认是LatinIME这个安卓原生输入法. default_input_methodcom.android.inputmethod.l…...

49. 字母异位词分组
思路:题目的意思是,将所有字母相同的字符串放到一个数组中 解题思路是:使用map,使用排序好的字符串作为key,源字符串作为value,就可以实现所有字母相同的字符串对应一个key vector<vector<string>> groupAnagrams(ve…...

IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...

网络六边形受到攻击
大家读完觉得有帮助记得关注和点赞!!! 抽象 现代智能交通系统 (ITS) 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 (…...
反向工程与模型迁移:打造未来商品详情API的可持续创新体系
在电商行业蓬勃发展的当下,商品详情API作为连接电商平台与开发者、商家及用户的关键纽带,其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息(如名称、价格、库存等)的获取与展示,已难以满足市场对个性化、智能…...

剑指offer20_链表中环的入口节点
链表中环的入口节点 给定一个链表,若其中包含环,则输出环的入口节点。 若其中不包含环,则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...
实现弹窗随键盘上移居中
实现弹窗随键盘上移的核心思路 在Android中,可以通过监听键盘的显示和隐藏事件,动态调整弹窗的位置。关键点在于获取键盘高度,并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...
uniapp 字符包含的相关方法
在uniapp中,如果你想检查一个字符串是否包含另一个子字符串,你可以使用JavaScript中的includes()方法或者indexOf()方法。这两种方法都可以达到目的,但它们在处理方式和返回值上有所不同。 使用includes()方法 includes()方法用于判断一个字…...
上位机开发过程中的设计模式体会(1):工厂方法模式、单例模式和生成器模式
简介 在我的 QT/C 开发工作中,合理运用设计模式极大地提高了代码的可维护性和可扩展性。本文将分享我在实际项目中应用的三种创造型模式:工厂方法模式、单例模式和生成器模式。 1. 工厂模式 (Factory Pattern) 应用场景 在我的 QT 项目中曾经有一个需…...
Python 高级应用10:在python 大型项目中 FastAPI 和 Django 的相互配合
无论是python,或者java 的大型项目中,都会涉及到 自身平台微服务之间的相互调用,以及和第三发平台的 接口对接,那在python 中是怎么实现的呢? 在 Python Web 开发中,FastAPI 和 Django 是两个重要但定位不…...
深度解析:etcd 在 Milvus 向量数据库中的关键作用
目录 🚀 深度解析:etcd 在 Milvus 向量数据库中的关键作用 💡 什么是 etcd? 🧠 Milvus 架构简介 📦 etcd 在 Milvus 中的核心作用 🔧 实际工作流程示意 ⚠️ 如果 etcd 出现问题会怎样&am…...
在Spring Boot中集成RabbitMQ的完整指南
前言 在现代微服务架构中,消息队列(Message Queue)是实现异步通信、解耦系统组件的重要工具。RabbitMQ 是一个流行的消息中间件,支持多种消息协议,具有高可靠性和可扩展性。 本博客将详细介绍如何在 Spring Boot 项目…...