“退出”是每个程序的必备功能,因为太平常,多数程序员都对这不以为然。但在大型分布式应用中,因各模块、服务等都是分布式部署和协作,这种RPC式的场景下,如何让某个模块或服务优雅地“退出”,则远非调用一个Kill指令这么简单。本文将详述NIO框架Netty是如何实现“优雅”地“退出”的。
Kill -9 PID带来的问题
在Linux上通常会通过kill -9 pid的方式强制将某个进程杀掉,这种方式简单高效,因此很多程序的停止脚本经常会选择使用kill -9 pid的方式。
无论是Linux的Kill -9 pid还是windows的taskkill /f /pid强制进程退出,都会带来一些副作用:对应用软件而言其效果等同于突然掉电,可能会导致如下一些问题:
缓存中的数据尚未持久化到磁盘中,导致数据丢失;
正在进行文件的write操作,没有更新完成,突然退出,导致文件损坏;
线程的消息队列中尚有接收到的请求消息还没来得及处理,导致请求消息丢失;
数据库操作已经完成,例如账户余额更新,准备返回应答消息给客户端时,消息尚在通信线程的发送队列中排队等待发送,进程强制退出导致应答消息没有返回给客户端,客户端发起超时重试,会带来重复更新问题;
其它问题等...
Java如何优雅地退出
Java的优雅停机通常通过注册JDK的ShutdownHook来实现,当系统接收到退出指令后,首先标记系统处于退出状态,不再接收新的消息,然后将积压的消息处理完,最后调用资源回收接口将资源销毁,最后各线程退出执行。
通常优雅退出需要有超时控制机制,例如30S,如果到达超时时间仍然没有完成退出前的资源回收等操作,则由停机脚本直接调用kill -9 pid,强制退出。
如何实现Netty的优雅退出
要实现Netty的优雅退出,首先需要了解通用Java进程的优雅退出如何实现。下面我们先讲解下优雅退出的实现原理,并结合实际代码进行讲解。最后看下如何实现Netty的优雅退出。
信号简介
信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的,它是进程间一种异步通信的机制。以Linux的kill命令为例,kill -s SIGKILL pid (即kill -9 pid) 立即杀死指定pid的进程,SIGKILL就是发送给pid进程的信号。
Windows平台存在一些差异,它的一些信号举例如下:SIGINT(Ctrl+C中断)、SIGILL、SIGTERM (kill发出的软件终止)、SIGBREAK (Ctrl+Break中断)。即时通讯聊天软件app开发可以加蔚可云的v:weikeyun24咨询
信号选择:为了不干扰正常信号的运作,又能模拟Java异步通知,在Linux上我们需要先选定一种特殊的信号。通过查看信号列表上的描述,发现 SIGUSR1 和 SIGUSR2 是允许用户自定义的信号,我们可以选择SIGUSR2,为了测试方便,在Windows上我们可以选择SIGINT。
Netty的优雅退出
在实际项目中,Netty作为高性能的异步NIO通信框架,往往用作基础通信框架负责各种协议的接入、解析和调度等,例如在RPC和分布式服务框架中,往往会使用Netty作为内部私有协议的基础通信框架。
当应用进程优雅退出时,作为通信框架的Netty也需要优雅退出,主要原因如下:
尽快的释放NIO线程、句柄等资源;
如果使用flush做批量消息发送,需要将积攒在发送队列中的待发送消息发送完成;
正在write或者read的消息,需要继续处理;
设置在NioEventLoop线程调度器中的定时任务,需要执行或者清理。
Netty的优雅退出总结起来有三大步操作:
把NIO线程的状态位设置成ST_SHUTTING_DOWN状态,不再处理新的消息(不允许再对外发送消息);
退出前的预处理操作:把发送队列中尚未发送或者正在发送的消息发送完、把已经到期或者在退出超时之前到期的定时任务执行完成、把用户注册到NIO线程的退出Hook任务执行完成;
资源的释放操作:所有Channel的释放、多路复用器的去注册和关闭、所有队列和定时任务的清空取消,最后是NIO线程的退出。
一些误区
在实际工作中,由于对优雅退出和资源释放的原理不太清楚,或者对Netty的接口不太了解,很容易把优雅退出和资源释放混淆,导致出现各种问题。
如下案例:本意是想把某个Channel关闭,但是却调用了Channel关联的EventLoop的shutdownGracefully,导致把EventLoop线程和注册在该线程持有的多路复用器上所有的Channel都关闭了。