关于netty:长连接Netty服务内存泄漏看我如何一步步捉虫解决-京东云技术团队

24次阅读

共计 7578 个字符,预计需要花费 19 分钟才能阅读完成。

作者:京东科技 王长春

背景

事件要回顾到双 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 改变会比拟大

正文完
 0