1. 背景
1.1 直播平台内存泄漏问题
某直播平台,一些网红的直播间在业务高峰期,会有 10W+ 的粉丝接入,如果瞬间发生大量客户端连接掉线、或者一些客户端网络比较慢,发现基于 Netty 构建的服务端内存会飙升,发生内存泄漏(OOM),导致直播卡顿、或者客户端接收不到服务端推送的消息,用户体验受到很大影响。
1.2 问题分析
首先对 GC 数据进行分析,发现老年代已满,发生多次 Full GC,耗时达 3 分多,系统已经无法正常运行(示例):
图 1 直播高峰期服务端 GC 统计数据
Dump 内存堆栈进行分析,发现大量的发送任务堆积,导致内存溢出(示例):
图 2 直播高峰期服务端内存 Dump 文件分析
通过以上分析可以看出,在直播高峰期,服务端向上万客户端推送消息时,发生了发送队列积压,引起内存泄漏,最终导致服务端频繁 GC,无法正常处理业务。
1.3 解决策略
服务端在进行消息发送的时候做保护,具体策略如下:
-
根据可接入的最大用户数做客户端并发接入数流控,需要根据内存、CPU 处理能力,以及性能测试结果做综合评估。
-
设置消息发送的高低水位,针对消息的平均大小、客户端并发接入数、JVM 内存大小进行计算,得出一个合理的高水位取值。服务端在推送消息时,对 Channel 的状态进行判断,如果达到高水位之后,Channel 的状态会被 Netty 置为不可写,此时服务端不要继续发送消息,防止发送队列积压。
服务端基于上述策略优化了代码,内存泄漏问题得到解决。
1.4. 总结
尽管 Netty 框架本身做了大量的可靠性设计,但是对于具体的业务场景,仍然需要用户做针对特定领域和场景的可靠性设计,这样才能提升应用的可靠性。
除了消息发送积压导致的内存泄漏,Netty 还有其它常见的一些内存泄漏点,本文将针对这些可能导致内存泄漏的功能点进行分析和总结。
2. 消息收发防内存泄漏策略
2.1. 消息接收
2.1.1 消息读取
Netty 的消息读取并不存在消息队列,但是如果消息解码策略不当,则可能会发生内存泄漏,主要有如下几点:
1. 畸形码流攻击:如果客户端按照协议规范,将消息长度值故意伪造的非常大,可能会导致接收方内存溢出。
2. 代码 BUG:错误的将消息长度字段设置或者编码成一个非常大的值,可能会导致对方内存溢出。
3. 高并发场景:单个消息长度比较大,例如几十 M 的小视频,同时并发接入的客户端过多,会导致所有 Channel 持有的消息接收 ByteBuf 内存总和达到上限,发生 OOM。
避免内存泄漏的策略如下:
- 无论采用哪种解码器实现,都对消息的最大长度做限制,当超过限制之后,抛出解码失败异常,用户可以选择忽略当前已经读取的消息,或者直接关闭链接。
以 Netty 的 DelimiterBasedFrameDecoder 代码为例,创建 DelimiterBasedFrameDecoder 对象实例时,指定一个比较合理的消息最大长度限制,防止内存溢出:
/**
{1}
* Creates a new instance.
{1}
*
{1}
*@parammaxFrameLength the maximum length of the decoded frame.
{1}
* A {@linkTooLongFrameException} is thrown if
{1}
* the length of the frame exceeds this value.
{1}
*@paramstripDelimiter whether the decoded frame should strip out the
{1}
* delimiter or not
{1}
*@paramdelimiter the delimiter
{1}
*/
publicDelimiterBasedFrameDecoder(
intmaxFrameLength,booleanstripDelimiter, ByteBuf delimiter) {
this(maxFrameLength, stripDelimiter,true, delimiter);
}
- 需要根据单个 Netty 服务端可以支持的最大客户端并发连接数、消息的最大长度限制以及当前 JVM 配置的最大内存进行计算,并结合业务场景,合理设置 maxFrameLength 的取值。
2.1.2 ChannelHandler 的并发执行
Netty 的 ChannelHandler 支持串行和异步并发执行两种策略,在将 ChannelHandler 加入到 ChannelPipeline 时,如果指定了 EventExecutorGroup,则 ChannelHandler 将由 EventExecutorGroup 中的 EventExecutor 异步执行。这样的好处是可以实现 Netty I/O 线程与业务 ChannelHandler 逻辑执行的分离,防止 ChannelHandler 中耗时业务逻辑的执行阻塞 I/O 线程。
ChannelHandler 异步执行的流程如下所示:
图 3 ChannelHandler 异步并发执行流程
如果业务 ChannelHandler 中执行的业务逻辑耗时较长,消息的读取速度又比较快,