JVM安全退出(如何优雅的关闭java服务)

背景

用户:货都到了,购物车里怎么还有刚买的东西,what?
产品:有用户反映,提单完成了,怎么没清购物车,研发赶忙看看是否是有bug啊?
研发:恩,我看看,!@#¥%……&*()一顿狂查,搜嘎,当时在上线,重启应用,异步任务丢了……
产品:能不能行,上线你就丢任务,丢不丢人啊!
研发:…………数据库

 

上线!重启!你还在为丢失任务而烦恼么?看这里看这里,今后再也不丢任务,JVM能够安全退出的windows

在交易流程中,为了提高服务的性能,咱们作了一些异步化的优化,好比更新用户最近使用的收货地址、提单完成后经过MQ去发送各类通知类消息、清理用户的购物车等等这些操做,异步化加快了应用的响应速度同时也带来一个隐患,如何保障异步操做的执行?这个场景主要发生在应用重启时,对于经过线程或线程池进行的异步化,JVM重启时,后台执行的异步操做可能还没有完成。这时,须要经过JVM安全关闭来保证异步操做进行完成后,JVM再执行关闭。
更普遍的说,在Linux上不少应用一般会经过kill -9 pid的方式强制将进程杀掉,这种方式简单高效,所以不少应用的中止脚本常常会选择使用kill -9 pid的方式。强制进程退出,会带来一些反作用,对应用程序而言其效果等同于忽然掉电,可能会致使以下一些问题:缓存

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

这些问题都有可能对咱们的业务产生影响,形成没必要要的损失,为了不这些问题,咱们须要在JVM关闭时作些扫尾的工做,为此JVM提供了关闭钩子(shutdown hooks)来作这些事情。本文探讨了利用关闭钩子的相关内容。tomcat

JVM 关闭

首先,咱们了解下哪些状况会致使JVM关闭,以下图安全

image

对于强制关闭的几种状况,系统关机,操做系统会通知JVM进程关闭并等待,一旦等待超时,系统会强制停止JVM进程;kill -九、Runtime.halt()、断电、系统crash这些种方式会直接无商量停止JVM进程,JVM彻底没有执行扫尾工做的机会。所以对用应用程序而言,咱们强烈不建议使用kill -9 这种暴力方式退出。
而对于正常关闭、异常关闭的几种状况,JVM关闭前,都会调用已注册的shutdown hooks,基于这种机制,咱们能够将扫尾的工做放在shutdown hooks中,进而使咱们的应用程序安全的退出。基于平台通用性的考虑,咱们更推荐应用程序使用System.exit(0)这种方式退出JVM。并发

JVM 与 shutdown hooks 交互流程以下图所示,能够对照源码进一步的学习shutdown hooks工做原理。
image异步

Jvm安全退出

对于tomcat类Web应用,咱们能够直接经过Runtime.addShutdownHook(Thread hook)注册自定义钩子,在钩子中实现资源的清理;而对于worker类应用,咱们能够采用以下的方式安全的退出应用。ide

基于信号的进程通知机制

信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求能够说是同样的。通俗来说,信号就是进程间的一种异步通讯机制。信号具备平台相关性,Linux平台支持的一些终止进程信号以下所示:函数

信号名称 用途
SIGKILL 终止进程,强制杀死进程
SIGTERM 终止进程,软件终止信号
SIGTSTP 中止进程,终端来的中止信号
SIGPROF 终止进程,统计分布图用计时器到时
SIGUSR1 终止进程,用户定义信号1
SIGUSR2 终止进程,用户定义信号2
SIGINT 终止进程,中断进程
SIGQUIT 创建CORE文件终止进程,而且生成core文件

Windows平台存在一些差别,它的一些信号举例以下所示:性能

信号名称 用途
SIGINT Ctrl+C中断
SIGTERM kill发出的软件终止
SIGBREAK Ctrl+Break中断

信号选择:为了避免干扰正常信号的运做,又能模拟Java异步通知,在Linux上咱们须要先选定一种特殊的信号。经过查看信号列表上的描述,发现 SIGUSR1 和 SIGUSR2 是容许用户自定义的信号,咱们能够选择SIGUSR2,在Windows上咱们能够选择SIGINT。

经过这种信号机制,对应用程序JVM发送特定信号,JVM能够感知并处理该信号,进而能够接受程序退出指令。

安全退出实现

首先看下通用的JVM安全退出的流程图:

image

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

1 Signal sig = new Signal(getOSSignalType());

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

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

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

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

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

1 Signal.handle(sig, shutdownHandler);

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

1 public class ShutdownHandler implements SignalHandler {
2     /**
3      * 处理信号
4      *
5      * @param signal 信号
6      */
7     public void handle(Signal signal) {
8     }
9 }

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

1 private void registerShutdownHook()
2  {
3         Thread t = new Thread(new ShutdownHook(), "ShutdownHook-Thread");
4         Runtime.getRuntime().addShutdownHook(t);
5  }

第五步,接收到进程退出信号后,在回调的handle接口中执行虚拟机的退出操做,示例代码以下:

1 Runtime.getRuntime().exit(0);

JVM退出时,底层会自动检测用户是否注册了ShutdownHook任务,若是有,则会自动执行注册钩子的Run方法,应用只须要在ShutdownHook中执行扫尾工做便可,示例代码以下:

 1 class ShutdownHook implements Runnable
 2 {
 3         @Override
 4         public void run() {
 5                 System.out.println("ShutdownHook execute start...");
 6                 try {
 7                    TimeUnit.SECONDS.sleep(10);//模拟应用进程退出前的处理操做
 8                 } catch (InterruptedException e) {
 9                     e.printStackTrace();
10                 }
11                 System.out.println("ShutdownHook execute end...");
12                 }
13 }

经过以上的几个步骤,咱们能够轻松实现JVM的安全退出,另外,一般安全退出须要有超时控制机制,例如30S,若是到达超时时间仍然没有完成退出,则由停机脚本直接调用kill -9强制退出。

使用关闭钩子的注意事项

  • 关闭钩子本质上是一个线程(也称为Hook线程),对于一个JVM中注册的多个关闭钩子它们将会并发执行,因此JVM并不保证它们的执行顺序;因为是并发执行的,那么极可能由于代码不当致使出现竞态条件或死锁等问题,为了不该问题,强烈建议在一个钩子中执行一系列操做。

  • Hook线程会延迟JVM的关闭时间,这就要求在编写钩子过程当中必需要尽量的减小Hook线程的执行时间,避免hook线程中出现耗时的计算、等待用户I/O等等操做。

  • 关闭钩子执行过程当中可能被强制打断,好比在操做系统关机时,操做系统会等待进程中止,等待超时,进程仍未中止,操做系统会强制的杀死该进程,在这类状况下,关闭钩子在执行过程当中被强制停止。
  • 在关闭钩子中,不能执行注册、移除钩子的操做,JVM将关闭钩子序列初始化完毕后,不容许再次添加或者移除已经存在的钩子,不然JVM抛出 IllegalStateException。
  • 不能在钩子调用System.exit(),不然卡住JVM的关闭过程,可是能够调用Runtime.halt()。
  • Hook线程中一样会抛出异常,对于未捕捉的异常,线程的默认异常处理器处理该异常,不会影响其余hook线程以及JVM正常退出。

总结

为了保障应用重启过程当中异步操做的执行,避免强制退出JVM可能产生的各类问题,咱们能够采用关闭钩子、自定义信号的方式,主动的通知JVM退出,并在JVM关闭前,执行应用程序的一些扫尾工做,进一步保证应用程序能够安全的退出。

相关文章
相关标签/搜索