Log4jLogEvent导致服务jvm fullGC 问题排查

现象:

应用服务某机器从23日下午14:20多分开始,报告fullGC。dump文件触发GC后未恢复,old区占用维持72%以上。且GC后无释放。

问题1:为什么jvm数据占用无法释放?

dump文件获取后分析,大部分被log4j组件占用。

从上图可见,持有内存最多的对象指向 Log4jLogEvent 和 AsyncAppender。

通过 右键 -> List objects -> with outgoing references 和 右键 -> List objects -> with incoming references,可以分别分析类的实例本身持有的属性,和类的实例被哪些其他实例持有。前者帮助确定是什么对象过多,后者帮助确定这些对象被谁生成,为何存在。

分析 AsyncAppender的outgoing references,共6个Appender,其中一个Appender通过queue字段持有了1025个Log4jLogEvent,占用了2.5G左右的内存,通过name可以定位到是业务代码中定义的某上报日志的Appender。

从占用20MB左右内存的Log4jLogEvent中随机挑选一个,show gc roots 如下。

 可见大对象被3类对象持有:

  1. this$0 com.???.???Appender$AsyncThread
  2. this$0 org.apache.logging.log4j.core.appender.AsyncAppender$AsyncThread
  3. configuration org.apache.logging.log4j.core.LoggerContext

其中前两个分别是业务的本地文件Appender和日志中心Appender(AsyncAppender)。三类对象都通过config/configuration变量持有了一个Log4jConfiguration的变量。经学习得知,该变量为log4j的根configuration(从系统的log4j.xml加载生成)。该变量持有的appenders是一个ConcurrentMap<String, Appender>,是日志AppenderName和Appender实体的map。其中每个Appender通过queue变量,持有了自己这个Appender对应的BlockingQueue<Log4jLogEvent>,用于维护需要打印的日志事件。对AsyncAppender,持有的是ArrayBlockingQueue。

至此,得知无法GC释放内存原因:由于所有的日志打印线程显式持有config,进而持有所有Appender对象及其内部的queue对象。其中 "ScribeAsyncAppender"的queue被大日志event占满(1024个),导致内存持有较多。

问题2:为什么"ScribeAsyncAppender"的queue里的日志event没有被消费或清理?

经学习,log4j2的基本工作流程如下:

AsyncThread负责从queue中阻塞take事件(对发生异常的业务来说是Log4jLogEvent),消费并完成日志输出。所以在正常情况下,AsyncAppender$AsyncThread的状态应该是Runnable,Running或Waiting。

在此次业务声明的“ScribeAsyncAppender”中,具体流程为:

  1. 业务代码打印时,生成logEvent,给对应的Appender提交任务(org.apache.logging.log4j.core.appender.AsyncAppender#append)。
  2. append内部通过transfer方法,将event提交给AsyncAppender中的queue。
  3. 对提交失败场景,如果配置为阻塞提交(blocking==true),由业务阻塞提交任务到日志平台(经常导致业务线程block的问题)。
  4. 提交成功后,AsyncAppender依赖内部类(AsyncThread)进行queue任务消费,提交及打印(queue消费和日志打印都依赖此线程)。
  5. 执行过程会获取Appender内部,真正执行的appenders的append方法。结合MAT可知此处为ScribeAppender,对应的发送日志方法为org.apache.logging.log4j.scribe.appender.ScribeAppender#sendLogEntry,是一个synchronized全局锁方法。

具体代码如下:

image.png

image.png

综上,推测是由于"ScribeAsyncAppender"的日志输出线程异常退出,导致Appender中的queue无法消费,导致内存占用无法清空。

使用jstack分析进程的线程情况,发现其他日志Appender的守护线程均打印了出来,状态均为Waiting。但"ScribeAsyncAppender"的AsyncThread未能发现,推测已经流转到了Terminated。

结合MAT分析的堆栈进行辅助确认(下图),ScribeAsyncAppender对象持有的thread,threadStatus为2(native jdk的赋值,含义取决于机器上运行的jvm版本及其实现对此字段的写入),但另一正常运作的AsyncAppender的守护线程状态为WAITING,threadStatus显示为657。可以推断得那个线程至少不是WAITING状态。

image.png image.png

且当前业务的日志message内容极大(业务不合理),导致日志提交到logAgent时发生异常的概率较大。推断大概率是由于上报日志过程异常,导致消费queue的日志线程出现异常后关闭,AsyncScribeAppender的queue没有消费线程,逐渐填满。(尽管从代码看,上报日志的AsyncThread,在while的代码块内有大量的try-catch,目的是保障守护线程不会被异常终结)

问题3:AsyncAppender中的thread 到底发生了什么导致异常退出?

从代码结合dump文件(可知全部对象及其内部字段的实际类型),对AsyncThread尝试分析,未得到结论。AsyncAppender内部的AsyncThread.run()内部大量的try-catch,保证异常不打断线程;appenders里真正打印日志的AppendControl是ScribeAppender,代码量较多,且如果没有异常日志,不知道是哪行可能出错。

回头从机器的日志中尝试找到异常日志,在业务日志目录的上一层,找到了jvm打印出的关键日志。

其中指出了包括这两个日志Appender对应的AsyncThread,以及若干个业务线程的异常来源:java.lang.OutOfMemoryError。其中前4个异常打印了stack,后续异常(含AsyncAppender-ScribeAsyncAppender)的error stack被jvm优化掉了,未打印。

Exception in thread "???Appender-mainLogFile" java.lang.OutOfMemoryError: Java heap space
		at java.util.Arrays.copyOf(Arrays.java:3332)
		....
Exception in thread "pool-35-thread-11" java.lang.OutOfMemoryError: Java heap space
		at java.util.Arrays.copyOf(Arrays.java:3332)
		....
Exception in thread "pool-35-thread-6" java.lang.OutOfMemoryError: Java heap space
		at java.util.Arrays.copyOf(Arrays.java:3332)
		....
Exception in thread "pool-35-thread-5" java.lang.OutOfMemoryError: Java heap space
		at java.util.Arrays.copyOf(Arrays.java:3332)
		....
Exception in thread "AsyncAppender-ScribeAsyncAppender" java.lang.OutOfMemoryError: Java heap space
2022-06-23 11:13:58,569 pool-35-thread-7 ERROR Appender ScribeAsyncAppender is unable to write primary appenders. queue is full
Exception in thread "pool-35-thread-17" java.lang.OutOfMemoryError: Java heap space
Exception in thread "pool-35-thread-18" java.lang.OutOfMemoryError: Java heap space
Exception in thread "pool-35-thread-21" java.lang.OutOfMemoryError: Java heap space
Exception in thread "pool-35-thread-20" java.lang.OutOfMemoryError: Java heap space

由于是Error,包括“pool-35-thread-?”的来源(ThreadPoolExecutor)和AsyncAppender中的线程都没有做捕捉(当然也不应该捕捉)。该Error打断了线程执行,导致线程中断退出。结合异常栈可以定位代码报错位置。

image.png

解决方案:

由于业务的特殊性,代码中有业务大对象(如单个Map持有几千个实体等),本身是松散的小pojo组成,但序列化成一条日志会导致组成单个超大对象,导致日志线程、业务线程均有OOM。

目前先将service层日志取消掉,看下后续业务表现。

猜你喜欢

转载自juejin.im/post/7114331850708877325