Log4j-tag丢失
一、引言
最近有个线上日志丢失tag的问题,是组内封装了后置请求的拦截器把请求的响应结果存到ClickHouse里面去,但是日志总有一些tag丢失。
作者提出父级线程的threadlocal被清空,同事认为可能是threadlocal的弱引用在gc的时候被回收。两种想法其实都是有可能的,那就要一个个验证。这里要感谢ld和cbc的共同排查、讨论,最后锁定原因是父级线程的MDC被ar组件清空。
二、问题
1、架构背景
这里先讲讲问题后面架构的背景,由于需要按照特定的格式存一些日志数据到ck,组内封装的后置拦截器会使用slf4j的MDC.getCopyOfContextMap()获取请求被存储在ThreadLocal里面的map,里面放了一些订单、人员信息的key-value,拿到这些数据再和一些响应参数存入ck。
集团的日志框架里面实现了servlet的Filter,在servlet容器初始化的时候会运行这个拦截器,把一部分埋点运行,但是会清空,保证进入业务处理的时候是干净的。真正用到的买点是在业务线程里面实现的,所以在业务处理完成到后置拦截器的过程中可能存在clear的操作。需要把框架整体结构研究下。
2、问题背景
在落数据的监控,看到有几列偶现是空的。

看代码确认这些空列都是从MDC取出来的。
三、分析
快速的讨论锁定了出问题的代码。
1、代码
这个代码是从父线程取出来日志信息,作为入参返回给子runnable,等到子runnable启动的时候,它就变成了子线程里面的threadlocal。
@Overridepublic void apply(PostResponseFilterArgs args) {try {Runnable logTask = () -> doLog(args);if (CommonLogConfig.logAsync()&& StringUtilsExt.isNotBlank(CommonLogConfig.logExecutor())) {TaskRunner.create(LogTagsWrap.wrap(logTask)).start(CommonLogConfig.logExecutor());} else {logTask.run();}} catch (Throwable e) {LOGGER.info(TITLE, e);}}
public static Runnable wrap(Runnable runnable) {Map<String, String> logTags = log.getCommonTags();return () -> {log.setCommonTags(logTags);runnable.run();};}private static void doLog(PostResponseFilterArgs args) {// 构建业务信息Map<String, String> map = LogUtil.buildService(args);// 设置MDC信息LogUtil.setMdc(map);// 记录CK日志Throwable throwable = args.executionResult().throwable();if (Objects.nonNull(throwable)) {LogUtil.error(map, throwable, CommonLogConfig.logScenario());} else {LogUtil.info(map, null, CommonLogConfig.logScenario());}}
2、浅拷贝✖️
作者一开始以为这里父级线程取出来的可能是浅拷贝,然后在子线程启动之前被其他请求使用了,导致并发清空。但是追溯下去,MDC使用的是深拷贝拿到日志信息。
@Overridepublic Map<String, String> getCopy() {final StringMap map = localMap.get();return map == null ? new HashMap<>() : map.toMap();}@Overridepublic Map<String, String> getCopy() {final Map<String, String> map = localMap.get();return map == null ? new HashMap<>() : new HashMap<>(map);}
public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {int s = m.size();if (s > 0) {if (table == null) { // pre-sizefloat ft = ((float)s / loadFactor) + 1.0F;int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);if (t > threshold)threshold = tableSizeFor(t);}else if (s > threshold)resize();for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {K key = e.getKey();V value = e.getValue();putVal(hash(key), key, value, false, evict);}}}
3、子级threadlocal在gc时回收✖️
不过子线程里面一定是在set mdc信息的时候是空的,也就引出来开头作者和同事的两个猜想。threallocal作者感觉是不太可能的,因为线程执行完才算是强引用链结束,但是还是写个测试方法试一试。
使用system.gc强制回收,看看会不会把threadlocal给回收,结果并没有。这里要设置启动参数,不然system.gc不一定会执行。

public static void main(String[] args) {List<Task> list = new ArrayList<>();for (int i = 0; i < 10000;i++) {
System.out.println(i + "start");Map<String, String> md = new HashMap<>();md.put("orderid",i + "666");// 设置MDC信息MDC.setContextMap(md);int finalI = i;Runnable logTask = () -> {
// 构建SOA信息Map<String, String> map = new HashMap<>();map.put("soa", finalI + "666");System.gc();// 设置MDC信息LogUtil.setMdc(map);System.out.println(map);if (map.size() == 1) {
System.out.println(finalI + "soaerror");}
};Task t = TaskRunner.create(LogTagsWrap.wrap(logTask));list.add(t);System.out.println(i + "end");}
for (Task t : list) {
t.start(CommonLogConfig.logExecutor());}
}
4、父级MDC信息清空☑️
那么接下来就是看会不会清空父级线程的threadlocal了,作者把框架整体看了一遍。感觉出问题的地方还不少,就看这些拦截器是不是会异步走清理。

cbc使用过arthas进行线上诊断,作者也学习了一波,使用watch对全限定名的类方法进行字节码动态生成,打印方法的出入参便于排查。
但是作者这里的情况比较特殊,由于需要确定父级丢失还是子级的gc回收,需要在两个方法里面取出MDC的log信息,在为空的时候打印出来。作者和cbc折腾了许久,比较复杂的监控arthas还是不好用的。就在这时候ld给出了新的方向。
5、ar组件清空父级MDC信息☑️
他发现出问题的机器ip固定在某两台机器上面,于是我们开始分析这两天机器与其他机器有什么不同。cbc之前接触过测开组做了一个ar组件,使用java-agengt方法在服务发布时注入,这是我们观察不到但是有可能进行拦截增强的代码。
于是我们开始研究他的源码,果然这个组件使用了 SerivceInstrumentation extends TypeInstrumentation类进行字节码增强,并且是在业务处理的方法外层进行增强,比后置拦截器早。
@Overrideprotected ElementMatcher<TypeDescription> typeMatcher() {return named("server.OperationHandler");}private static void recordMocker(Mocker mocker) {// 业务处理MDC.clear();}
他的过程可以画成这样。

关掉ar组件之后,可以看到没有再发生有问题的ck日志记录,了解到ar组件的执行频率是一小时10次,这也说明了为什么我们发现的监控是部份发生,其实这个组件导致了必然的结果。
一切的偶然总有必然的原因!
四、总结
再次抒感:一切的偶然总有必然的原因!这次的问题告诉了我们,问题不一定出现在看得见的代码,java-ageng之类看不见的代码很多时候是另外一个方向。
这里再次感谢ld和cbc的协作分析,合作无间!
相关文章:
Log4j-tag丢失
一、引言 最近有个线上日志丢失tag的问题,是组内封装了后置请求的拦截器把请求的响应结果存到ClickHouse里面去,但是日志总有一些tag丢失。 作者提出父级线程的threadlocal被清空,同事认为可能是threadlocal的弱引用在gc的时候被回收。两种想…...
代码随想录算法训练营第五十六天|1143.最长公共子序列 ● 1035.不相交的线 ● 53. 最大子序和 动态规划
1143. 最长公共子序列 int longestCommonSubsequence(char * text1, char * text2){int len1 strlen(text1);int len2 strlen(text2);int dp[len11][len21];for (int i 0; i < len1; i){for (int j 0; j < len2; j){dp[i][j] 0;}}for (int i 1; i < len1; i){f…...
虚拟机和Windows的文件传输
拖拽/复制粘贴 直接将虚拟机linux系统的文件拖曳到windows桌面,或者直接将windows的文件拖曳到虚拟机linux系统当中,可以实现文件传输。当然复制粘贴方式也可以,但是前提是需要下载安装好VMware tools。 共享文件夹 概念:在Win…...
leetcode分类刷题:二叉树(八、二叉搜索树特有的自顶向下遍历)
二叉搜索树是一个有序树:每个二叉树都满足左子树上所有节点的值均小于它的根节点的值,右子树上所有节点的值均大于它的根节点的值;利用该性质,可以实现二叉搜索树特有的自顶向下遍历 700. 二叉搜索树中的搜索 思路1、自顶向下的遍…...
Vue 插槽 组件插入不固定内容
定义好一个组件,如果想插入图片或视频这非常不好的控制应该显示什么,这个时候可以使用插槽插入自定义内容 默认插槽 <Login><template><h1>我是插入的内容</h1></template></Login >组件 <slot></slot>…...
webpack打包时配置环境变量
webpack打包时配置环境变量 一、常规环境变量配置1. 使用webpack.DefinePlugin定义全局常量2. 在Vue静态页面中使用该环境变量 二、纯静态文件配置环境变量1. 使用npm或yarn安装html-webpack-plugin2. 在Webpack配置中引入并使用插件3. 使用htmlwebpackplugin.options方式配置环…...
【c++|opencv】一、基础操作---3.访问图像元素
every blog every motto: You can do more than you think. https://blog.csdn.net/weixin_39190382?typeblog 0. 前言 访问图像元素 1. 访问图像像素 1.1 访问某像素 //灰度图像: image.at<uchar>(j, i) //j为行数,i为列数 //BGR彩色图像 i…...
机器视觉3D项目评估的基本要素及测量案例分析
目录 一. 检测需求确认 1、产品名称:【了解是什么产品上的零件,功能是什么】 2、*产品尺寸:【最大兼容尺寸】 3、*测量项目:【确认清楚测量点位】 4、*精度要求:【若客户提出的精度值过大或者过小,可以和客…...
力扣日记10.31-【栈与队列篇】前 K 个高频元素
力扣日记:【栈与队列篇】前 K 个高频元素 日期:2023.10.31 参考:代码随想录、力扣 347. 前 K 个高频元素 题目描述 难度:中等 给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意…...
TensorFlow案例学习:简单的音频识别
前言 以下内容均来源于官方教程:简单的音频识别:识别关键字 音频识别 下载数据集 下载地址:http://storage.googleapis.com/download.tensorflow.org/data/mini_speech_commands.zip 可以直接浏览器访问下载。 下载完成后将其解压到项目…...
css小程序踩坑记录
写标签设置距离 一直设置不动 写个双层 设置动了 神奇 好玩...
Android sqlite分页上传离线订单后删除
1、判断订单表的的总数是否大于0,如果大于0开始上传订单 public int getOrderCount() {String query "SELECT COUNT(*) FROM " TABLE_NAME;Cursor cursor db.rawQuery(query, null);int count 0;if (cursor.moveToFirst()) {count cursor.getInt(0);…...
Flask基本教程以及Jinjia2模板引擎简介
flask基本使用 直接看代码吧,非常容易上手: # 创建flask应用 app Flask(__name__)# 路由 app.route("/index", methods[GET]) def index():return "FLASK:欢迎访问主页!"if __name__ "__main__"…...
Django实战项目-学习任务系统-兑换物品管理
接着上期代码框架,开发第5个功能,兑换物品管理,再增加一个学习兑换物品表,主要用来维护兑换物品,所需积分,物品状态等信息,还有一个积分流水表,完成任务奖励积分,兑换物品…...
jmeter和postman你选哪个做接口测试?
软件测试行业做功能测试和接口测试的人相对比较多。在测试工作中,有高手,自然也会有小白,但有一点我们无法否认,就是每一个高手都是从小白开始的,所以今天我们就来谈谈一大部分人在做的接口测试,小白变高手…...
mac版本 Adobe总是弹窗提示验证问题如何解决
来自: mac软件下载macsc站 mac电脑使用过程中总是弹出Adobe 的弹窗提示,尤其是打开Adobe的软件,更是频繁的弹出提示: Your Adobe app is not genuine. Adobe reserves the right to disable this software after a 0 grace period…...
钡铼技术ARM工控机在机器人控制领域的应用
ARM工控机是一种基于ARM架构的工业控制计算机,用于在工业自动化领域中进行数据采集、监控、控制和通信等应用。ARM(Advanced RISC Machine)架构是一种低功耗、高性能的处理器架构,广泛应用于移动设备、嵌入式系统和物联网等领域。…...
HTML+CSS+JS实现计算器
🙈作者简介:练习时长两年半的Java up主 🙉个人主页:程序员老茶 🙊 ps:点赞👍是免费的,却可以让写博客的作者开心好久好久😎 📚系列专栏:Java全栈,…...
Git工作原理和常见问题处理方案
博客定位Git工作区域工作区域划分暂存区设计目的 Git基本操作核心操作初始化和配置指令 HEAD指针Git版本回滚指令介绍reset模式reset hard使用场景reset soft使用场景reset mixed使用场景reset使用注意事项checkout使用场景 Git分支管理什么是分支分支应用场景分支相关指令被合…...
C++-实现一个简单的菜单程序
C-实现一个简单的菜单程序 1,if-else语句实现1.1,代码实现1.2,功能检测 2,switch语句实现2.1,代码实现2.2,功能检测 1,if-else语句实现 实现一个简单的菜单程序,运行时显示"Men…...
【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15
缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下: struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...
Frozen-Flask :将 Flask 应用“冻结”为静态文件
Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是:将一个 Flask Web 应用生成成纯静态 HTML 文件,从而可以部署到静态网站托管服务上,如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...
OkHttp 中实现断点续传 demo
在 OkHttp 中实现断点续传主要通过以下步骤完成,核心是利用 HTTP 协议的 Range 请求头指定下载范围: 实现原理 Range 请求头:向服务器请求文件的特定字节范围(如 Range: bytes1024-) 本地文件记录:保存已…...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...
有限自动机到正规文法转换器v1.0
1 项目简介 这是一个功能强大的有限自动机(Finite Automaton, FA)到正规文法(Regular Grammar)转换器,它配备了一个直观且完整的图形用户界面,使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...
鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南
1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发,使用DevEco Studio作为开发工具,采用Java语言实现,包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...
面向无人机海岸带生态系统监测的语义分割基准数据集
描述:海岸带生态系统的监测是维护生态平衡和可持续发展的重要任务。语义分割技术在遥感影像中的应用为海岸带生态系统的精准监测提供了有效手段。然而,目前该领域仍面临一个挑战,即缺乏公开的专门面向海岸带生态系统的语义分割基准数据集。受…...
基于Java+VUE+MariaDB实现(Web)仿小米商城
仿小米商城 环境安装 nodejs maven JDK11 运行 mvn clean install -DskipTestscd adminmvn spring-boot:runcd ../webmvn spring-boot:runcd ../xiaomi-store-admin-vuenpm installnpm run servecd ../xiaomi-store-vuenpm installnpm run serve 注意:运行前…...
【Linux系统】Linux环境变量:系统配置的隐形指挥官
。# Linux系列 文章目录 前言一、环境变量的概念二、常见的环境变量三、环境变量特点及其相关指令3.1 环境变量的全局性3.2、环境变量的生命周期 四、环境变量的组织方式五、C语言对环境变量的操作5.1 设置环境变量:setenv5.2 删除环境变量:unsetenv5.3 遍历所有环境…...
论文阅读:LLM4Drive: A Survey of Large Language Models for Autonomous Driving
地址:LLM4Drive: A Survey of Large Language Models for Autonomous Driving 摘要翻译 自动驾驶技术作为推动交通和城市出行变革的催化剂,正从基于规则的系统向数据驱动策略转变。传统的模块化系统受限于级联模块间的累积误差和缺乏灵活性的预设规则。…...
