优雅地停止服务,也就是要保证把没有处理完的工作处理完成。比如停止一些依赖的服务,输出一些日志,发一些信号给其他的应用系统等,这个在保证系统的高可用中是非常有必要的。
尤其像长连接网关这种,管理了大量的TCP 连接,绝不能直接暴力关闭,必须要保证已有的任务全都处理完,并且在关闭的过程中不会有新的请求进来。
本文以开源项目SONA为例,详细解析网关服务优雅的原理和实现,帮助读者更好理解和实践。本文最后附上开源项目地址。
Sona 平台是一个搭建语音房产品的全端解决方案,包含了房间管理、实时音视频、房间IM、长连接网关等能力。其中最基础核心的就是长连接网关。
我们很多时候都需要安全的将服务停止,也就是把没有处理完的工作继续处理完成。比如停止一些依赖的服务,输出一些日志,发一些信号给其他的应用系统等,这个在保证系统的高可用中是非常有必要的。
尤其像 长连接网关这种,管理了大量的TCP 连接,绝不能直接暴力关闭,必须要保证已有的任务全都处理完,并且在关闭的过程中不会有新的请求进来。
服务的优雅关闭通常都是利用 JDK 里面提供的 Runtime.addShutDownHook(Thread hook)方法,JVM 提供一种 ShutdownHook(钩子)机制,当 JVM 接受到系统的关闭通知之后,会调用 ShutdownHook 内的方法,用以完成清理操作,从而平滑的退出应用。
像Spring 、Dubbo 等都是基于这个实现的优雅关闭
Spring 框架本身也依赖于 shutdown hook 执行优雅停机,通过调用 AbstractApplicationContext
的 registerShutdownHook 方法
在 doClose 方法里面,主要做了如下几件事:
LifecycleProcessor
的 onClose 方法 (很多 spring 相关的jar里面会基于LifecycleProcessor 在容器关闭时做一些清理工作,比如 Kafka Listener)因为在Mercury 业务处理的 Handler 里面用到了 spring bean (主要是RocketMQ相关的) ,所以关闭连接、清理Netty 资源的时机一定要在 Spring 容器关闭之前才可以。
从上面 doClose 的流程可以看出,只要在 destroyBeans 之前执行 Netty 资源的关闭即可。
Spring 提供了 ApplicationListener
接口,开发者可以实现这个接口监听到 Spring 容器的 ContextClosedEvent 关闭事件。我选择的就是这种方式。因为没有修改Spring中默认的事件发布器SimpleApplicationEventMulticaster
,会同步的执行 onApplicationEvent 方法,这样就保证了在关闭Netty 相关资源之后才会去销毁 bean。
Netty 相关资源优雅关闭的主要流程如下:
1.关闭 server channel
服务端关闭 NioServerSocketChannel,取消端口绑定,关闭服务
直接调用 channel.close() ;
2.发送重连消息
当 客户端收到 server 下发的 reconnect 消息之后,就会断开当前连接,然后重新建立连接。
长连接网关单台机器上会有数万连接,这里不会一次性给所有连接下发 reconnect 消息,不然可能会导致这数万客户端同时发起重新建连,会造成其他的网关机器 CPU使用率突增。所以这里最好做平滑处理。
我们每次只会同时给指定数量的连接下发 reconnect 消息 ,然后 等待几秒后再接着下发。
虽然Server 下发了 重连消息,但有时候可能因为各种网络原因,客户端并没有收到,或者客户端收到了,但是由于某些原因没有断开连接,也就不会重新建立连接。
这时我们就需要在服务端主动 close 掉,这里也同样做了平滑处理,每次只close 指定数量的channel 。
后面就一直不停的检测是否还存在有效连接,如果有的话等待 250 ms 再重新检测,不过检测时间最多 3秒
4.Netty 的 shutdownGracefully
Netty 自身提供了优雅退出的方式,那就是 EventExecutorGroup 的 shutdownGracefully() 方法
NioEventLoopGroup 实际是 NioEventLoop 的线程组,它的优雅退出比较简单,直接遍历 EventLoop 数组,循环调用它们的 shutdownGracefully 方法。
这里再简单介绍下 netty shutdownGracefully 的实现原理:
最终调用的是 SingleThreadEventExecutor
里面的 shutdownGracefully
这里贴一下里面的核心代码
这段代码考虑了多线程同时调用关闭的情况,使用 自旋 + CAS 的方式修改当前NioEventLoop所关联的线程的状态(volatile修饰的成员变量state)。
这里并没有执行具体的关闭操作。其中的关键点,就是将线程状态修改为ST_SHUTTING_DOWN。
NioEventLoop所关联的线程总共有5个状态 :
private static final int ST_NOT_STARTED = 1; // 线程还未启动
private static final int ST_STARTED = 2; // 线程已经启动
private static final int ST_SHUTTING_DOWN = 3; // 线程正在关闭
private static final int ST_SHUTDOWN = 4; // 线程已经关闭
private static final int ST_TERMINATED = 5; // 线程已经终止
完成状态修改之后,剩下的操作主要在 NioEventLoop 中进行
在 NioEventLoop 里面最重要的就是 run 方法, 里面一直在不停的循环 select 、处理 IO 事件和 task。
在每次循环的最后,都会去 check 一下 线程的状态,如果是 ST_SHUTTING_DOWN ,就会执行 closeAll 方法
主要做的事是 把注册在 selector 上的所有 Channel 都关闭,循环调用 Channel Unsafe 的 close 方法,但是有些 Channel 正在发送消息,暂时还不能关,需要稍后再执行。
NioEventLoop 执行完 closeAll()操作之后,需要调用 confirmShutdown 看是否真的能够退出
在 NioEventLoop 的 run 方法中,已经调用了 runAllTasks 方法,随后在 confirmShutdown 中又再次调用了 runAllTasks 方法。
这是因为 为了防止schedule task 或者用户自定义的 task 执行过多占用了 NioEventLoop 线程的调度资源,Netty 里面有个 IO ratio ,默认是 50,表示 NioEventLoop 线程 I/O 操作和非 I/O 操作时间的比例。有了执行时间限制,因此可能会导致已经到期的定时任务、普通任务没有执行完,需要等待下次 Selector 轮询继续执行。在线程退出之前,需要对本该执行但是没有执行完成的 Task 进行扫尾处理,所以在 confirmShutdown 中再次调用了 runAllTasks 方法。
至此,Netty 的线程才正式退出。
本文详细介绍了SONA长连接网关中是如何实现服务优雅关闭的,在后续的系列文章中会对网关中的其他技术细节进行详细的介绍。
目前sona已经在比心的github仓库上开源,仓库地址:
GitHub - BixinTech/sona: Sona 平台是一个搭建语音房产品的全端解决方案,包含了房间管理、实时音视频、房间IM、长连接网关等能力。Sona 平台是一个搭建语音房产品的全端解决方案,包含了房间管理、实时音视频、房间IM、长连接网关等能力。 - GitHub - BixinTech/sona: Sona 平台是一个搭建语音房产品的全端解决方案,包含了房间管理、实时音视频、房间IM、长连接网关等能力。https://github.com/BixinTech/sona
欢迎你访问我们的项目,有任何想交流的想法可以留言联系我们。
往期阅读:
从0到1快速了解netty长连接网关协议_聊天室程序猿的博客-CSDN博客
详解netty长连接网关请求处理模型_聊天室程序猿的博客-CSDN博客
详解IM网关连接检测的原理与实现_聊天室程序猿的博客-CSDN博客
比心聊天室的架构演进_聊天室程序猿的博客-CSDN博客