长连接Netty服务内存泄漏,看我如何一步步捉“虫”解决
作者:京东科技 王长春
背景
事情要回顾到双11.11备战前夕,在那个风雨交加的夜晚,一个急促的咚咚报警,惊破了电闪雷鸣的黑夜,将沉浸在梦香,熟睡的我惊醒。
一看手机咚咚报警,不好!有大事发生了!电话马上打给老板:
老板说: 长连接吗?
我说:是的!
老板说:该来的还是要来的,最终还是来了,快,赶紧先把服务重启下!
我说:已经重启了!
老板说: 这问题必须给我解决了!
我说:必须的!
线上应用长连接Netty服务出现内存泄漏了!真让人头大
在这风雨交加的夜晚,此时,面对毫无头绪的问题,以及迫切想攻克问题的心,已经让我兴奋不已,手一把揉揉刚还迷糊的眼,今晚又注定是一个不眠之夜!
应用介绍
说起支付业务的长连接服务,真是说来话长,我们这就长话短说:
随着业务及系统架构的复杂化,一些场景,用户操作无法同步得到结果。一般采用的短连接轮训的策略,客户端需要不停的发起请求,时效性较差还浪费服务器资源。
短轮训痛点:
- 时效性差
- 耗费服务器性能
- 建立、关闭链接频繁
相比于短连接轮训策略,长连接服务可做到实时推送数据,并且在一个链接保持期间可进行多次数据推送。服务应用常见场景:PC端扫码支付,用户打开扫码支付页面,手机扫码完成支付,页面实时展示支付成功信息,提供良好的用户体验。
长连服务优势:
- 时效性高提升用户体验
- 减少链接建立次数
- 一次链接多次推送数据
- 提高系统吞吐量

这个长连接服务使用Netty框架,Netty的高性能为这个应用带来了无上的荣光,承接了众多长连接使用场景的业务:
- PC收银台微信支付
- 声波红包
- POS线下扫码支付
问题现象
回到线上问题,出现内存泄漏的是长连接前置服务,观察线上服务,这个应用的内存泄漏的现象总伴随着内存的增长,这个增长真是非常的缓慢,缓慢,缓慢,2、3个月内从30%慢慢增长到70%,极难发现:

每次发生内存泄漏,内存快耗尽时,总得重启下,虽说重启是最快解决的方法,但是程序员是天生懒惰的,要数着日子来重启,那绝对不是一个优秀程序员的行为!问题必须彻底解决!
问题排查与复现
排查
遇到问题,毫无头绪,首先还是需要去案发第一现场,排查“死者(应用实例)”死亡现场,通过在发生FullGC的时间点,通过Digger查询ERROR日志,没想到还真找到破案的第一线索:

io.netty.util.ResourceLeakDetector [176] - LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetection.level=advanced' or call ResourceLeakDetector.setLevel() See http://netty.io/wiki/reference-counted-objects.html for more information.
线上日志竟然有一个明显的"LEAK"泄漏字样,作为技术人的敏锐的技术嗅觉,和找Bug的直觉,可以确认,这就是事故案发第一现场。
我们凭借下大学四六级英文水平的,继续翻译下线索,原来是这呐!
ByteBuf.release() 在垃圾回收之前没有被调用。启用高级泄漏报告以找出泄漏发生的位置。要启用高级泄漏报告,请指定 JVM 选项“-Dio.netty.leakDetectionLevel=advanced”或调用 ResourceLeakDetector.setLevel()
啊哈!这信息不就是说了嘛!ByteBuf.release()在垃圾回收前没有调用,有ByteBuf对象没有被释放,ByteBuf可是分配在直接内存的,没有被释放,那就意味着堆外内存泄漏,所以内存一直是非常缓慢的增长,GC都不能够进行释放。
提供了这个线索,那到底是我们应用中哪段代码出现了ByteBuf对象的内存泄漏呢?
项目这么大,Netty通信处理那么多,怎么找呢?自己从中搜索,那肯定是不靠谱,找到了又怎么释放呢?
复现
面对这一连三问?别着急,Netty的日志提示还是非常完善:启用高级泄漏报告找出泄漏发生位置嘛,生产上不可能启用,并且生产发生时间极长,时间上来不及,而且未经验证,不能直接生产发布,那就本地代码复现一下!找到具体代码位置。
为了本地复现Netty泄漏,定位详细的内存泄漏代码,我们需要做这几步:
1、配置足够小的本地JVM内存,以便快速模拟堆外内存泄漏。
如图,我们设置设置PermSize=30M, MaxPermSize=43M

