Netty 如何优雅地实现退出机制及其原理

Netty 如何优雅地实现退出机制及其原理

进程的优雅退出

Kill -9 PID 带来的问题

在 Linux 上通常会通过 kill -9 pid 的方式强制将某个进程杀掉,这种方式简单高效,因此很多程序的停止脚本经常会选择使用 kill -9 pid 的方式。

无论是 Linux 的 Kill -9 pid 还是 windows 的 taskkill /f /pid 强制进程退出, 都会带来一些副作用:对应用软件而言其效果等同于突然掉电,可能会导致如下一些问题:

  1. 缓存中的数据尚未持久化到磁盘中,导致数据丢失;
  2. 正在进行文件的 write 操作,没有更新完成,突然退出,导致文件损坏;
  3. 线程的消息队列中尚有接收到的请求消息还没来得及处理,导致请求消息丢失;
  4. 数据库操作已经完成,例如账户余额更新,准备返回应答消息给客户端时,消息尚在通信线程的发送队列中排队等待发送,进程强制退出导致应答消息没有返回给客户端,客户端发起超时重试,会带来重复更新问题;
  5. 其它问题等…

JAVA 优雅退出

Java 的优雅停机通常通过注册 JDK 的 ShutdownHook 来实现,当系统接收到退出指令后,首先标记系统处于退出状态,不再接收新的消息,然后将积压的消息处理完,最后调用资源回收接口将资源销毁,最后各线程退出执行。

Netty 如何优雅地实现退出机制及其原理

通常优雅退出需要有超时控制机制,例如 30S,如果到达超时时间仍然没有完成退出前的资源回收等操作,则由停机脚本直接调用 kill -9 pid,强制退出。

如何实现 Netty 的优雅退出

要实现 Netty 的优雅退出,首先需要了解通用 Java 进程的优雅退出如何实现。下面我们先讲解下优雅退出的实现原理,并结合实际代码进行讲解。最后看下如何实现 Netty 的优雅退出。

信号简介

信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的,它是进程间一种异步通信的机制。以 Linux 的 kill 命令为例,kill -s SIGKILL pid (即 kill -9 pid) 立即杀死指定 pid 的进程,SIGKILL 就是发送给 pid 进程的信号。

信号具有平台相关性,Linux 平台支持的一些终止进程信号如下所示:

Netty 如何优雅地实现退出机制及其原理

Windows 平台存在一些差异,它的一些信号举例如下:SIGINT(Ctrl+C 中断)、SIGILL、SIGTERM (kill 发出的软件终止)、SIGBREAK (Ctrl+Break 中断)。

信号选择:为了不干扰正常信号的运作,又能模拟 Java 异步通知,在 Linux 上我们需要先选定一种特殊的信号。通过查看信号列表上的描述,发现 SIGUSR1 和 SIGUSR2 是允许用户自定义的信号, 我们可以选择 SIGUSR2,为了测试方便,在 Windows 上我们可以选择 SIGINT。

Java 程序的优雅退出

首先看下通用的 Java 进程优雅退出的流程图:

Netty 如何优雅地实现退出机制及其原理

1, 应用进程启动的时候,初始化 Signal 实例,它的代码示例如下:

Signal sig = new Signal(getOSSignalType());

其中 Signal 构造函数的参数为 String 字符串,也就是 2.1.1 小节中介绍的信号量名称。

2, 根据操作系统的名称来获取对应的信号名称,代码如下:

private String getOSSignalType()
 {
 return System.getProperties().getProperty("os.name").
		 toLowerCase().startsWith("win") ? "INT" : "USR2";
 }

判断是否是 windows 操作系统,如果是则选择 SIGINT,接收 Ctrl+C 中断的指令;否则选择 USR2 信号,接收 SIGUSR2(等价于 kill -12 pid)指令。

3, 将实例化之后的 SignalHandler 注册到 JDK 的 Signal,一旦 Java 进程接收到 kill -12 或者 Ctrl+C 则回调 handle 接口,代码示例如下:

Signal.handle(sig, shutdownHandler);

其中 shutdownHandler 实现了 SignalHandler 接口的 handle(Signal sgin) 方法,代码示例如下:

Netty 如何优雅地实现退出机制及其原理

4, 在接收到信号回调的 handle 接口中,初始化 JDK 的 ShutdownHook 线程,并将其注册到 Runtime 中,示例代码如下:

private void invokeShutdownHook()
 {
	Thread t = new Thread(new ShutdownHook(), "ShutdownHook-Thread");
	Runtime.getRuntime().addShutdownHook(t);
 }

5,接收到进程退出信号后,在回调的 handle 接口中执行虚拟机的退出操作,示例代码如下:

Runtime.getRuntime().exit(0);

虚拟机退出时,底层会自动检测用户是否注册了 ShutdownHook 任务,如果有,则会自动将 ShutdownHook 线程拉起,执行它的 Run 方法,用户只需要在 ShutdownHook 中执行资源释放操作即可,示例代码如下:

