一步步解决长连接 Netty 服务内存泄漏
作者 | 京东云开发者-京东科技 王长春
原文链接:https://my.oschina.net/u/4090830/blog/8685855
背景
老板说:长连接吗?
我说:是的!
老板说:该来的还是要来的,最终还是来了,快,赶紧先把服务重启下!
我说:已经重启了!
老板说:这问题必须给我解决了!
我说:必须的!
应用介绍
时效性差
耗费服务器性能
建立、关闭链接频繁
时效性高提升用户体验
减少链接建立次数
一次链接多次推送数据
提高系统吞吐量
Netty
框架,Netty
的高性能为这个应用带来了无上的荣光,承接了众多长连接使用场景的业务:PC 收银台微信支付
声波红包
POS 线下扫码支付
问题现象
问题排查与复现
排查
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 定时批量发请求,以达到服务的堆外内存泄漏。
启动项目,通过 JProfiler
JVM 监控工具,我们观察到内存缓慢的增长,最终触发了本地 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
官方也针对这个问题做了专门的讨论,一般的经验法则是,最后访问引用计数对象的一方负责销毁该引用计数对象,具体来说:如果一个 [发送] 组件将一个引用计数的对象传递给另一个 [接收] 组件,则发送组件通常不需要销毁它,而是由接收组件进行销毁。
如果一个组件使用了一个引用计数的对象,并且知道没有其他对象将再访问它(即,不会将引用传递给另一个组件),则该组件应该销毁它。
方式一:手动释放,哪里使用了,使用完就手动释放。
方式二:升级
ChannelHandler
为 SimpleChannelHandler
, 在 SimpleChannelHandler
中,Netty
对收到的所有消息都调用了 ReferenceCountUtil.release(msg)
。方式三:如果处理过程中不确定
ByteBuf
是否应该被释放,那交给 Netty 的 ReferenceCountUtil.release(msg)
来释放,这个方法会判断上下文是否可以释放。ChannelHandler
,如果升级 SimpleChannelHandler
对现有 API 接口变动比较大,同时如果手动释放,不确定是否应该释放风险也大,因此使用方式三,如下:线上实例内存正常
FullHttpRequest
中 ByteBuf
内存释放成功。从此长连接前置内存泄漏的问题彻底解决。总结
-Dio.netty.leakDetection.level
DISABLED - 完全禁用泄漏检测。不推荐。
SIMPLE - 抽样 1% 的缓冲区是否有泄漏。默认。
ADVANCED - 抽样 1% 的缓冲区是否泄漏,以及能定位到缓冲区泄漏的代码位置。
PARANOID - 与 ADVANCED 相同,只是它适用于每个缓冲区,适用于自动化测试阶段。如果生成输出包含 “LEAK:”,则可能会使生成失败。
-Dio.netty.leakDetectionLevel=advanced
定位到了具体内存泄漏的代码。在 PARANOID 泄漏检测级别以及 SIMPLE 级别运行单元测试和集成测试。
在 SIMPLE 级别向整个集群推出应用程序之前,请先在相当长的时间内查看是否存在泄漏。
如果有泄漏,灰度发布中使用 ADVANCED 级别,以获得有关泄漏来源的一些提示。
不要将泄漏的应用程序部署到整个群集。
方式二:如果处理过程中不确定
ByteBuf
是否应该被释放,那交给 Netty
的 ReferenceCountUtil.release(msg)
来释放,这个方法会判断上下文中是否可以释放,简单方便。方式三:升级
ChannelHandler
为 SimpleChannelHandler
, 在 SimpleChannelHandler 中,Netty 对收到的所有消息都调用了 ReferenceCountUtil.release(msg)
,升级接口,可能对现有 API 改动会比较大。往期推荐
我们为何期待Rust 2.0?
碾压ChatGPT、自主完成任务、Star数超8万的Auto-GPT,是炒作还是未来?
🌟 活动推荐
2023 年 5 月 27-28 日,GOTC 2023 全球开源技术峰会将在上海张江科学会堂隆重举行。
为期 2 天的开源行业盛会,将以行业展览、主题发言、特别论坛、分论坛、快闪演讲的形式来诠释此次大会主题 ——“Open Source, Into the Future”。与会者将一起探讨元宇宙、3D 与游戏、eBPF、Web3.0、区块链等热门技术主题,以及 OSPO、汽车软件、AIGC、开源教育培训、云原生、信创等热门话题,探讨开源未来,助力开源发展。
长按识别下方二维码立即查看 GOTC 2023 详情/报名。
微信扫码关注该文公众号作者