0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

一步步解决长连接Netty服务内存泄漏

OSC开源社区 来源:OSCHINA 社区 2023-04-27 14:06 次阅读

线上应用长连接 Netty 服务出现内存泄漏了!

应用介绍

说起支付业务的长连接服务,真是说来话长,我们这就长话短说: 随着业务及系统架构的复杂化,一些场景,用户操作无法同步得到结果。一般采用的短连接轮训的策略,客户端需要不停的发起请求,时效性较差还浪费服务器资源。 短轮训痛点:

时效性差

耗费服务器性能

建立、关闭链接频繁

相比于短连接轮训策略,长连接服务可做到实时推送数据,并且在一个链接保持期间可进行多次数据推送。服务应用常见场景:PC 端扫码支付,用户打开扫码支付页面,手机扫码完成支付,页面实时展示支付成功信息,提供良好的用户体验。 长连服务优势:

时效性高提升用户体验

减少链接建立次数

一次链接多次推送数据

提高系统吞吐量

dab1abd8-e4c0-11ed-ab56-dac502259ad0.png 这个长连接服务使用 Netty 框架,Netty 的高性能为这个应用带来了无上的荣光,承接了众多长连接使用场景的业务:

PC 收银台微信支付

声波红包

POS 线下扫码支付

问题现象

回到线上问题,出现内存泄漏的是长连接前置服务,观察线上服务,这个应用的内存泄漏的现象总伴随着内存的增长,这个增长真是非常的缓慢,缓慢,缓慢,2、3 个月内从 30% 慢慢增长到 70%,极难发现:

dac11424-e4c0-11ed-ab56-dac502259ad0.png

每次发生内存泄漏,内存快耗尽时,总得重启下,虽说重启是最快解决的方法,但是程序员是天生懒惰的,要数着日子来重启,那绝对不是一个优秀程序员的行为!问题必须彻底解决!

问题排查与复现

排查

遇到问题,毫无头绪,首先还是需要去案发第一现场,排查 “死者 (应用实例)” 死亡现场,通过在发生 FullGC 的时间点,通过 Digger 查询ERROR日志,没想到还真找到破案的第一线索:

dac7f78a-e4c0-11ed-ab56-dac502259ad0.png

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

dad006aa-e4c0-11ed-ab56-dac502259ad0.png

2、模拟足够多的长连接请求,我们使用 Postman 定时批量发请求,以达到服务的堆外内存泄漏。

启动项目,通过JProfilerJVM 监控工具,我们观察到内存缓慢的增长,最终触发了本地Netty的堆外内存泄漏,本地复现成功:

dadcaae0-e4c0-11ed-ab56-dac502259ad0.jpg

dae86272-e4c0-11ed-ab56-dac502259ad0.png

_那问题具体出现在代码中哪块呢?_我们最重要的是定位具体代码,在开启了Netty的高级内存泄漏级别为高级,来定位下:

3、开启Netty的高级内存泄漏检测级别,JVM 参数如下:

-Dio.netty.leakDetectionLevel=advanced

daf2393c-e4c0-11ed-ab56-dac502259ad0.png

再启动项目,模拟请求,达到本地应用 JVM 内存泄漏,Netty 输出如下具体日志信息,可以看到,具体的日志信息比之前的信息更加完善:

2020-09-24 2059.078 [nioEventLoopGroup-3-1] INFO  io.netty.handler.logging.LoggingHandler [101] - [id: 0x2a5e5026, L:/00008883] READ: [id: 0x926e140c, L:/127.0.0.1:8883 - R:/127.0.0.1:58920]
2020-09-24 2059.078 [nioEventLoopGroup-3-1] INFO  io.netty.handler.logging.LoggingHandler [101] - [id: 0x2a5e5026, L:/00008883] READ COMPLETE
2020-09-24 2059.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)

dafa8b00-e4c0-11ed-ab56-dac502259ad0.jpg

不得不说Netty内存泄漏排查这点是真香!真香好评!

问题解决

找到问题了,那我么就需要解决,如何释放ByteBuf内存呢?

如何回收泄漏的 ByteBuf

其实Netty官方也针对这个问题做了专门的讨论,一般的经验法则是,最后访问引用计数对象的一方负责销毁该引用计数对象,具体来说:

如果一个 [发送] 组件将一个引用计数的对象传递给另一个 [接收] 组件,则发送组件通常不需要销毁它,而是由接收组件进行销毁。

如果一个组件使用了一个引用计数的对象,并且知道没有其他对象将再访问它(即,不会将引用传递给另一个组件),则该组件应该销毁它。

总结起来主要三个方式:

方式一:手动释放,哪里使用了,使用完就手动释放。

方式二:升级ChannelHandler为SimpleChannelHandler, 在SimpleChannelHandler中,Netty对收到的所有消息都调用了ReferenceCountUtil.release(msg)。