class ShutdownHook implements Runnable
{
	@Override
	public void run() {
		System.out.println("ShutdownHook execute start...");
		System.out.print("Netty NioEventLoopGroup shutdownGracefully...");
		try {
			TimeUnit.SECONDS.sleep(10);// 模拟应用进程退出前的处理操作 
		} catch (InterruptedException e) {
				e.printStackTrace();
		}
		System.out.println("ShutdownHook execute end...");
	System.out.println("Sytem shutdown over, the cost time is 10000MS");
		}
}

下面我们在 Windows 环境中对通用的 Java 优雅退出程序进行测试,打开 CMD 控制台,拉起待测试程序,如下所示:

启动进程:

Netty 如何优雅地实现退出机制及其原理

查看线程信息,发现注册的 ShutdownHook 线程没有启动,符合预期:

Netty 如何优雅地实现退出机制及其原理

在控制台执行 Ctrl+C,使进程退出,示例如下:

Netty 如何优雅地实现退出机制及其原理

如上图所示,我们定义的 ShutdownHook 线程在 JVM 退出时被执行,作为测试程序,它休眠 10S 之后退出,控制台打印的相关信息如下:

Netty 如何优雅地实现退出机制及其原理

下面我们总结下通用的 Java 程序优雅退出的技术要点:

Netty 如何优雅地实现退出机制及其原理

Netty 的优雅退出

在实际项目中,Netty 作为高性能的异步 NIO 通信框架,往往用作基础通信框架负责各种协议的接入、解析和调度等,例如在 RPC 和分布式服务框架中,往往会使用 Netty 作为内部私有协议的基础通信框架。

当应用进程优雅退出时,作为通信框架的 Netty 也需要优雅退出,主要原因如下:

  1. 尽快的释放 NIO 线程、句柄等资源;
  2. 如果使用 flush 做批量消息发送,需要将积攒在发送队列中的待发送消息发送完成;
  3. 正在 write 或者 read 的消息,需要继续处理;
  4. 设置在 NioEventLoop 线程调度器中的定时任务,需要执行或者清理。

下面我们看下 Netty 优雅退出涉及的主要操作和资源对象:

Netty 如何优雅地实现退出机制及其原理

Netty 的优雅退出总结起来有三大步操作:

  1. 把 NIO 线程的状态位设置成 ST_SHUTTING_DOWN 状态,不再处理新的消息(不允许再对外发送消息);
  2. 退出前的预处理操作:把发送队列中尚未发送或者正在发送的消息发送完、把已经到期或者在退出超时之前到期的定时任务执行完成、把用户注册到 NIO 线程的退出 Hook 任务执行完成;
  3. 资源的释放操作:所有 Channel 的释放、多路复用器的去注册和关闭、所有队列和定时任务的清空取消,最后是 NIO 线程的退出。

下面我们具体看下如何实现 Netty 的优雅退出:

Netty 优雅退出的接口和总入口在 EventLoopGroup,调用它的 shutdownGracefully 方法即可,相关代码如下:

bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();

除了无参的 shutdownGracefully 方法,还可以指定退出的超时时间和周期,相关接口定义如下:

Netty 如何优雅地实现退出机制及其原理

EventLoopGroup 的 shutdownGracefully 工作原理下个章节做详细讲解,结合 Java 通用的优雅退出机制,即可实现 Netty 的优雅退出,相关伪代码如下:

// 统一定义 JVM 退出事件,并将 JVM 退出事件作为主题对进程内部发布 
// 所有需要优雅退出的消费者订阅 JVM 退出事件主题 
// 监听 JVM 退出的 ShutdownHook 被启动之后,发布 JVM 退出事件 
// 消费者监听到 JVM 退出事件,开始执行自身的优雅退出 
// 如果所有的非守护线程都成功完成优雅退出,进程主动退出 
// 如果到了退出的超时时间仍然没正常退出,则由停机脚本通过 kill -9 pid 强杀进程,强制退出

总结一下

JVM 的 ShutdownHook 被触发之后,调用所有 EventLoopGroup 实例的 shutdownGracefully 方法进行优雅退出。由于 Netty 自身对优雅退出有较完善的支持,所以实现起来相对比较简单。

我整理了一些互联网公司java程序员在面试中涉及到的绝大部分架构面试题及答案做成了文档和架构视频资料免费分享给大家(包括Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术资料)也可以关注获得更多的面试资料,节省大家收集的时间! 现在进群571617441免费获取!!

Netty 优雅退出原理分析

Netty 优雅退出涉及到线程组、线程、链路、定时任务等,底层实现细节非常复杂,下面我们就层层分解,通过源码来剖析它的实现原理。

NioEventLoopGroup

NioEventLoopGroup 实际是 NioEventLoop 的线程组,它的优雅退出比较简单,直接遍历 EventLoop 数组,循环调用它们的 shutdownGracefully 方法,源码如下:

