导致NioSocketChannel泄漏的可能原因有两个。
(1)代码有缺陷,HTTPS 客户端关闭连接之后,服务端没有正确关闭连接,多发生在netty做https服务端并使用http/1.1时。
(2)
服务端负载比较重,客户端超时之后的断连和重连速度超过服务端关闭连接速度,导致服务端的NioSocketChannel发生积压。随着积压数的增加,导致占用的内存快速增加,频繁GC使得服务端处理更慢,积压更严重,最终导致OOM异常。
明确方向之后,具体的定位策略有3点。
(1)通过netstat命令查看服务端的端口连接状态,是不断地创建和回收连接,还是服务端没有关闭连接导致连接一直增长。
(2)查看NioSocketChannel的状态,是全部没关闭,还是部分关闭、部分打开。
(3)停止压测一段时间,观察连接数,以及服务端的内存占用情况,看服务端是否可以自动恢复。
在压测过程中,动态采集HTTPS的连接状态,发现超时的连接被服务端关闭,如图18-3所示。
接着通过OQL在内存堆栈中查询NioSocketChannel的连接状态,其中处于关闭状态的连接数为25,如图18-4所示。
服务端尚未主动关闭的NioSocketChannel实例个数为5806,如图18-5所示。
从OQL查询可以看出,内存中尚有被服务端关闭但是还没来得及被NioSocketChannel对象,证明客户端超时关闭连接后,服务端感知了连接关闭事件并主动关闭了连接。动恢复。停止压测观察服务端内存使用情况,如图18-6所示。
经过上述分析得知,并不是由于服务端忘记关闭 NioSocketChannel导致内存泄漏的,而是由于服务端关闭 NioSocketChannel的速度没有各厂端按八迷度内寸以NioSocketChannel缓慢积压,当积压到一定数量,无法在新生代被GC,所以达到晋升阈值后被复制到老年代,引起老年代GC,最终导致OOM异常。
netty实现http方式的rpc框架,问题还原:
客户端采用HTTP连接池的方式与服务端进行RPC调用,单个客户端连接池上限为200,客户端部署了30个实例,而服务端只部署了3个实例。在业务高峰期,每个服务端需要处理6000个HTTP连接,
服务端时延增大之后,导致客户端批量超时,超时之后客户端会关闭连接重新发起connect 操作,在某个瞬间,几千个HTTPS 连接同时发起SSL握手操作,由于服务端此时也处于高负荷运行状态,导致部分连接SSL握手失败或者超时,超时之后客户端继续重连,进一步加重服务端的处理压力,最终导致服务端来不及释放客户端关闭的连接,引起 NioSocketChannel大量积压,最终导致OOM异常。
客户端也没有流控机制,只要连接数不够用,就会一直创建连接,达到连接池配置的最大连接数。正是由于客户端和服务端都没有对高并发时大量的HTTPS 链路断连和重连进行保护,导致了服务端OOM异常,业务中断。
问题解决:
基于Netty的Pipeline机制,可以对SSL握手成功、SSL连接关闭做切面拦截(类似于Spring的AOP机制,但是没采用反射机制,性能更高),通过流控切面接口,对HTTPS连接进行计数,根据计数器进行流控,服务端的流控算法如下。
(1)获取流控阈值。
(2)从全局上下文中获取当前的并发连接数,与流控阈值对比,如果小于流控阈值,则对当前的计数器进行原子自增,允许客户端连接。
(3)如果等于或者大于流控阈值,则抛出流控异常给客户端。
(4)SSL连接关闭时,获取上下文中的并发连接数,进行原子自减。
(1)流控的 ChannelHandler声明为@ChannelHandler.Sharable,这样创建一个全局流控实例,就可以在所有的SSL 连接中共享。
(2)
通过userEventTriggered方法拦截SsIHandshakeCompletionEvent和SslCloseCompletion-Event事件,在SSL握手成功和SSL 连接关闭时更新流控计数器。
(3)流控并不是仅针对ESTABLISHED状态的HTTP连接,而是针对所有状态的连接,因为客户端关闭连接,并不意味着服务端也同时关闭连接,只有触发SsCloseCompletion-Event事件时,服务端才真正关闭了NioSocketChannel,GC才会回收连接关联的内存。
(4)流控ChannelHandler会被多个NioEventLoop线程调用,因此对于相关的计数器更新等操作,要保证并发安全性,避免使用全局锁,可以通过原子类等提升性能。