![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=1739290897-oZ498JXIrar9rS7CF5fYX0S3VJvhc4iH-0-1bc57f8886d5b6167fc6e7a8f3f73dc4)
它的执行结果如图1-8所示。
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/29_2.jpg?sign=1739290897-XNo2tjsSWUtIwTuH4PBUrhABOlvgTZBi-0-5837fa7e0f5f63a18236b906afe97062)
图1-8 ShutdownHook执行结果
除了注册ShutdownHook,还可以通过监听信号量并注册SignalHandler的方式实现优雅退出,它的工作原理如图1-9所示。
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/30_1.jpg?sign=1739290897-sdfzAItcy0n6muymqs6dGBTlLLNeVMfr-0-9542373e6b3c3b08c6864d403e918ae8)
图1-9 SignalHandler的工作原理
(1)启动应用进程的时候,初始化Signal实例,代码如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/30_2.jpg?sign=1739290897-sQbrm2fYv6ZTLV8zszFfYEcIIkzoL4Hb-0-05b6e9df70cbd20a119dde0c2fb8b782)
其中Signal构造函数的参数为String字符串,它代表了操作系统支持的信号量列表(此处注意:不同操作系统支持的信号量不同),如表1-1所示为Linux支持的一些常用信号量。
表1-1 Linux支持的一些常用信号量
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/30_3.jpg?sign=1739290897-kn4lIVp97pm75IBm4lBxl1pf6khdCuuX-0-753b7a74e9b65a6ee8bb746c2daeb194)
(2)根据操作系统的名称来获取对应的信号名称:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/31_1.jpg?sign=1739290897-XdmGFjqVKqvUijU2KiMG9mOQLJRymxOF-0-f2a8f250aa1af8bf131aa739c21560fa)
判断是否是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=1739290897-nVPLwUk15FniCtPy4cih0FXHUycyEjDE-0-448e7d399ee15f551060f694d9bdb26e)
(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=1739290897-UgRZMhrXCCM37BU75wOXpE5tpqwboQSv-0-e0d40194d79f3a50e1ab0651139f1fd1)
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/32_1.jpg?sign=1739290897-aOCK5P7QYZfiAlgj1tvbNnD4ShO1E0MU-0-4950ef07d193e5fccb08ff6d0ec80134)
在Windows上按Ctrl+C组合键停止进程,执行结果如图1-10所示。
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/32_2.jpg?sign=1739290897-PIPZx8hczYZAbmXUOqXhwspVJ1YUf8qr-0-3f6fe87852ee8a71cc586ee9393d4756)
图1-10 模拟SignalHandler阻塞执行结果
通过线程堆栈分析,发现代码阻塞在SIGINT handler中,如图1-11所示。
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/32_3.jpg?sign=1739290897-OP5kNHCSdoyF7I2hlwGOpujXaoD6i4DK-0-7d3b69e13ee3004f96d34dbebc730725)
图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=1739290897-hhbLH8EDJGF7WqQ0t6fZt8wKUkLJN19d-0-3997e9ca6400411414b1a76818927a2e)
图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=1739290897-p4csPM4OnNWFoL0yljFSgF0z1Skzf3x6-0-7703b04693486bc4a649fdf0d8fafae1)
除了无参的 shutdownGracefully方法,还可以指定退出的超时时间和周期,相关接口定义如图1-13所示。
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/34_2.jpg?sign=1739290897-EkSyb5iEQ4ZKCOmoZHmwIV71ivo85YTf-0-445d237a0628804efaabf7ca99923cc5)
图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=1739290897-uw9aHd00EG7avyNCrmlSgDE1MKH87XAa-0-645826cbc428c860469d309916ea2663)
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=1739290897-UMuEhR3muCAiq36kgk1cJ8MTHEjINJ4v-0-f95c5a551c8f8258439cafe326d2d58a)
从上述代码可以看出,采用 AtomicIntegerFieldUpdater的 compareAndSet对新老线程状态进行修改,如果在修改当前线程时发现状态已经被别的线程修改过,则继续自旋,直到发现线程状态已经处于ST_SHUTTING_DOWN、ST_SHUTDOWN和ST_TERMINATED状态,或者自己的更新操作成功,才会退出循环。
完成状态修改之后,剩下的操作主要在NioEventLoop中进行,代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/35_3.jpg?sign=1739290897-ywUQztV1nS7w8FAAGLHDhmwkWgpwBwpU-0-64eabb2d7d07f168df8aef14bc73dc57)
继续分析 closeAll 的实现,它的原理是把注册在 selector 上的所有 Channel 都关闭,核心代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/36_1.jpg?sign=1739290897-367St98cYlWirRkcfOTYIH8FOnpZdbhW-0-7f1f2bc2abacda02528da7ba13f93591)
循环调用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=1739290897-cFJushNd4gsBkX6B3mcHWmtwBMrPfohH-0-fd4090c189f85b53f3ce90264f757603)
(2)将发送队列清空,不再允许发送新的消息:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/37_1.jpg?sign=1739290897-dLP4vXhc4iojDF4farj4dO3Va4FGq34G-0-7d952a50f27b14da8e14fa3e5afcf19f)
(3)调用NioSocketChannel的doClose方法,关闭链路:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/37_2.jpg?sign=1739290897-jHGXL31XMh8FP2C4sXezN6WT6NiAWhwj-0-f92ce2c82900925826d21fc16509af30)
(4)调用pipeline的fireChannelInactive,触发链路关闭通知事件:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/37_3.jpg?sign=1739290897-BHctyqr1kTr05h2Tuh5lRSB1UnvwX25M-0-9c89ead1b1c5524b1cd1f7fb35363c39)
(5)调用AbstractNioChannel的Deregister,从多路复用器上取消selectionKey:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/37_4.jpg?sign=1739290897-Jjan9O609Y89gyIdVhUnZeYBIQTpbvnl-0-ebc3a3be61bbffca6137134501590b62)
(6)调用ChannelOutboundBuffer的close方法,释放发送队列中所有尚未完成发送的ByteBuf(关闭之前没有被flushed的message),等待GC:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/37_5.jpg?sign=1739290897-7weKFMWPXY98fb0MdQ289aB7QKEcFDRO-0-ae4d0998475358de15913ad3afcfa644)
执行完资源释放和连接关闭操作之后,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=1739290897-eaD9iJeq2LKTLKSGGUx98b7J2LxZA8tr-0-f8b3a0014febe2337ea93a661d1e11d9)
在confirmShutdown方法中,执行如下操作。
(1)执行尚在TaskQueue中排队的Task,代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/38_2.jpg?sign=1739290897-dxEzkQz74zGiK6xsMEUDIQzQqZQgbqzO-0-7061d8b7ebc70dfe6b8f1f4ec5dd7252)
(2)执行注册到NioEventLoop中的ShutdownHook,代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/38_3.jpg?sign=1739290897-KgKuAB7HIROxbYpzOFfqD2PLi2ZVWOts-0-908a621b9270325cca950cbbad733f89)
(3)判断是否到达优雅退出的指定超时时间,如果达到或者过了超时时间,则立即退出,代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/39_1.jpg?sign=1739290897-9xlYhINCuvautci5UNKSueCbCO3I2PHU-0-ca3ed4f0f6e8b4973b9925eca90e2087)
(4)如果没到达指定的超时时间,暂时不退出,每隔100ms检测一下是否有新的任务加入,有新任务则继续执行:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/39_2.jpg?sign=1739290897-aXdrwVHBJf35tpbF89hJM5R9ELeg1z81-0-2df54be7f4857ba2353879d10c2d9136)
当confirmShutdown返回true,NioEventLoop线程正式退出,Netty的优雅退出完成,代码示例如下(NioEventLoop的run方法):
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/39_3.jpg?sign=1739290897-Q5Ppxfz2JgBO0AI5KETA4idaY7Dmd1Sv-0-995d80d94f97748068a97317739aef0f)
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)接口,指定退出的超时时间,以防止因为一些任务执行被阻塞而无法正常退出。