2、模拟足够多的长连接请求,我们使用Postman定时批量发请求,以达到服务的堆外内存泄漏。
启动项目,通过JProfilerJVM监控工具,我们观察到内存缓慢的增长,最终触发了本地Netty的堆外内存泄漏,本地复现成功:


_那问题具体出现在代码中哪块呢?_我们最重要的是定位具体代码,在开启了Netty的高级内存泄漏级别为高级,来定位下:
3、开启Netty的高级内存泄漏检测级别,JVM参数如下:
-Dio.netty.leakDetectionLevel=advanced

再启动项目,模拟请求,达到本地应用JVM内存泄漏,Netty输出如下具体日志信息,可以看到,具体的日志信息比之前的信息更加完善:
2020-09-24 20:11:59.078 [nioEventLoopGroup-3-1] INFO io.netty.handler.logging.LoggingHandler [101] - [id: 0x2a5e5026, L:/0:0:0:0:0:0:0:0:8883] READ: [id: 0x926e140c, L:/127.0.0.1:8883 - R:/127.0.0.1:58920]
2020-09-24 20:11:59.078 [nioEventLoopGroup-3-1] INFO io.netty.handler.logging.LoggingHandler [101] - [id: 0x2a5e5026, L:/0:0:0:0:0:0:0:0:8883] READ COMPLETE
2020-09-24 20:11:59.079 [nioEventLoopGroup-2-8] ERROR io.netty.util.ResourceLeakDetector [171] - LEAK: ByteBuf.release() was not called before it's garbage-collected. See http://netty.io/wiki/reference-counted-objects.html for more information.
WARNING: 1 leak records were discarded because the leak record count is limited to 4. Use system property io.netty.leakDetection.maxRecords to increase the limit.
Recent access records: 5
#5:
io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.readBytes(AdvancedLeakAwareCompositeByteBuf.java:476)
io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.readBytes(AdvancedLeakAwareCompositeByteBuf.java:36)
com.jd.jr.keeplive.front.service.nettyServer.handler.LongRotationServerHandler.getClientMassageInfo(LongRotationServerHandler.java:169)
com.jd.jr.keeplive.front.service.nettyServer.handler.LongRotationServerHandler.handleHttpFrame(LongRotationServerHandler.java:121)
com.jd.jr.keeplive.front.service.nettyServer.handler.LongRotationServerHandler.channelRead(LongRotationServerHandler.java:80)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
......
#4:
Hint: 'LongRotationServerHandler#0' will handle the message from this point.
io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.touch(AdvancedLeakAwareCompositeByteBuf.java:1028)
io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.touch(AdvancedLeakAwareCompositeByteBuf.java:36)
io.netty.handler.codec.http.HttpObjectAggregator$AggregatedFullHttpMessage.touch(HttpObjectAggregator.java:359)
......
#3:
Hint: 'HttpServerExpectContinueHandler#0' will handle the message from this point.
io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.touch(AdvancedLeakAwareCompositeByteBuf.java:1028)
io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.touch(AdvancedLeakAwareCompositeByteBuf.java:36)
io.netty.handler.codec.http.HttpObjectAggregator$AggregatedFullHttpMessage.touch(HttpObjectAggregator.java:359)
......
#2:
Hint: 'HttpHeartbeatHandler#0' will handle the message from this point.
io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.touch(AdvancedLeakAwareCompositeByteBuf.java:1028)
io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.touch(AdvancedLeakAwareCompositeByteBuf.java:36)
io.netty.handler.codec.http.HttpObjectAggregator$AggregatedFullHttpMessage.touch(HttpObjectAggregator.java:359)
......
#1:
Hint: 'IdleStateHandler#0' will handle the message from this point.
io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.touch(AdvancedLeakAwareCompositeByteBuf.java:1028)
io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.touch(AdvancedLeakAwareCompositeByteBuf.java:36)
io.netty.handler.codec.http.HttpObjectAggregator$AggregatedFullHttpMessage.touch(HttpObjectAggregator.java:359)
......
Created at:
io.netty.util.ResourceLeakDetector.track(ResourceLeakDetector.java:237)
io.netty.buffer.AbstractByteBufAllocator.compositeDirectBuffer(AbstractByteBufAllocator.java:217)
io.netty.buffer.AbstractByteBufAllocator.compositeBuffer(AbstractByteBufAllocator.java:195)
io.netty.handler.codec.MessageAggregator.decode(MessageAggregator.java:255)
......
开启高级的泄漏检测级别后,通过上面异常日志,我们可以看到内存泄漏的具体地方:com.jd.jr.keeplive.front.service.nettyServer.handler.LongRotationServerHandler.getClientMassageInfo(LongRotationServerHandler.java:169)