Netty 如何优雅地实现退出机制及其原理

NioEventLoop

调用 NioEventLoop 的 shutdownGracefully 方法,首先就是要修改线程状态为正在关闭状态,它的实现在父类 SingleThreadEventExecutor 中,它们的继承关系如下:

Netty 如何优雅地实现退出机制及其原理

SingleThreadEventExecutor 的 shutdownGracefully 代码比较简单,就是修改线程的状态位,需要注意的是修改时需要对并发调用做判断,如果是由 NioEventLoop 自身调用,则不需要加锁,否则需要加锁,代码如下:

Netty 如何优雅地实现退出机制及其原理

解释下为什么要加锁,因为 shutdownGracefully 是 public 的方法,任何能够获取到 NioEventLoop 的代码都可以调用它,在 Netty 中,业务代码通常不需要直接获取 NioEventLoop 并操作它,但是 Netty 对 NioEventLoop 做了比较厚的封装,它不仅仅只能读写消息,还能够执行定时任务,并作为线程池执行用户自定义 Task。因此在 Channel 中将获取 NioEventLoop 的方法开放了出来,这就意味着用户只要能够获取到 Channel,理论上就会存在并发执行 shutdownGracefully 的可能,因此在优雅退出的时候做了并发保护。

完成状态修改之后,剩下的操作主要在 NioEventLoop 中进行,代码如下:

Netty 如何优雅地实现退出机制及其原理

我们继续看下 closeAll 的实现,它的原理是把注册在 selector 上的所有 Channel 都关闭,但是有些 Channel 正在发送消息,暂时还不能关,需要稍后再执行,核心代码如下:

Netty 如何优雅地实现退出机制及其原理

循环调用 Channel Unsafe 的 close 方法,下面我们跳转到 Unsafe 中,对 close 方法进行分析。

AbstractUnsafe

AbstractUnsafe 的 close 方法主要做了如下几件事:

  • 判断当前该链路是否有消息正在发送,如果有则将关闭操作封装成 Task 放到 eventLoop 中稍后再执行:

Netty 如何优雅地实现退出机制及其原理

  • 将发送队列清空,不再允许发送新的消息:

Netty 如何优雅地实现退出机制及其原理

  • 调用 SocketChannel 的 close 方法,关闭链路:

Netty 如何优雅地实现退出机制及其原理

  • 调用 pipeline 的 fireChannelInactive,触发链路关闭通知事件:

Netty 如何优雅地实现退出机制及其原理

  • 最后是调用 deregister,从多路复用器上取消 SelectionKey:

Netty 如何优雅地实现退出机制及其原理

至此,优雅退出流程已经完成,这是否意味着 NioEventLoop 线程可以退出了,其实并非如此。

在此处,只是做了 Channel 的关闭和从 Selector 上的去注册,总结如下:

  1. 通过 inFlush0 来判断当前是否正在发送消息,如果是,则不执行 Channel 关闭动作,放入 NIO 线程的任务队列中稍后再执行 close() 操作;
  2. 因为已经不允许新的发送消息加入,一旦发送操作完成,就执行链路关闭、触发链路关闭事件和从 Selector 上取消注册操作。

之前已经说了,NioEventLoop 除了 I/O 读写之外,还兼具定时任务执行、关闭 ShutdownHook 的执行等,如果此时有到期的定时任务,即使 Chanel 已经关闭,但是仍然需要继续执行,线程不能退出。下面我们具体分析下 TaskQueue 的处理流程。

TaskQueue

NioEventLoop 执行完 closeAll()操作之后,需要调用 confirmShutdown 看是否真的能够退出,它的处理逻辑如下:

执行 TaskQueue 中排队的 Task,代码如下:

Netty 如何优雅地实现退出机制及其原理

执行注册到 NioEventLoop 中的 ShutdownHook,代码如下:

Netty 如何优雅地实现退出机制及其原理

判断是否到达优雅退出的指定超时时间,如果达到或者过了超时时间,则立即退出,代码如下:

Netty 如何优雅地实现退出机制及其原理

如果没到达指定的超时时间,暂时不退出,每隔 100MS 检测下是否有新的任务加入,有则继续执行:

Netty 如何优雅地实现退出机制及其原理

在 confirmShutdown 方法中,夹杂了一些对已经废弃的 shutdown()方法的处理,例如:

Netty 如何优雅地实现退出机制及其原理

调用新的 shutdownGracefully 系列方法,该判断条件是永远都不会成立的,因此对于已经废弃的 shutdown 相关的处理逻辑,不再详细分析。

到此为止,confirmShutdown 方法讲解完毕,confirmShutdown 返回 true,则 NioEventLoop 线程正式退出,Netty 的优雅退出完成,代码如下:

Netty 如何优雅地实现退出机制及其原理

本文到此结束!喜欢的朋友帮忙点点赞和关注一下,感谢支持!!

猜你喜欢

转载自blog.csdn.net/qwe123147369/article/details/91617033