![Netty进阶之路:跟着案例学Netty](https://wfqqreader-1252317822.image.myqcloud.com/cover/642/25462642/b_25462642.jpg)
1.2 Netty优雅退出机制
在Linux上通常会通过kill-9 pid的方式强制将某个进程杀掉,这种方式简单高效,因此很多程序的停止脚本经常会使用kill-9 pid的方式。
无论是Linux的kill-9 pid还是Windows的taskkill/f/pid强制进程退出,都会带来一些副作用,对应用软件而言其效果等同于突然掉电,可能会导致如下问题。
(1)缓存中的数据尚未持久化到磁盘中,导致数据丢失。
(2)正在进行文件的写(write)操作,没有更新完成,突然退出,导致文件损坏。
(3)线程的消息队列中尚有接收到的请求消息还没来得及处理,导致请求消息丢失。
(4)数据库操作已经完成,例如账户余额更新,准备返回应答消息给客户端时,消息尚在通信线程的发送队列中排队等待发送,进程强制退出导致应答消息没有返回给客户端,客户端发起超时重试,会带来重复更新问题。
(5)句柄资源没有及时释放等其他问题。
1.2.1 Java优雅退出机制
Java的优雅停机通常通过注册JDK的ShutdownHook来实现,当系统接收到退出指令时,首先标记系统处于退出状态,不再接收新的消息,然后将积压的消息处理完,最后调用资源回收接口将资源销毁,各线程退出执行。
通过JDK ShutdownHook实现的优雅退出代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/29_1.jpg?sign=1738953015-oEsf0t7KxcnTrcKXgpPyyMASUSEnYTgw-0-9a1313953d097165d8ffc54bbf6987a3)
它的执行结果如图1-8所示。
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/29_2.jpg?sign=1738953015-6olKT4AjOMiPnAIUR7nox4Fer9jBYuXU-0-9dc0afc2dcc2ce27bf4e65b0dc0db048)
图1-8 ShutdownHook执行结果
除了注册ShutdownHook,还可以通过监听信号量并注册SignalHandler的方式实现优雅退出,它的工作原理如图1-9所示。
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/30_1.jpg?sign=1738953015-L9PAX7GRniGS0u6kbwdLyn2jtuo5bGn3-0-a60f7bfd4bb6c7dac4c332ca67342f70)
图1-9 SignalHandler的工作原理
(1)启动应用进程的时候,初始化Signal实例,代码如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/30_2.jpg?sign=1738953015-u5FZwYrGwotib7yYCWHUrxDcHigpjA0Z-0-da0945e236229b0af0f3622438f87540)
其中Signal构造函数的参数为String字符串,它代表了操作系统支持的信号量列表(此处注意:不同操作系统支持的信号量不同),如表1-1所示为Linux支持的一些常用信号量。
表1-1 Linux支持的一些常用信号量
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/30_3.jpg?sign=1738953015-W2ebwRgVb9QKsJBDKf9B7KT3tiErodKu-0-7f9c6ceddd614b9dc8bb602852c7e57e)
(2)根据操作系统的名称来获取对应的信号名称:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/31_1.jpg?sign=1738953015-conNfMG3jh1R4NsgNexERvbvSiPW1iwv-0-10913cf3b322a4f38a3e4caa6c70ecac)
判断是否是Windows操作系统,如果是则选择SIGINT,接收Ctrl+C中断的指令,否则选择TERM信号,接收SIGTERM(等价于kill pid)指令(备注:这里仅是支持Windows和Linux操作系统的代码示例)。
(3)将实例化之后的SignalHandler注册到JDK的Signal,一旦Java进程接收到kill pid或Ctrl+C,则回调handle接口:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/31_2.jpg?sign=1738953015-USBGFsIKZDNFG9pNtUA9div5y9FubCxF-0-c5ab1d285b7d71eb6914ccfbb98cc964)
(4)在接收到信号回调的handle接口中,判断信号量的类型,如果是SIGTERM,则执行应用的优雅退出操作,对于 Netty,需要调用 EventLoopGroup 的 shutdownGracefully方法,释放通信层资源。
1.2.2 Java优雅退出的注意点
对于通过注册ShutdownHook实现的优雅退出,需要注意如下几点,防止踩坑。
(1)ShutdownHook在某些情况下并不会被执行,例如JVM崩溃、无法接收信号量和kill-9 pid等。
(2)当存在多个ShutdownHook时,JVM无法保证它们的执行先后顺序。
(3)在JVM关闭期间不能动态添加或者去除ShutdownHook。
(4)不能在ShutdownHook中调用System.exit(),它会卡住JVM,导致进程无法退出。
对于采用注册 SignalHandler 实现优雅退出的程序,在 handle 接口中一定要避免阻塞操作,否则它会导致已经注册的 ShutdownHook无法执行,系统也无法退出,代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/31_3.jpg?sign=1738953015-ejB0UG5Y17fEsVBhvbQpqyXL8o4LbDVb-0-48204e9bf027aca1489d952721a91e2d)
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/32_1.jpg?sign=1738953015-EOsUOuazxclidfatyqPjqVOsmndErQhk-0-58dc8e559351076c9d2d7509d49af739)
在Windows上按Ctrl+C组合键停止进程,执行结果如图1-10所示。
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/32_2.jpg?sign=1738953015-Lmv050FZ4sMmOGDmONltwUnHKCdk38SU-0-a50c8631a155fe3453e2875a5392ead7)
图1-10 模拟SignalHandler阻塞执行结果
通过线程堆栈分析,发现代码阻塞在SIGINT handler中,如图1-11所示。
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/32_3.jpg?sign=1738953015-JQRtqbSIWtGCCYCUDn4GnHjq3WdIBy5X-0-2ff07542456c1658a8563569f9664772)
图1-11 模拟SignalHandler阻塞线程堆栈
由于SignalHandler发生了阻塞,导致ShutdownHook无法执行,因此没有打印ShutdownHook执行相关日志。如果SignalHandler执行的操作比较耗时,建议异步或放到ShutdownHook中执行。
1.2.3 Netty优雅退出机制
在实际项目中,Netty作为高性能的异步 NIO通信框架,往往作为基础通信框架负责各种协议的接入、解析和调度等,例如在RPC和分布式服务框架中,往往会使用Netty作为内部私有协议的基础通信框架。
当应用进程优雅退出时,作为通信框架的Netty也需要优雅退出,主要原因如下。
(1)尽快释放NIO线程和句柄等资源。
(2)如果使用flush做批量消息发送,需要将积压在发送队列中的待发送消息发送完成。
(3)正在写或者读的消息,需要继续处理。
(4)设置在NioEventLoop线程调度器中的定时任务,需要执行或清理。
下面看下Netty优雅退出涉及的主要操作和资源对象,如图1-12所示。
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/33_1.jpg?sign=1738953015-SdeqGP0IBjFcwKnjP0v91Gc36lw0xWZ9-0-3bb6194b946cbbeb314d16d16b1163fb)
图1-12 Netty优雅退出涉及的主要操作和资源对象
Netty优雅退出总结起来有如下三大类操作。
(1)把 NIO线程的状态位设置成 ST_SHUTTING_DOWN,不再处理新的消息(不允许再对外发送消息)。
(2)退出前的预处理操作:把发送队列中尚未发送或者正在发送的消息发送完(备注:不保证能够发送完)、把已经到期或在退出超时之前到期的定时任务执行完成、把用户注册到NIO线程的退出Hook任务执行完成。
(3)资源的释放操作:所有Channel的释放、多路复用器的去注册和关闭、所有队列和定时任务的清空取消,最后是EventLoop线程的退出。
Netty 优雅退出的接口和总入口是 EventLoopGroup,调用它的 shutdownGracefully 方法即可,相关代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/34_1.jpg?sign=1738953015-knZhwSv0gKPxDuiIkZb9LbGaa1jIITYs-0-4fdd8c0126d35d2e0b0286026b445079)
除了无参的 shutdownGracefully方法,还可以指定退出的超时时间和周期,相关接口定义如图1-13所示。
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/34_2.jpg?sign=1738953015-K818CBcLN0KSxlNlTFDr2FSo5n0h28sZ-0-991e62822073772ae770dc93cc2a7728)
图1-13 EventLoopGroup优雅退出相关接口定义
其中,强制退出已经被标注为废弃,在实际项目中尽量不要使用。当 JVM 的ShutdownHook被触发之后,调用所有EventLoopGroup实例的 shutdownGracefully方法进行优雅退出。由于Netty自身对优雅退出有较完善的支持,所以实现起来相对比较简单。
1.2.4 Netty优雅退出原理和源码分析
Netty优雅退出涉及线程组、NIO线程、Channel和定时任务等,底层实现细节比较复杂,下面我们就层层分解,通过源码分析来了解它的实现原理。
1.NioEventLoopGroup
NioEventLoopGroup 实际上是 NioEventLoop 线程组,它的优雅退出比较简单,可直接遍历EventLoop数组,循环调用它们的shutdownGracefully方法,源码如下(MultithreadEvent-ExecutorGroup的shutdownGracefully方法):
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/35_1.jpg?sign=1738953015-E5WQ7MV739KAQ6p4WFa6KCoBDPfzWEOV-0-041713cf3beb1354e312a4414e6425a9)
2.NioEventLoop
调用NioEventLoop的shutdownGracefully方法,首先要修改线程状态为正在关闭状态,它的实现在父类SingleThreadEventExecutor中,需要注意的是,修改线程状态位时要对并发调用做保护,因为调用shutdownGracefully方法可能由NioEventLoop线程发起,也可能多个应用线程并发执行。对于线程状态的修改需要做并发保护,最简单的策略就是加锁,或者采用原子类加自旋的方式避免加锁,Netty 5采用的是加锁策略,Netty 4则采用后者,Netty 4的处理逻辑如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/35_2.jpg?sign=1738953015-imLIChWoK6ZyCmJx0AGk9R5arY81wBFb-0-2eb74dcedd539361b86c172d805907ea)
从上述代码可以看出,采用 AtomicIntegerFieldUpdater的 compareAndSet对新老线程状态进行修改,如果在修改当前线程时发现状态已经被别的线程修改过,则继续自旋,直到发现线程状态已经处于ST_SHUTTING_DOWN、ST_SHUTDOWN和ST_TERMINATED状态,或者自己的更新操作成功,才会退出循环。
完成状态修改之后,剩下的操作主要在NioEventLoop中进行,代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/35_3.jpg?sign=1738953015-yXhG9zb47meAJD8PpJV4TYXnKDaV2LJv-0-6af5152bd73f8c88aad8a00bb0266c14)
继续分析 closeAll 的实现,它的原理是把注册在 selector 上的所有 Channel 都关闭,核心代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/36_1.jpg?sign=1738953015-YH4WyEjf6XYmvyN2Mkc4THehvzP6QuK2-0-4314e0396dc6a1e622a6b7884acdfa79)
循环调用Channel Unsafe的close方法,下面跳转到Unsafe中,对close方法进行分析。
3.AbstractUnsafe
AbstractUnsafe的close方法主要完成如下几个功能。
(1)判断当前链路是否有消息正在发送,如果有则将SelectionKey的去注册操作封装成Task放到eventLoop中稍后再执行:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/36_2.jpg?sign=1738953015-UkKng3deterCTGsHKF6d2mYdJ5ux03DA-0-3fb24aa195bb308a6c87f542f05813c8)
(2)将发送队列清空,不再允许发送新的消息:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/37_1.jpg?sign=1738953015-XucFvASSKWO3fO44MuMBad8Q3hlrSBW5-0-29e16fed6fc09c74950aff5a0aa9c78e)
(3)调用NioSocketChannel的doClose方法,关闭链路:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/37_2.jpg?sign=1738953015-ofdiwFZyecjWnWCfOT8TTJYoL9sRWziE-0-12b748931ee73da974ab7f3ef763c4e2)
(4)调用pipeline的fireChannelInactive,触发链路关闭通知事件:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/37_3.jpg?sign=1738953015-GDp2rRSH6XQyDnTJ1cVb8TiZeiBjfCiO-0-6d37f6292db346d66996455cad9b1ea1)
(5)调用AbstractNioChannel的Deregister,从多路复用器上取消selectionKey:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/37_4.jpg?sign=1738953015-fK49BCEjcPYNWMxvwJ9gYxbx4WwFhLof-0-ff1a94f6df707082e9ba67f90a8f4638)
(6)调用ChannelOutboundBuffer的close方法,释放发送队列中所有尚未完成发送的ByteBuf(关闭之前没有被flushed的message),等待GC:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/37_5.jpg?sign=1738953015-DcVHCbdsleKqNJp4oCdETxq1LoXucdmP-0-f1497b5c576e7cb5369428c02b0deb56)
执行完资源释放和连接关闭操作之后,NioEventLoop 还有扫尾工作需要执行,NioEventLoop 除了 I/O 读写,还负责定时任务执行、ShutdownHook(备注:此处非 JDK原生的ShutdownHook)的执行等,如果此时有到期的定时任务,即使Channel已经关闭,但是仍然需要继续执行,线程不能退出,下面继续分析TaskQueue的退出处理流程。
4.TaskQueue
NioEventLoop执行完closeAll()操作,需要调用confirmShutdown看是否真的可以退出,它的判断逻辑如下(NioEventLoop run方法):
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/38_1.jpg?sign=1738953015-DGGzAyQ83alEKFz7ZD2kNnvNl2KkPQEJ-0-691421116a67f4213b5fa9e037a76d01)
在confirmShutdown方法中,执行如下操作。
(1)执行尚在TaskQueue中排队的Task,代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/38_2.jpg?sign=1738953015-aEzGLwlyKgy8NkIbghuepMas8JTPxT0u-0-8106a0f1cc56cbd8148271cf8cf85bf4)
(2)执行注册到NioEventLoop中的ShutdownHook,代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/38_3.jpg?sign=1738953015-bIXKF7wskpivBronIxTORsJRhTrk2FVl-0-02f6bcc7e3d978a794442d3bd5b9027c)
(3)判断是否到达优雅退出的指定超时时间,如果达到或者过了超时时间,则立即退出,代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/39_1.jpg?sign=1738953015-E9lKgpbixFmwUuIm5WfOSuDjcTr94yfD-0-4a1b7d843727a5092be3529affc07fc2)
(4)如果没到达指定的超时时间,暂时不退出,每隔100ms检测一下是否有新的任务加入,有新任务则继续执行:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/39_2.jpg?sign=1738953015-jxRKXwEPZebTyRqTwDD7KS65l5IjNLYg-0-a884574a5c494467495fc82bc8c70928)
当confirmShutdown返回true,NioEventLoop线程正式退出,Netty的优雅退出完成,代码示例如下(NioEventLoop的run方法):
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/39_3.jpg?sign=1738953015-1ObsuZyADAP47mgRyxtuqzSrMHtF7S9e-0-d665cf47b278c2d1efe859e5ca78b540)
1.2.5 Netty优雅退出的一些误区
不同版本Netty优雅退出的实现策略不同,特别是大版本之间(Netty 3.X/4.X/5.X)的差异还是比较大的,但是都保证不了优雅退出时所有消息队列排队的消息能够处理完,主要原因如下。
(1)待发送的消息:调用优雅退出方法之后,不会立即关闭链路。ChannelOutboundBuffer中的消息可以继续发送,本轮发送操作执行完成之后,无论是否还有消息尚未发送出去,在下一轮的 Selector轮询中,链路都将被关闭,没有发送完成的消息将会被释放和丢弃。
(2)需要发送的新消息:由于应用线程可以随时通过调用 Channel 的 write 系列接口发送消息,即便ShutdownHook触发了Netty的优雅退出方法,在Netty优雅退出方法执行期间,应用线程仍然有可能继续调用Channel发送消息,这些消息将发送失败。
应用注册在 NioEventLoop 线程上的普通 Task、Scheduled Task (定时任务)和ShutdownHook,也无法保证被完全执行,这取决于优雅退出超时时间和任务的数量,以及执行速度。
因此,应用程序的正确性不能完全依赖 Netty的优雅退出机制,需要在应用层面做容错设计和处理。例如,服务端在返回响应之前关闭了,导致响应没有发送给客户端,这可能会触发客户端的 I/O异常,或者恰好发生了超时异常,客户端需要对 I/O或超时异常做容错处理,采用Failover重试其他可用的服务端,而不能寄希望于服务端永远正确。Netty优雅退出更重要的是保证资源、句柄和线程的快速释放,以及相关对象的清理。
Netty 优雅退出通常用于应用进程退出时,在应用的 ShutdownHook 中调用EventLoopGroup的shutdownGracefully(long quietPeriod,long timeout,TimeUnit unit)接口,指定退出的超时时间,以防止因为一些任务执行被阻塞而无法正常退出。