一次 Netty 堆外内存泄露问题排查

131 阅读8分钟

转载自博客:cloud.tencent.com/developer/a… 把上面文章的内容导出成word

一次Netty堆外内存泄露问题排查****

这篇文章对于排查使用了netty引发的堆外内存泄露问题,有一定的通用性,希望对你有所启发。

(一)背景****

最近在做一个基于websocket的长连中间件,服务端使用实现了socket.io协议(基于websocket协议,提供长轮询降级能力) 的netty-socketio框架,该框架为netty实现,鉴于本人对netty比较熟,并且对比同样实现了socket.io协议的其他框架,这个框架的口碑要更好一些,因此选择这个框架作为底层核心。

任何开源框架都避免不了bug的存在,我们在使用这个开源框架的时候,就遇到一个堆外内存泄露的bug,鉴于对netty比较熟,于是接下来便想挑战一下,找出那只臭虫(bug),接下来便是现象和排查过程,想看结论的同学可以直接拉到最后总结。

(二)现象****

某天早上突然收到告警,nginx服务端大量5xx。我们使用nginx作为服务端websocket的七层负载,5xx爆发通常表明服务端不可用。由于目前nginx告警没有细分具体哪台机器不可用,接下来,到cat(点评美团统一监控平台)去检查一下整个集群的各项指标,发现如下两个异常: 某台机器在同一时间点爆发gc,同一时间,jvm线程阻塞。

63c17f9a1f3a307daf3fff08aaa0acfb_ud9dvr9ic8.jpeg

396ab1e3e0a2aa5ca14a6f83fa8cf1e1_xvxj8y1hyh.jpeg 接下来,便开始漫长的堆外内存泄露排查之旅行。

(三)排查过程****

1.阶段1: 怀疑是log4j2线程被大量阻塞****

首先想到的是定位哪些线程被阻塞,最后查出来是log4j2狂打日志导致netty的nio线程阻塞(由于没有及时保留现场,所以截图缺失),nio线程阻塞之后,我们的服务器无法处理客户端的请求,对nginx来说是5xx。

68f6232d2b56d64011e25de26dc1d0f0_n2pzfsh8ks.jpeg 接下来,查看log4j2的配置文件,发现打印到控制台的这个appender忘记注释掉了,所以我初步猜测是因为这个项目打印的日志过多,而log4j2打印到控制台是同步阻塞打印的,接下来,把线上所有机器的这行注释掉,以为大功告成,没想到,过不了几天,5xx告警又来敲门了,看来,这个问题没那么简单。

2.阶段2:可疑日志浮现****

接下来,只能硬着头皮去查日志,查看故障发生点前后的日志,发现了一处可疑的地方。

在极短的时间内,狂打failed to allocate64(bytes)of direct memory(…) 日志(瞬间十几个日志文件,每个日志文件几百M),日志里抛出一个netty自己封装的OutOfDirectMemoryError ,说白了,就是堆外内存不够用了,netty一直在喊冤。

9078576887a004557a4bd30500b4416b_dccgzyt6d5.jpeg

堆外内存泄露,听到这个名词就有点沮丧,因为这个问题的排查就像c语言内存泄露一样难以排查,首先想到的是,在OOM爆发之前,查看有无异常,然后查遍了cat上与机器相关的所有指标,查遍了OOM日志之前的所有日志,均未发现任何异常!这个时候心里开始骂了……

3.阶段3:定位OOM源****

但是没办法,只能看着这堆讨厌的OOM日志发着呆,妄图答案能够蹦到眼前。一筹莫展之际,突然一道光在眼前一闪而过,在OOM下方的几行日志变得耀眼起来(为啥之前就没想认真查看日志?估计是被堆外内存泄露这几个词吓怕了吧==),这几行字是….PlatformDepedeng.incrementMemory()… 。

原来,堆外内存是否够用,是netty这边自己统计的,那是不是可以找到统计代码,找到统计代码之后我们就可以看到netty里面的对外内存统计逻辑了?于是,接下来翻翻代码,找到这段逻辑,在PlatformDepedent这个类里面。

1ba67b291ebee038caf1dd270a7224a4_i5dq0mokvy.jpeg

这个地方,是一个对已使用堆外内存计数的操作,计数器为DIRECT_MEMORY_COUNTER ,如果发现已使用内存大于堆外内存的上限(用户自行指定),就抛出一个自定义OOM Error,异常里面的文本内容正是我们在日志里面看到的。

47a3b42fdac66f709df750aac30749cc_hf7ojfkogl.jpeg

接下来,验证一下是否这个函数是在堆外内存分配的时候被调用,果然,在netty每次分配堆外内存之前,都会计数,想到这,思路开始慢慢清晰起来,心情也开始变好。

4.阶段4:反射进行堆外内存监控****

