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

现象:对固定接口测试环境做压测。qps保持在单机37左右,各服务未出现异常。但是压测持续30分钟左右时,突然出现大量线程block告警。

查看阻塞线程,大部分线程阻塞于ThrowableProxy.toExtendedStackTrace方法。

问题0:线程阻塞概念,一般有哪些原因?

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

问题1:正常情况下,下游抛异常,上游日志线程是否会阻塞?

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

问题2:下游短时间内抛大量异常,上游线程为什么会阻塞?

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

问题3:为什么日志线程需要去加载类?

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

问题4:为什么日志线程无法加载异常类?

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


image.png

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


image.png

问题5:为何会一直重复加载类,而不是从缓存里取?

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

问题6:异步打印日志,为何还会出现阻塞?

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

问题6:高并发打日志导致线程阻塞,CPU飙升。问题如何解决?

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

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

你可能感兴趣的:(log4j并发打印日志导致线程Block问题排查记录)