关于Netty 的退出机制

前几天很走心接到一个电话,问了个问题问题,关于Netty 的优雅关闭。

我当时蒙圈了一会,想都没想就说了句

Runtime.getRuntime().exit(0);

呵呵,这句是不对的,电话完毕之后我就猛找资料,看了看文章,发现很受用了,也写了下来,大家一起学习学习

Kill -9 PID带来的问题

大家都知道 Kill -9 

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

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

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

JAVA优雅退出

Runtime.getRuntime().addShutdownHook

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

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

Netty的优雅退出

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

2.0.1. 信号简介

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

 

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

 

信号名称

用途

SIGKILL

终止进程,强制杀死进程

SIGTERM

终止进程,软件终止信号

SIGTSTP

停止进程,终端来的停止信号

SIGPROF

终止进程,统计分布图用计时器到时

SIGUSR1

终止进程,用户定义信号1

SIGUSR2

终止进程,用户定义信号2

SIGINT

终止进程,中断进程

SIGQUIT

建立CORE文件终止进程,并且生成core文件

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

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

2.0.2. Java程序的优雅退出

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

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

Signal sig = new Signal(getOSSignalType());

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

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

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

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

Signal.handle(sig, shutdownHandler);

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

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

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

 

第五步,接收到进程退出信号后,在回调的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");
		}
}

 

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

2.0.3. Netty的优雅退出

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

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

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

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

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

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

除特别注明外,本站所有文章均为duzhi原创,转载请注明出处来自https://www.duzhi.me/article/37.html

联系我们

******

在线咨询:点击这里给我发消息

邮件:ashang.peng#aliyun.com

QR code