log4j并发打印日志导致线程Block问题排查记录

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了log4j并发打印日志导致线程Block问题排查记录相关的知识,希望对你有一定的参考价值。

参考技术A 查看阻塞线程,大部分线程阻塞于ThrowableProxy.toExtendedStackTrace方法。

1.锁对象有两个队列,等待队列+同步队列。线程被wait之后进入等待队列,notify之后进入同步队列,随后获取到锁进行业务操作。如果在notify之后,有多个线程竞争,线程没有获取到锁,则进入block状态,等待下次notify后重新竞争锁。

1.下游抛少量异常时,首先解析异常堆栈时是用的反射,不会生成代理类,就没有类解析这步操作。
2.即使大量反射导致生成了代理类,但如果异常较少,对于代理类的解析竞争线程少,会造成阻塞但是现象不明显。

1.日志线程会通过反射获取异常类的包名,类名,行号,jar版本号等信息。反射两种方式实现,第一种jni native方法调用,第二种生成代理类GeneratorMethodAccessor,生成包含目标方法的代理类,通过DelegatingClassLoader加载后调用。默认是15次调用后通过类加载器处理反射,可通过sun.reflect.noInflation来设置。 初次调用使用native方法效率更高,代理类需要初始化。但是长期调用,代理类效率高是jni的10-40倍左右。
2.当大量异常时,jvm会优化通过类加载器来反射获取异常类信息。但是由于log4j bug,无法解析代理类,导致线程阻塞。

获取异常信息的包名,类名,报错方法行号,版本号等信息。

log4j加载类有3个流程。
1.双亲委派加载。通过BootStrapClassLoader,ExtensionClassLoader,ApplicationClassLoader顺序加载,但是都没加载成功。并且由于该步骤需要sychronized(class.name),导致大量线程在此阻塞。抛异常后到第二步。

2.Class.forName()。由于没有完整的异常类class文件,导致无法正常加载。
3.使用当前类的类加载器加载。当前类加载器为UrlClassLoader,无法加载反射对应的类。只有DelegatingClassLoader能加载。
最后返回了null。

始终没有加载成功过,所以缓存中没有对应的类实例,每次都在ClassLoader.loadClass这个地方阻塞,同步加载,其他线程被block。

log4j 2.7队列满了之后,默认的处理策略是由主线程来处理日志,同步写日志。

1.已知异常日志,只打印错误信息名,e.getMessage(),不打印堆栈信息。

破除问题的触发条件之一即可:
高并发打印异常栈日志(QPS>50);打日志增加限流
异常栈中包含反射相关的类(RPC中间件、aop); 异常堆栈中过滤掉反射类。MethodGeneratorAccessor,DelegatingMethodAccessor,NativeMethodAccessor

以上是关于log4j并发打印日志导致线程Block问题排查记录的主要内容,如果未能解决你的问题,请参考以下文章

日志导致线程Block的这些坑,你不得不防

java日志导致线程Block的这些坑,你不得不防

记一次log4j不打印日志的踩坑记

JVM反调调用优化,导致发生大量异常时log4j2线程阻塞

SpringBoot日志跟踪

syslog日志打印