不得不说Netty内存泄漏排查这点是真香!真香好评!
问题解决
找到问题了,那我么就需要解决,如何释放ByteBuf内存呢?
如何回收泄漏的ByteBuf
其实Netty官方也针对这个问题做了专门的讨论,一般的经验法则是,最后访问引用计数对象的一方负责销毁该引用计数对象,具体来说:
如果一个[发送]组件将一个引用计数的对象传递给另一个[接收]组件,则发送组件通常不需要销毁它,而是由接收组件进行销毁。
如果一个组件使用了一个引用计数的对象,并且知道没有其他对象将再访问它(即,不会将引用传递给另一个组件),则该组件应该销毁它。
详情请看翻译的Netty官方文档对引用计数的功能使用:
【翻译】Netty的对象引用计数
【原文】Reference counted objects
总结起来主要三个方式:
方式一:手动释放,哪里使用了,使用完就手动释放。
方式二:升级ChannelHandler为SimpleChannelHandler, 在SimpleChannelHandler中,Netty对收到的所有消息都调用了ReferenceCountUtil.release(msg)。
方式三:如果处理过程中不确定ByteBuf是否应该被释放,那交给Netty的ReferenceCountUtil.release(msg)来释放,这个方法会判断上下文是否可以释放。
考虑到长连接前置应用使用的是ChannelHandler,如果升级SimpleChannelHandler对现有API接口变动比较大,同时如果手动释放,不确定是否应该释放风险也大,因此使用方式三,如下:

线上实例内存正常
问题修复后,线上服务正常,内存使用率也没有再出现因泄漏而增长,从线上我们增加的日志中看出,FullHttpRequest中ByteBuf内存释放成功。从此长连接前置内存泄漏的问题彻底解决。

