记一次Netty PooledByteBufAllocator引发的直接内存OOM事件
记一次Netty引发的直接内存OOM事件
目录
问题描述
近期在我们环境上遇到了一个关于直接内存OOM的问题,曾经是没有遇到过的:
根据使用人员的反馈,最近产品使用一段时间就会发生这个OOM,重启后又能好一阵子。
猜测大概的原因是近期我们的产品配置了一个多维列表视图,这个视图会发起大量的接口请求,其中一个接口返回了大量的数据以支持整个视图的渲染:
经过观察,每次访问这个视图时直接内存都会上涨,并且始终是100%,直到最大限制时出现OOM:
问题分析
初步分析
通过异常日志可以看到,这里正在申请4194304 bytes(也就是4MB)的直接内存,但目前已经申请了802890569 bytes(也就是765MB),而最大限制是805306368 bytes(768MB),因此发生OOM。
由于我这里启动应用程序时仅使用 -Xmx=768m指定了最大堆内存,而没有指定最大直接内存,HotSpot默认将最大堆内存作为最大直接内存限制,也就是768MB。
对于使用了这么多直接内存,并且是逐步增长的,而且增长了之后没有释放,于是本能地第一时间怀疑的是不是Netty在使用直接内存时没有调用ByteBuf的release方法释放内存。
堆栈快照分析
于是打算进一步看看内存明细,看看是不是有很多ByteBuf对象没有被回收,使用jmap命令生成堆栈hprof文件
1 | jmap -dump:format=b,file=heap.hprof 30551 |
然后使用MAT分析hprof文件:
确实发现了很多ByteBuf对象没释放!
怀疑Netty存在内存泄漏
确定了这个问题之后就得看为什么Netty中会存在这么多没释放的ByteBuf对象了,Netty作为一款成熟的基础网络框架,应当不会犯这种低级错误。 在github上查一下,也没有发现与OOM有关的open状态的issue:
但是呢内存确实是持续增长的,不可能是我踩着狗屎第一个遇到吧。于是问了下AI,当Netty出现内存泄漏后应该怎么排查,AI告诉我在应用程序启动参数上加上开启Netty内存泄漏检测,于是我照着做了:
1 | nohup java $jvm_opts -Xmx768M -jar main.jar -Dio.netty.leakDetection.level=ADVANCED -Dio.netty.leakDetectionTargetRecords=10 --spring.config.location=shared.properties,main.properties,nodes.yml > /dev/null 2>&1 & |
但在下一次OOM发生的时候也没有发现任何关于内存泄漏的输出,那就真的没有内存泄漏了,至少不是Netty操作直接内存产生的内存泄漏。
于是将异常堆栈日志丢到搜索引擎上搜索一番,看到关于PooledByteBufAllocator对象的一些说明,这个Allocator为了提升内存分配效率,为每个线程都提供一个ByteBuf缓存,调用release时并没有释放内存,而是将其放回内存池中。如果每次请求申请的内存太大 又开启了太多的线程,那就真的有可能发生OOM。
Netty源码分析
我们来翻一番PooledByteBufAllocator的源码,从异常栈位置跟进:
1 |
|
1 | PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) { |
回头再看
这里我有点好奇调用newDirectBuffer时传个参数如何,到底申请多大的内存,毕竟这个问题以前并没有遇到,只是调用了一个返回数据比较大的接口时才遇到的,于是通过arthas观察请求入参:
1 | watch io.netty.buffer.PooledByteBufAllocator newDirectBuffer "{params}" -x 3 -b |
当从前端界面发起这个接口请求时,newDirectBuffer被调用了多次,但是分配的内存都不是太多:
单独重复多次这个请求时,并不是每次都能看到直接内存在增长,这或许是使用了旧线程,那这个线程的内存池已经足够分配接下来需要申请的内存。
结论
由于Netty PooledByteBufAllocator线程内部内存池缓存的原因,如果开启的请求线程太多,就会持续的占用内存,这种情况下无非两种处理方案: 
- 减少线程数  
- 增加直接内存限制
