一、需求背景
1.某日,某同事根据日志ID排查生产环境问题过程中,发现日志不全
2.经排查发现中间有很多线程为ForkJoinPool.commonPool-worker的日志ID是丢失的
3.经代码review,发现这些丢失日志ID的log.info都是在parallelStream代码块中的
4.经了解,因为使用了parallelStream并发处理集合数据,这样能够提升接口性能,并且这个功能是jdk提供的,使用非常方便
以下为简化版的代码demo
经测试,发现在parallelStream的log.info无法正确的打印日志ID,那么在生产环境中,日志ID的丢失意味着日志排查问题变得困难,如下图所示
二、原因分析
为了提升接口性能,使用并发编程加快查询速度的确是比较不错的方案。
日志ID是使用org.slf4j.MDC进行传递的,经阅读源码,发现底层是使用ThreadLocal来进行数据存储的,多线程情况下,子线程无法访问到主线程的日志ID
并发编程项目中通常有2种用法:
1.使用线程池,如ThreadPoolExecutor、ThreadPoolTaskExecutor,可以自己new一个实例,这样的话可以通过自定义子类来做日志ID传递(这种方式已解决,具体可阅文章,这里就不详说了:https://www.toutiao.com/article/7126056949267268108)
2.使用ForkJoinPool,不是由自己new实例,而是jdk封装好的。例如CompletableFuture、list.parallelStream()、list.stream().parallel()等,底层都是使用了ForkJoinPool作为线程池实现(为了找到通用的解决方案,历时3个月)
>>> 那如何解决ForkJoinPool这个日志ID丢失的问题?
三、临时方案
当时无法在短时间内快速找到通用解决方案,所以想了1种临时方案:通过变量的方式传递到list.parallelStream()内部
如下图,这种方案需要改动代码
为什么要加subTraceId == null的判断?
答:主线程也会作为ForkJoinPool执行的一部分,主线程的日志ID不能清,否则后续的日志ID会丢失
四、寻找通用方案
1.方向错误,努力白费(辛酸经历,中途还想过放弃寻找通用方案)
方向1:参考ThreadPoolExecutor、ThreadPoolTaskExecutor,想办法自己new一个ForkJoinPool的实例,然后添加到spring容器使用
结果:最终发现ForkJoinPool是内部实现了1个静态的实例common从而告败
方向2:使用javaagent的方式修改ForkJoinPool或者其任务 ForkJoinTask等相关类的字节码,此想法来自一篇好文:一次「找回」TraceId的问题分析与过程思考(https://mp.weixin.qq.com/s/T7P2-tiroXWI9xd8FhsuFA)
结果:最终发现字节码框架Javassist底层对包名以java.开头的所有类进行了保护,而ForkJoinPool的包名java.util.concurrent,所以字节码修改方案也不通了
2.求助网友,集思广益
真的挺感谢这位‘新手村NPC’网友,给我提供了1个思路:竟然修改ForkJoinPool的思路走不通,那就尝试修改日志组件
3.修改日志组件
1.前面说过MDC底层是使用ThreadLocal来进行数据存储的,这就让我想到了阿里的TransmittableThreadLocal,能够在父子线程之间传递数据
先测试一下TransmittableThreadLocal能否在list.parallelStream()内部正确传递数据
注:需要在启动命令上加上:-javaagent:path/to/transmittable-thread-local-2.x.x.jar(替换为你maven路径中jar路径即可),否则会读取不到,因为TransmittableThreadLocal是基于字节码javaagent来实现的
结果:输出的值始终保持一致
2.修改MDC
MDC的ThreadLocal在哪里?通过断点的方式找到了MDCAdapter的实例LogbackMDCAdapter(其成员变量copyOnThreadLocal)
有没有办法在初始化时替换掉这个MDCAdapter的实例?MDCAdapter下面的MDCAdapter不是public的,只有getMDCAdapter方法而没有setMDCAdapter方法。
于是网上查询相关资料,方案是在项目中写个org.slf4j的包,然后通过以下方式赋值,因为同包下可访问(不得不说这操作挺骚的,佩服,这些知识点都忘了)
然后通过TtlMdcListener对TtlMDCAdapter进行实例化
logback.xml配置文件中增加TtlMdcListener的实例化
<contextListener class="com.ofpay.logback.TtlMdcListener"/>
这2个简单的类,既可以自己实现,也可以使用开源的maven,实现原理是一样的
<dependency><groupId>com.ofpay</groupId><artifactId>logback-mdc-ttl</artifactId><version>1.0.2</version></dependency>
于是,在完全不改业务代码的情况下,日志ID正确地传递下来了
测试结果:
注:需要在启动命令上加上:-javaagent:path/to/transmittable-thread-local-2.x.x.jar(替换为你maven路径中jar路径即可),否则会读取不到,因为TransmittableThreadLocal是基于字节码javaagent来实现的
怎么样?如果你觉得有用的话,还不快快收藏起来!!!
附:涉及的代码目录
github: https://github.com/897665787/springcloud-template
gitee:https://gitee.com/jq_di/springcloud-template
springcloud-template└── template-common └──src/main/resources └── logback-conf-base.xml-- 日志配置 └──pom.xml-- 引用logback-mdc-ttl└── template-web └──controller └── TraceIdController-- 日志ID测试demo