既然cat上关于堆外内存的监控没有任何异常(应该是没有统计准确,一直维持在1M),而这边我们又确认堆外内存已快超过上限,并且已经知道netty底层是使用哪个字段来统计的,那么接下来要做的第一件事情,就是反射拿到这个字段,然后我们自己统计netty使用堆外内存的情况。

堆外内存统计字段是DIRECT_MEMORY_COUNTER ,我们可以通过反射拿到这个字段,然后定期check这个值,就可以监控netty堆外内存的增长情况。

我们通过反射拿到这个字段,然后每隔一秒打印,我为什么要这样做?

因为,通过我们前面的分析,在爆发大量OOM现象之前,没有任何可疑的现象,那么只有两种情况,一种是突然某个瞬间分配了大量的堆外内存导致OOM,一种是堆外内存缓慢增长,到达某个点之后,最后一根稻草将机器压垮。这段代码加上去之后,打包上线。

91e5bde1fc04685132ab53656c792e70_r8qvexnzs9.jpeg

661c3cdb5b22d6ee99df781d95d8156d_sdx5y8imau.jpeg

5.阶段5:到底是缓慢增长还是瞬间飙升?****

代码上线之后,初始内存为16384k(16M),这是因为线上我们使用了池化堆外内存,默认一个chunk为16M,不必过于纠结。

没过一会,内存就开始缓慢飙升,并且没有释放的迹象,20几分钟之后,内存如下。

到了这里,猜测可能是前面提到的第二种情况,也就是内存缓慢增长造成的OOM,由于内存实在增长太慢,于是调整机器负载权重为其他机器的两倍,但是仍然是以几K级别在增长,这天刚好是周五,索性就过他个一个周末再开看。

过完一个愉快的周末之后,到公司第一时间便是连上跳板机,登录线上机器,开始tail -f继续查看日志,输完命令之后,怀着期待的心情重重的敲下了回车键。

果然不出所料,内存一直在缓慢增长,一个周末的时间,堆外内存已经飙到快一个G了,这个时候,我竟然想到了一句成语:只要功夫深,铁杵磨成针!虽然堆外内存几个K几个K的在增长,但是只要一直持续下去,总有把内存打爆的时候(线上堆外内存上限设置的是2G)。

到了这里,我又开始自问自答了:内存为啥会缓慢增长,伴随着什么而增长?因为我们的应用是面向用户端的websocket,那么,会不会是每一次有用户进来,交互完之后,然后离开,内存都会增长一些,然后不释放呢?带着这个疑问,我开始线下模拟。

8cef6feb091dae448a92131678e23143_s08ajsn2v3.jpeg

6.阶段6:线下模拟****

本地起好服务,把监控堆外内存的单位改为以B为单位(因为本地流量较小,打算一次一个客户端连接),另外,本地也使用非池化内存(内存数字较小,容易看出问题),这样,服务端启动之后,控制台打印信息如下。

在没有客户端接入的时候,堆外内存一直是0,在意料之中。接下来,怀着着无比激动的心情,打开浏览器,然后输入网址,开始我们的模拟之旅。

我们的模拟流程是:新建一个客户端链接->断开链接->再新建一个客户端链接->再断开链接。

如上图所示,一次connect和disconnect为一次连接的建立与关闭,上图绿色框框的日志分别是两次连接的生命周期。我们可以看到,内存每次都是在连接被关闭的的时候暴涨256B然后不释放,到了这里,问题进一步缩小,肯定是连接被关闭的时候,触发了框架的一个bug,这个bug在触发之前分配了256B的内存,然后bug触发,内存没有释放。问题缩小之后,接下来开始撸源码捉虫!

d77d7c32c5d8763131f55e81474ff0ab_lrnqahh3kw.jpeg

216311886167672166d292dae2376ae5_590e3e0e99.jpeg

1b40bf75a042d646a5856dc4c64b4948_ya06fawmy1.jpeg

7.阶段7:线下排查****

接下来,我将本地服务重启,开始完整的线下排查过程。将目光定位到netty-socketio这个框架的disconnect事件(客户端websocket连接关闭的时候回调用到这里),基本上可以确定是在disconnect事件前后申请的内存没有释放。

1d2221f74814884f77adc6320bc38099_f62r6gyarh.jpeg

452ad48b445d95be818941078cda995e_tq9328iisz.jpeg

aa2a165ddce285bcf015f44a4f7a95ed_r5r9r4edou.jpeg

372e75fc5123e83b582e28240761bdb2_jbjp3pil64.jpeg

597fff2eb78e30d7d80d1130ecd9f4b6_wrvk7ln92f.jpeg

d6730fba9023fb6ff9604bf8a4824601_5hvqc2zrva.jpeg

c54feef300a06e49e98f47bd1dc95332_uqpit6s307.jpeg

182a1450dc90a30f221d3119c5831788_9bwo640x42.jpeg