导读
Netty 是一个异步事件驱动的网络通信层框架,用于快速开发高可用高性能的服务端网络框架与客户端程序,它极大地简化了 TCP 和 UDP 套接字服务器等网络编程。
Netty 底层基于 JDK 的 NIO,我们为什么不直接基于 JDK 的 NIO 或者其他NIO框架:
- 使用 JDK 自带的 NIO 需要了解太多的概念,编程复杂。
- Netty 底层 IO 模型随意切换,而这一切只需要做微小的改动。
- Netty自带的拆包解包,异常检测等机制让我们从 NIO 的繁重细节中脱离出来,只需关心业务逻辑即可。
- Netty解决了JDK 的很多包括空轮训在内的 Bug。
- Netty底层对线程,Selector 做了很多细小的优化,精心设计的 Reactor 线程做到非常高效的并发处理。
- 自带各种协议栈,让我们处理任何一种通用协议都几乎不用亲自动手。
- Netty社区活跃,遇到问题随时邮件列表或者 issue。
- Netty已经历各大RPC框架(Dubbo),消息中间件(RocketMQ),大数据通信(Hadoop)框架的广泛的线上验证,健壮性无比强大。(以上优点摘自美团技术博客)
背景
最近在做一个基于异步事件驱动的基础框架,服务端使用实现 Vert.x框架,该框架底层网络通信基于 Netty 实现。基于对反应式编程的兴趣与研究,打算把Spring WebFlux与Vert.x结合在一起使用。
理想目标是创建一个纯异步的、反应式编程框架。
在完成初步目标(完成了Spring WebFlux与Vert.x整合使用)之后,进行初步的demo测试。在这个时候一切都没有问题,都按照理想中的步骤运行。基于对底层框架的负责以及展示区别于传统编程性能的提升。于是决定用Jmeter对这个demo进行压力测试。在压测一段时间后出现了netty堆外内存溢出的问题。此篇用来记录遇到问题以及排查解决问题的过程。为以后遇到这个问题的同学做下记录,避免此坑。
问题
整合Spring WebFlux与Vert.x后,底层网络通信使用netty,压测一段时间后,出现netty堆外内存溢出,如下图:
意思就是告诉我们netty去申请堆外内存的时候,出现了内存不足的情况。
排查过程
看到错误这种日志的时候,习惯性往最下面翻查,是reactor工作线程抛出的,除此之外没有任何有用信息。按照平时的排查思路,再去看dump文件,也没有发现任何异常(此处排除略)。
阶段一:
这个时候发现不能按照平时排查策略来解决问题,重新分析日志,这里有这么一段:
io.netty.util.internal.PlatformDependent.incrementMemoryCounter(...)是用来统计分配netty堆外内存的,其中有个字段用来保存已分配内存private static final AtomicLong DIRECT_MEMORY_COUNTER;于是决定把DIRECT_MEMORY_COUNTER这个字段值打印出来,用PlatformDependent.usedDirectMemory();可以直接获取,如下图:
阶段二:
定位堆外内存增长速度,压测了几分钟后:
发现是在不断的缓慢增长。
阶段三:
IDE中断点排查。
断点决定打在处理请求、返回的处理器上,如下图:
当断点进入这里的时候,一路F7进入调用方法中,并不断的观察控制台,堆外内存的大小变化。
跟踪到如下图的位置,执行过这个断点的时候就会出现堆外内存使用统计变化:
到这里基础就找到堆外内存溢出问题的原因了,再来就是如何解决问题。
阶段四:
解决问题。
因在org.springframework.http.codec.json.AbstractJackson2Encoder.encode(...)方法中申请了堆外内存分配,只需要在接口调用完毕的时候,手动释放此内存即可。
阶段五:
线上验证。
1 测试服务器上,启动程序,启动参数如下:
2 使用jmeter对用户分页接口进行压测(数据库:mysql),启动500个线程,压测10分钟,结果如下:
3 发现堆外内存使用变化:
4 Arthas Dashboard监控:
到此问题解决。