总结
一、Netty的内存泄漏排查其实并不难,Netty提供了比较完整的排查内存泄漏工具
JVM 选项-Dio.netty.leakDetection.level
目前有 4 个泄漏检测级别的:
DISABLED - 完全禁用泄漏检测。不推荐。
SIMPLE - 抽样 1% 的缓冲区是否有泄漏。默认。
ADVANCED - 抽样 1% 的缓冲区是否泄漏,以及能定位到缓冲区泄漏的代码位置。
PARANOID - 与 ADVANCED 相同,只是它适用于每个缓冲区,适用于自动化测试阶段。如果生成输出包含“LEAK:”,则可能会使生成失败。
本次内存泄漏问题,我们通过本地设置泄漏检测级别为高级,即:-Dio.netty.leakDetectionLevel=advanced定位到了具体内存泄漏的代码。
同时Netty也给出了避免泄漏的最佳实践:
在 PARANOID 泄漏检测级别以及 SIMPLE 级别运行单元测试和集成测试。
在 SIMPLE 级别向整个集群推出应用程序之前,请先在相当长的时间内查看是否存在泄漏。
如果有泄漏,灰度发布中使用 ADVANCED 级别,以获得有关泄漏来源的一些提示。
不要将泄漏的应用程序部署到整个群集。
二、解决Netty内存泄漏,Netty也提供了指导方案,主要有三种方式
方式一:手动释放,哪里使用了,使用完就手动释放,这个对使用方要求比较高了。
方式二:如果处理过程中不确定ByteBuf是否应该被释放,那交给Netty的ReferenceCountUtil.release(msg)来释放,这个方法会判断上下文中是否可以释放,简单方便。
方式三:升级ChannelHandler为SimpleChannelHandler, 在SimpleChannelHandler中,Netty对收到的所有消息都调用了ReferenceCountUtil.release(msg),升级接口,可能对现有API改动会比较大。
长连接Netty服务内存泄漏,看我如何一步步捉“虫”解决的更多相关文章
- 长连接锁服务优化实践 C10K问题 nodejs的内部构造 limits.conf文件修改 sysctl.conf文件修改
小结: 1. 当文件句柄数目超过 10 之后,epoll 性能将优于 select 和 poll:当文件句柄数目达到 10K 的时候,epoll 已经超过 select 和 poll 两个数量级. 2 ...
- 也谈---基于 HTTP 长连接的“服务(转载)
这里指讨论基于HTTP的推技术, 诸如flash,applet之类的东西不作分析, 他们就不能说是"纯粹"的浏览器应用了. 首先是一点背景知识, 大家都知道长连接避免了tcp连接的 ...
- NGINX轻松管理10万长连接 --- 基于2GB内存的CentOS 6.5 x86-64
http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=190176&id=4234854 一 前言 当管理大量连接时,特别 ...
- 记一次 .NET 某HIS系统后端服务 内存泄漏分析
一:背景 1. 讲故事 前天那位 his 老哥又来找我了,上次因为CPU爆高的问题我给解决了,看样子对我挺信任的,这次另一个程序又遇到内存泄漏,希望我帮忙诊断下. 其实这位老哥技术还是很不错的,他既然 ...
- 记一次 .NET 某消防物联网 后台服务 内存泄漏分析
一:背景 1. 讲故事 去年十月份有位朋友从微信找到我,说他的程序内存要炸掉了...截图如下: 时间有点久,图片都被清理了,不过有点讽刺的是,自己的程序本身就是做监控的,结果自己出了问题,太尴尬了 二 ...
- Netty堆外内存泄漏排查,这一篇全讲清楚了
上篇文章介绍了Netty内存模型原理,由于Netty在使用不当会导致堆外内存泄漏,网上关于这方面的资料比较少,所以写下这篇文章,专门介绍排查Netty堆外内存相关的知识点,诊断工具,以及排查思路提供参 ...
- netty内存泄漏
关于netty本身内存泄漏的资料,在此记录一下:https://blog.csdn.net/hannuotayouxi/article/details/78827499
- 不仅仅是百万级TCP长连接框架 t-io
t-io: 不仅仅是百万级TCP长连接框架 t-io是基于jdk aio实现的易学易用.稳定.性能强悍.将多线程运用到极致.内置功能丰富的即时通讯框架(广义上的即时通讯,并非指im),字母 t 寓意t ...
- (转)从内存管 理、内存泄漏、内存回收探讨C++内存管理
http://www.cr173.com/html/18898_all.html 内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟 ...
- JAVA内存泄漏解决办法
JVM调优工具 Jconsole,jProfile,VisualVM Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用.对垃圾回收算法有很详细的跟踪.详细说明参考这里 ...
随机推荐
- Excel 去除合并并保留原值的办法
部分Excel中,对行进行了合并.这个方便展示,但是筛选后数据展示会出现问题,需要去除合并,并在每行中保留原来的值. 1.先选择整行,并"取消单元格合并" 操作后出现大量的空值行. ...
- [小技巧]Win32 - VS中手动编辑RC文件
用win32进行窗口编程时,如果资源文件不妥善集中管理会一不小心会删除一些资源文件,这时再进行项目编译的时候会发现rc文件因为缺失某个资源文件导致无法打开,我们常用的rc图形编辑器等于废了. 这时就需 ...
- vuw3学习大全(2)
# composition(组合式api) ## 1.为什么使用composition vue3里面不需要Mixins了?因为有compoition api 能讲逻辑进行抽离和复用 大型组件中,其中* ...
- Chrome(谷歌)浏览器永久关闭恢复页面提示框(记录)
使用脚本调用Chrome浏览器启动指定页面,然后代码里的命令关闭,会被浏览器识别为非正常关闭. 再次执行脚本的时候会停留在空白页面,无法进入指定页面,设置为主页也无法进入. 排查可能是浏览器自动恢复页 ...
- unity 开发小技巧之C#篇 ----持续更新
1.ToString()方法是可以重写的 2.Start()方法可以主动调用 3.接口可以直接使用 jiekou?.Invoke() 来调用所有继承该接口的方法 4.Json 列表和服务器需要的数据不 ...
- zip文件自动打包
简单的文件打包 首先是问题 我们有一个文件的文件过大,我需要删除或者压缩,当然我们就是选择压缩 如果是单个我们可以直接使用压缩功能 但是多个呢? 首先获取当前目录下的文件,使用 a=`ls` | te ...
- 使用Wireshark查看HTTPS中TLS握手过程
通过使用Wireshark抓包分析TLS握手的过程,可以更容易理解和验证TLS协议,本文将先介绍Wireshark解密HTTPS流量的方法,然后分别验证TLS握手过程和TLS会话恢复的过程. 一.使用 ...
- Java-http请求工具-OkHttp用法
前言:一般Java项目后端发送请求都使用http,最近项目里面大佬建议把http都改成okhttp3(OkHttpClient).故今日记录部分常用发送方式. 代码:为了便于以后使用,这里封装一个Ok ...
- laravel groupBy 分页
$model=DB::table('tablebname') ->where(function($query) use ($res){ $query->where('xx','xx'); ...
- 《MySQL是怎样运行的》第七章小结