长连接Netty服务内存泄漏,看我如何一步步捉“虫”解决

这篇具有很好参考价值的文章主要介绍了长连接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

总结起来主要三个方式
方式一:手动释放,哪里使用了,使用完就手动释放。
方式二:升级ChannelHandlerSimpleChannelHandler, 在SimpleChannelHandler中,Netty对收到的所有消息都调用了ReferenceCountUtil.release(msg)
方式三:如果处理过程中不确定ByteBuf是否应该被释放,那交给Netty的ReferenceCountUtil.release(msg)来释放,这个方法会判断上下文是否可以释放。

考虑到长连接前置应用使用的是ChannelHandler,如果升级SimpleChannelHandler对现有API接口变动比较大,同时如果手动释放,不确定是否应该释放风险也大,因此使用方式三,如下:

线上实例内存正常

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

总结

一、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是否应该被释放,那交给NettyReferenceCountUtil.release(msg)来释放,这个方法会判断上下文中是否可以释放,简单方便
方式三:升级ChannelHandlerSimpleChannelHandler, 在SimpleChannelHandler中,Netty对收到的所有消息都调用了ReferenceCountUtil.release(msg)升级接口,可能对现有API改动会比较大文章来源地址https://www.toymoban.com/news/detail-422273.html

到了这里,关于长连接Netty服务内存泄漏,看我如何一步步捉“虫”解决的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 解决鸿蒙APP的内存泄漏

    解决鸿蒙(HarmonyOS)应用的内存泄漏问题需要采用一系列的策略和技术。与解决Android内存泄漏类似,以下是一些建议,希望对大家有所帮助。北京木奇移动技术有限公司,专业的软件外包开发公司,欢迎交流合作。 1.使用鸿蒙开发者工具: 鸿蒙提供了开发者工具,其中包括

    2024年02月01日
    浏览(33)
  • Handler内存泄漏原因及解决方案

    在Activity中,将Handler声明成非静态内部类或匿名内部类,这样Handle默认持有外部类Activity的引用。如果Activity在销毁时,Handler还有未执行完或者正在执行的Message,而Handler又持有Activity的引用,导致GC无法回收Activity,导致内存泄漏。如以下两种情形可能导致内存泄漏 1、在Act

    2024年02月16日
    浏览(59)
  • 常见的内存泄漏原因和解决方案

    1.全局引用 问题:在JavaScript代码中,使用全局变量或全局对象来保存对DOM元素或其他对象的引用,这可能导致内存泄漏。 解决方案:避免使用全局变量或全局对象,改用合适的作用域来管理变量和对象的生命周期。确保在不再需要时正确地释放这些引用。 2.事件监听器 问题

    2024年02月13日
    浏览(45)
  • 记录--解决前端内存泄漏:问题概览与实用解决方案

    内存泄漏是前端开发中的一个常见问题,可能导致项目变得缓慢、不稳定甚至崩溃。在本文中,我们将深入探讨在JavaScript、Vue和React项目中可能导致内存泄漏的情况,并提供详细的代码示例,以帮助开发人员更好地理解和解决这些问题。 1. 未正确清理事件处理器 JavaScript中的

    2024年02月11日
    浏览(45)
  • ()自定义DialogFragment以及解决其内存泄漏问题

    日常开发中,dialog是常见的功能,我们时常需要弹出来一些弹框提示用户 今天就定义了一个方便的dialog基类BaseSimpleDialogFragment, 支持快速地显示一个dialog 主要功能有: initAnimation:设置入场和出场动画 getGravity:设置dialog显示位置(屏幕上,中,下) getCanceledOnTouchOutside:点

    2024年02月15日
    浏览(39)
  • 前端内存泄漏和溢出的情况以及解决办法

    在平时写代码时,内存泄漏的情况会时有发生,虽然js有内存回收机制,但在平时编程中还是需要注意避免内存泄漏的情况;前几天做移动端时遇到一个内存泄漏造成移动端页面卡顿的问题,所以想总结下前端内存泄漏的情况,回顾下基础知识  程序运行时操作系统会分配相

    2024年01月19日
    浏览(68)
  • centos下使用jemalloc解决Mysql内存泄漏问题

    参考: MySQL bug:https://bugs.mysql.com/bug.php?id=83047tdsourcetag=s_pcqq_aiomsg  https://github.com/jemalloc/jemalloc/blob/dev/INSTALL.md (1)ptmalloc 是glibc的内存分配管理 (2)tcmalloc 是google的内存分配管理模块 (3)jemalloc 是BSD的提供的内存分配管理 (可以 使用jemalloc优化Nginx ) 三者jemalloc和tcmall

    2024年02月11日
    浏览(47)
  • 利用谷歌DevTool解决web网页内存泄漏问题

    目录 web网页内存泄漏 主要的内存泄漏来源 利用谷歌DevTool定位内存泄漏问题 性能Performance 主要功能 Performance insights性能数据分析 Memory内存 三种模式 相关概念 解决内存泄漏问题 第一步 :是否内存泄漏:js堆直增不降;降的不多 第二步:找到导致内存泄漏的函数或者变量,

    2024年02月08日
    浏览(241)
  • Android 内存泄漏的常见原因及其对应的解决方案

    Android应用程序中常见的内存泄漏原因有很多,以下是一些常见的原因及对应的解决方案: 1. 静态引用导致的内存泄漏: 静态变量持有对Activity或Fragment的引用,导致它们无法被垃圾回收机制释放。 解决方案: 确保不将Activity或Fragment的实例赋值给静态变量。如果确实需要使用

    2024年02月08日
    浏览(51)
  • Qt中postevent造成内存泄漏问题的通用解决方案

    在Qt中由QCoreApplication统一管理Qt事件的收发和销毁,其中sendEvent为阻塞式发送,用于单线程的事件发送;postevent为非阻塞式发送,构造事件的线程和接受事件的线程可以为两个线程。 最近在做一个个人项目ShaderLab 需要绘制OpenGL实时渲染的图像,由于OpenGL渲染基本都放在循环语

    2024年02月15日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包