记一次 Netty 堆外内存泄露排查过程

这篇文章对于排查使用了 netty 引发的堆外内存泄露问题,有一定的通用性,希望对你有所启发
 
背景最近在做一个基于 websocket 的长连中间件,服务端使用实现了 socket.io 协议(基于websocket协议,提供长轮询降级能力) 的 netty-socketio 框架,该框架为 netty 实现,鉴于本人对 netty 比较熟,并且对比同样实现了 socket.io 协议的其他框架,这个框架的口碑要更好一些,因此选择这个框架作为底层核心 。
任何开源框架都避免不了 bug 的存在,我们在使用这个开源框架的时候,就遇到一个堆外内存泄露的 bug,鉴于对 netty 比较熟,于是接下来便想挑战一下,找出那只臭虫(bug),接下来便是现象和排查过程,想看结论的同学可以直接拉到最后总结 。
 
现象某天早上突然收到告警,Nginx 服务端大量5xx
记一次 Netty 堆外内存泄露排查过程

文章插图
我们使用 nginx 作为服务端 websocket 的七层负载,5xx爆发通常表明服务端不可用 。由于目前 nginx 告警没有细分具体哪台机器不可用,接下来,到 cat(点评美团统一监控平台)去检查一下整个集群的各项指标,发现如下两个异常
记一次 Netty 堆外内存泄露排查过程

文章插图
某台机器在同一时间点爆发 gc,同一时间,jvm 线程阻塞
记一次 Netty 堆外内存泄露排查过程

文章插图
接下来,便开始漫长的 堆外内存泄露排查之旅行 。
 
排查过程阶段1: 怀疑是log4j2线程被大量阻塞,首先想到的是定位哪些线程被阻塞,最后查出来是 log4j2 狂打日志导致 netty 的 nio 线程阻塞(由于没有及时保留现场,所以截图缺失),nio 线程阻塞之后,我们的服务器无法处理客户端的请求,对nginx来说是5xx 。
接下来,查看 log4j2 的配置文件
记一次 Netty 堆外内存泄露排查过程

文章插图
发现打印到控制台的这个 Appender 忘记注释掉了,所以我初步猜测是因为这个项目打印的日志过多,而 log4j2 打印到控制台是同步阻塞打印的,接下来,把线上所有机器的这行注释掉,以为大功告成,没想到,过不了几天,5xx告警又来敲门了,看来,这个问题没那么简单 。
 
阶段2:可疑日志浮现接下来,只能硬着头皮去查日志,查看故障发生点前后的日志,发现了一处可疑的地方
记一次 Netty 堆外内存泄露排查过程

文章插图
在极短的时间内,狂打 failed to allocate64(bytes)of direct memory(...)日志(瞬间十几个日志文件,每个日志文件几百M),日志里抛出一个 netty 自己封装的OutOfDirectMemoryError,说白了,就是堆外内存不够用了,netty 一直在喊冤 。
堆外内存泄露,我去,听到这个名词就有点沮丧,因为这个问题的排查就像 c 语言内存泄露一样难以排查,首先想到的是,在 OOM 爆发之前,查看有无异常,然后查遍了 cat 上与机器相关的所有指标,查遍了 OOM 日志之前的所有日志,均未发现任何异常!这个时候心里开始骂了……
 
阶段3:定位OOM源但是没办法,只能看着这堆讨厌的 OOM 日志发着呆,妄图答案能够蹦到眼前 。一筹莫展之际,突然一道光在眼前一闪而过,在 OOM 下方的几行日志变得耀眼起来(为啥之前就没想认真查看日志?估计是被堆外内存泄露这几个词吓怕了吧==),这几行字是
....PlatformDepedeng.incrementMemory... 。我去,原来,堆外内存是否够用,是 netty 这边自己统计的,那是不是可以找到统计代码,找到统计代码之后我们就可以看到 netty 里面的对外内存统计逻辑了?于是,接下来翻翻代码,找到这段逻辑,在PlatformDepedent这个类里面
记一次 Netty 堆外内存泄露排查过程

文章插图
这个地方,是一个对已使用堆外内存计数的操作,计数器为 DIRECT_MEMORY_COUNTER,如果发现已使用内存大于堆外内存的上限(用户自行指定),就抛出一个自定义 OOM Error,异常里面的文本内容正是我们在日志里面看到的 。
接下来,验证一下是否这个函数是在堆外内存分配的时候被调用
记一次 Netty 堆外内存泄露排查过程

文章插图
果然,在 netty 每次分配堆外内存之前,都会计数,想到这,思路开始慢慢清晰起来,心情也开始变好 。
 
阶段4:反射进行堆外内存监控既然 cat 上关于堆外内存的监控没有任何异常(应该是没有统计准确,一直维持在 1M),而这边我们又确认堆外内存已快超过上限,并且已经知道 netty 底层是使用哪个字段来统计的,那么接下来要做的第一件事情,就是反射拿到这个字段,然后我们自己统计 netty 使用堆外内存的情况 。


推荐阅读