![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=1739278067-qfz5ptfrr6OexwDGeH5ShuNFERPGclUe-0-8111973fa0974fb260b4c133ab5bc552)
它的执行结果如图1-8所示。
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/29_2.jpg?sign=1739278067-tPF1sKeUNkY4RAEMS0ng6ckmfwJfBSn5-0-a121875f91d456e8bc6d193c3b0b38ce)
图1-8 ShutdownHook执行结果
除了注册ShutdownHook,还可以通过监听信号量并注册SignalHandler的方式实现优雅退出,它的工作原理如图1-9所示。
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/30_1.jpg?sign=1739278067-OnjMKLtvohyQssJZ5zBpjFCiE7Kfnv35-0-c537951373ff092d80b54554218f0b8d)
图1-9 SignalHandler的工作原理
(1)启动应用进程的时候,初始化Signal实例,代码如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/30_2.jpg?sign=1739278067-ON17BkYenlcakWQbxTgj3i44AuZ0jKBR-0-1cf7b2fdbebbd860ce6b3048ee733026)
其中Signal构造函数的参数为String字符串,它代表了操作系统支持的信号量列表(此处注意:不同操作系统支持的信号量不同),如表1-1所示为Linux支持的一些常用信号量。
表1-1 Linux支持的一些常用信号量
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/30_3.jpg?sign=1739278067-BJRQRKONnYxtWRUsfDS5pf8Twe6uD1TN-0-5113d4fb2471fba48477b0534afd6530)
(2)根据操作系统的名称来获取对应的信号名称:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/31_1.jpg?sign=1739278067-eZfH9MPWTIqUU5l2k69pd5SH1gVJ6E1G-0-3b94eabe9572d2885c194b55bda3c2dc)
判断是否是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=1739278067-KEvSeXoOJGzLznsHd4fLApE0KicmNPZf-0-b409d72a1cb17e5f1032f792bbebd0de)
(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=1739278067-nWWAWflPEoxYgvBNehUPwtqt52qIUXY2-0-8fcf2c55aa0e32ac0d492fd8d23220c9)
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/32_1.jpg?sign=1739278067-uSIgy2Ud8DzbNeFA662GcoYLZWmVEK80-0-eceac2fada43c1015c8d13997a0875e4)
在Windows上按Ctrl+C组合键停止进程,执行结果如图1-10所示。
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/32_2.jpg?sign=1739278067-KEx5naiFyhIc9xRpiu92NgwggFdzc7Hx-0-10fb23910018b81688015e668845c0a2)
图1-10 模拟SignalHandler阻塞执行结果
通过线程堆栈分析,发现代码阻塞在SIGINT handler中,如图1-11所示。
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/32_3.jpg?sign=1739278067-3OmIuqo3ShO6wxTYxzGsplKlzwfbYl0X-0-7af264b707caa007d89e4c8805d156b0)
图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=1739278067-TlWKFj6aJYVT2g2qy4z5XQKpoqApFgDw-0-cdb05b86590705f1aa88e0827226f19d)
图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=1739278067-L1quTTse3Js6jrZd8NgbArqZtXDJcuTT-0-98163899a117bcb894b400c5b3443000)
除了无参的 shutdownGracefully方法,还可以指定退出的超时时间和周期,相关接口定义如图1-13所示。
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/34_2.jpg?sign=1739278067-G0CgD01PvMnmZvqz8Vzn9KuDP5o5Fckz-0-47b87caaf52f129da1e17c8f6295c2f4)
图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=1739278067-YvAhw8AGOHs5Mbshj4JjBAoHYrBlznxT-0-6823b62482aca39ced6444ccb93dc0b1)
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=1739278067-7RQs0sGDPWzB7sxA4DXAzOOl79X3O4iD-0-22c570b74a3baa7e8b9a16c41f3d9abd)
从上述代码可以看出,采用 AtomicIntegerFieldUpdater的 compareAndSet对新老线程状态进行修改,如果在修改当前线程时发现状态已经被别的线程修改过,则继续自旋,直到发现线程状态已经处于ST_SHUTTING_DOWN、ST_SHUTDOWN和ST_TERMINATED状态,或者自己的更新操作成功,才会退出循环。
完成状态修改之后,剩下的操作主要在NioEventLoop中进行,代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/35_3.jpg?sign=1739278067-Wsjm9SiI9858bmNjlbHUwYfXvHWKTz8J-0-33c2160a5b23a2c2fe095da9bd6f31d5)
继续分析 closeAll 的实现,它的原理是把注册在 selector 上的所有 Channel 都关闭,核心代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/36_1.jpg?sign=1739278067-jxTwBJMeutykvuzQXmFFF4TvdaqagQMr-0-a1ffd2412bba8f958427563c36fc2c89)
循环调用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=1739278067-Df3CCkbPAunbDZ5RN6Tw4cBfe4hlf69Z-0-4e101b6737090f9a5e534a80a9849a3b)
(2)将发送队列清空,不再允许发送新的消息:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/37_1.jpg?sign=1739278067-wioqR2pvaMhqft822rQOjHLpQBOhCq40-0-6a5ec32240bf5d2bdf49594642890874)
(3)调用NioSocketChannel的doClose方法,关闭链路:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/37_2.jpg?sign=1739278067-LWfcKp7gn9XcbBvZdJzgvxScMYj6370a-0-fff82e0aa53244e316d71e18cf7223d3)
(4)调用pipeline的fireChannelInactive,触发链路关闭通知事件:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/37_3.jpg?sign=1739278067-MYOfw9qTkYUo8mTp63nMlQMjmMwXpk7c-0-36ff7304ff016d28d7be2340a1275709)
(5)调用AbstractNioChannel的Deregister,从多路复用器上取消selectionKey:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/37_4.jpg?sign=1739278067-IRLYw6DMJui83YVahZYuR6jpzdmcqUaW-0-8531635533628801611f699ccdb80129)
(6)调用ChannelOutboundBuffer的close方法,释放发送队列中所有尚未完成发送的ByteBuf(关闭之前没有被flushed的message),等待GC:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/37_5.jpg?sign=1739278067-s8x22OHOtMZRFTdKwpXhsKTBpdOtSjK7-0-137ccfab351fa5fd5de07edb4a3baf86)
执行完资源释放和连接关闭操作之后,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=1739278067-mBpxfUrRxG20GuN0UFnYycnaDRiKkIDg-0-5a9c45264852e45e1bb55e460569a6dc)
在confirmShutdown方法中,执行如下操作。
(1)执行尚在TaskQueue中排队的Task,代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/38_2.jpg?sign=1739278067-7STJ0Unwb2M5gwpaG32SzYeVnB73KgXf-0-9488a0850f83c54eaa590af9ef27c0eb)
(2)执行注册到NioEventLoop中的ShutdownHook,代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/38_3.jpg?sign=1739278067-yd4zro6MNBm2rPnnsu3zvWoHqCBqI7SO-0-a5ddcee0646802ae2d00700f05cb85ab)
(3)判断是否到达优雅退出的指定超时时间,如果达到或者过了超时时间,则立即退出,代码示例如下:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/39_1.jpg?sign=1739278067-86Mb0Qxf0CoYEAlle8B7N9WVnzRBBetZ-0-176211e8059c69c571c98c2042a591de)
(4)如果没到达指定的超时时间,暂时不退出,每隔100ms检测一下是否有新的任务加入,有新任务则继续执行:
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/39_2.jpg?sign=1739278067-Ersg91f2USB2HAyA0hRE0l03QhwbOlc6-0-6e888fbb1fba52eaeecf81b71e7952cd)
当confirmShutdown返回true,NioEventLoop线程正式退出,Netty的优雅退出完成,代码示例如下(NioEventLoop的run方法):
![](https://epubservercos.yuewen.com/8B73A5/13916128705951906/epubprivate/OEBPS/Images/39_3.jpg?sign=1739278067-bR4wHxYoCH9SwYq8duZrP7iSEQ6DAcl3-0-936248b31e8dda28b559314e29e8a5fc)
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)接口,指定退出的超时时间,以防止因为一些任务执行被阻塞而无法正常退出。