方式三:如果处理过程中不确定ByteBuf是否应该被释放,那交给 Netty 的ReferenceCountUtil.release(msg)来释放,这个方法会判断上下文是否可以释放。 考虑到长连接前置应用使用的是ChannelHandler,如果升级SimpleChannelHandler对现有 API 接口变动比较大,同时如果手动释放,不确定是否应该释放风险也大,因此使用方式三,如下:

db039efc-e4c0-11ed-ab56-dac502259ad0.jpg

线上实例内存正常

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

db0d8390-e4c0-11ed-ab56-dac502259ad0.png

总结

一、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 改动会比较大。





审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • JVM
    JVM
    +关注

    关注

    0

    文章

    152

    浏览量

    12130
  • 内存泄漏
    +关注

    关注

    0

    文章

    38

    浏览量

    9167

原文标题:一步步解决长连接 Netty 服务内存泄漏

文章出处:【微信号:OSC开源社区,微信公众号:OSC开源社区】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    一步步教你在局域网内玩转NAT命令

    一步步教你在局域网内玩转NAT命令  NAT是网络管理中常用的技术命令,其使用环境多是:多个内部计算机在访问INTERNET时使用同个公网IP地址;第二是当公司希望对内部计算机进行有效的安全保护
    发表于 02-24 18:01

    外国牛人教你一步步快速打造首台机器人(超详细)

    外国牛人教你一步步快速打造首台机器人(超详细)
    发表于 08-15 19:30

    一步步写嵌入式操作系统—ARM编程的方法与实践ch02

    一步步写嵌入式操作系统—ARM编程的方法与实践ch02
    发表于 08-20 20:54

    STM32初学者笔记一步步建立自己的STM32函数(分享)

    `[table=98%][tr][td]STM32初学者笔记(1) 一步步建立自己的STM32函数使用自己建立的 STM32F103.H 的 头文件。里面有大量中文注释。非常适合初学者。STM32
    发表于 03-10 11:26

    推荐本非常实用的Multisim仿真教程,分为模电,数电,一步步进阶掌握Multsim的仿真工具

    推荐本非常实用的Multisim仿真教程,分为模电,数电,一步步进阶掌握Multsim的仿真工具文件有点大,请全部下载后解压即可
    发表于 12-31 14:18

    CC2530一步步演示程序烧写

    CC2530一步步演示程序烧写第一步——先安装IAR开发环境第二歩——安装CC2530烧写工具第三歩——CC2530串口配置软件使用具体完整步骤看下面文档
    发表于 03-03 14:33

    一步步建立_STM32_UCOS_模板

    一步步建立_STM32_UCOS_模板
    发表于 09-29 11:46

    菜鸟一步步入门SAM4S-XPLAINED--IAR开发环境

    菜鸟一步步入门SAM4S-XPLAINED--IAR开发环境
    发表于 01-25 10:55

    自己搭建物联网后台的,一步步实现物联网系统

    本帖最后由 只耳朵怪 于 2018-5-30 09:20 编辑 第一步:制作自己的物联网开发板。下面是我自己制作的块基于ESP8266的wifi 物联板子个ESP8266+
    发表于 05-29 19:43

    XMC使用经验:教你一步步使用KEIL-MDK开发XMC1300

    使用手册和例程中,大部分都是采用DAVE平台来讲解的,采用KEI-MDK环境的资料非常少,下面就用此板子作为讲解平台,以个翻转板上6路LED灯为DEMO一步步讲解如何建立MDK工程,及编写外设底层驱动
    发表于 12-14 09:39

    一步步进行调试GPRS模块

    背景:在不知道硬件是否正确情况下,一步步进行调试,最终完成调试。以下是自己调试步骤。1、从gprs模块TX ,RX 单独焊接两个线出来,通过上位机发送AT指令,是否能正常工作。
    发表于 01-25 07:33

    ARM嵌入式系统如何入门?怎样一步步的去学习

    ARM嵌入式系统的学习步骤对于很多新手来说,不知道ARM嵌入式系统如何入门?怎样一步步的去学习?接下来信盈达教育嵌入式培训网就详解的为大家介绍:关于ARM嵌入式系统学习步骤:1.做个最小系统板:如果
    发表于 02-16 06:33

    stm32是如何一步步实现设置地址匹配接收唤醒中断功能的

    为什么要设置地址匹配接收唤醒中断呢?stm32是如何一步步实现设置地址匹配接收唤醒中断功能的?
    发表于 02-28 08:07

    一步步拆解STC32G屠龙刀示波器开源程序,边学边用

    曲线点的函数,3、建立AD采集函数,把数据存储到波形曲线图的数组中4、通过绘图函数,快速把曲线绘制出来四、一步一步重新搭建逻辑说起来是相对容易的,但直要一步步去实现,还是有很多困难的所以,我自己
    发表于 09-29 19:59

    一步步介绍CmBacktrace的相关知识和使用方法

    。定位错误的方法也往往是连接上仿真器,一步步 F10/F11 单步,定位到具体的错误代码,再去猜测、排除、推敲错误原因,这种过程十分痛苦,且花费的时间很长。 当然,也有部分开发者通过故障寄存器信息来定位
    发表于 10-26 15:44