导致Netty内存泄漏的原因很多,例如,使用内存池方式创建的对象忘记释放,或者系统处理压力过大导致发送队列积压。


尽管Netty 采用了NIO非阻塞通信,I/O处理往往不会成为业务瓶颈,但是如果客户端并发压力过大,超过了服务端的处理能力,又没有流控保护,则很容易发生内存泄漏。




【客户端发送大量数据】



问题代码:某个客户端建立多个线程循环发送消息,客户端的发送队列会比较繁忙,此时客户端内存使用会不断飙升。




java Netty 接收数据量大导致断线重现 netty不断发送大量数据_客户端

 


java Netty 接收数据量大导致断线重现 netty不断发送大量数据_客户端_02




统计GC数据,发现老年代已满,发生多次Full GC,耗时3分钟多,系统已经无法正常运行,如图5-2所示。查看CPU的使用情况,发现GC线程占用了大量的CPU资源,




java Netty 接收数据量大导致断线重现 netty不断发送大量数据_java_03




分析原因查看dump图可以知道NioEventLoop,即消息处理handler线程内存溢出了,继续对引用关系进行分析,发现真正泄漏的对象是 WriteAndFlushTask,它包含了待


发送的客户端请求消息msg 及 promise对象,引用关系示例如图5-5所示。




java Netty 接收数据量大导致断线重现 netty不断发送大量数据_服务端_04



Netty的消息发送队列为什么会积压呢?通过源码分析发现,调用Channel的write方法时,如果发送方为业务线程,则将发送操作封装成WriteTask,放到Netty的NioEventLoop中执行,源码如下(AbstractChannelHandlerContext):




java Netty 接收数据量大导致断线重现 netty不断发送大量数据_客户端_05



这里有个结论,如果是业务方线程调用write方法,会被netty强制转化成使用NioEventLoop线程去执行,这里也符合netty的反应器模式设计:链接建立由Reactor线程池处理,接收到的消息转发给handler线程池处理,自然handler也必须负责消息的出栈。



为防止客户端发送巨量消息导致发送队列堆积,则在发送前需要判断该channel能否可读,netty提供了高低水位来实现客户端消息流控,先用setWriteBufferHignWaterMark预设最高字节数,当达到这个高水位时channel会自动变为不可写,改动如下:




java Netty 接收数据量大导致断线重现 netty不断发送大量数据_服务端_06



在实际项目中,根据业务QPS规划、客户端处理性能、网络带宽、链路数、消息平均码流大小等综合因素计算并设置高水位(WriteBufferHighWaterMark)值,利用高水位做消息发送速度的流控,既可以保护自身,同时又能减轻服务端的压力,防止服务端被压挂。





【什么情况会造成消息挤压发送不出去或发送不及时】



1.网络瓶颈,当发送速度超过网络链接处理能力,会导致发送队列积压。



2. 当对端读取速度小于己方发送速度,导致自身TCP发送缓冲区满,频繁发生write0字节时,待发送消息会在Netty发送队列中排队。 发送队列堆积的本质是服务端接收不及时,因为消息是从客户端send区转移到服务端recv区的,如果服务端recv区满了则客户端的send区也会逐渐变满,然后客户端数据不能放进send区则只能堆积到队列中。同样的,客户端接收消息不及时业务挤压服务端发送队列。这个情况很好模拟,在服务端程序cahnnelRead方法中设置断点,相当于不接收客户端消息,客户端就很容易出现这个情况。