笔者的项目需要改造成支持优雅停机,停止服务时不在接受新的请求(不接受请求所指,不是拒绝请求,而是客户端不再往服务端分发),但是已经接受的请求,需要在处理完,再停止服务。
目前大致的思路是:微服务框架实现优雅停机的功能,在destroy方法里微服务框架先把本地服务从zk上下线、等待5秒(等待处理中的业务结束),后续再判断业务是否结束,如果未结束根据配置确定是否再等待,然后关闭连接。
为此,需要对系统进行一些重构,将系统部署的服务器进行分组,分为现网业务、MQ、定时任务3组
通过流控(nginx)确保现网用户的流量流量指向现网业务组,MQ和定时任务组不接受现网流量
对MQ和定时任务设计统一的配置中心,各个模块暴露启停接口,通过接受配置中心的通知来控制启停
MQ和定时任务需要中间状态监控和停机后的续传业务,以确保停机后可以继续执行中断的业务。
shutdownHook主要用于:
Application
正常退出,在退出时执行特定的业务逻辑,或者关闭资源等操作。- 虚拟机非正常退出,比如用户按下ctrl+c、OOM宕机、操作系统关闭(kill pid)等。在退出时执行必要的挽救措施。
以下博文来自:https://www.cnblogs.com/shuo1208/p/5871224.html
JDK在1.3之后提供了Java Runtime.addShutdownHook(Thread hook)方法,可以注册一个JVM关闭的钩子,这个钩子可以在以下几种场景被调用:
- 1)程序正常退出
- 2)使用System.exit()
- 3)终端使用Ctrl+C触发的中断
- 4)系统关闭
- 5)使用Kill pid命令干掉进程
注:在使用kill -9 pid是不会JVM注册的钩子不会被调用。
在JDK中方法的声明:
public void addShutdownHook(Thread hook)
参数
hook -- 一个初始化但尚未启动的线程对象,注册到JVM钩子的运行代码。
异常
IllegalArgumentException -- 如果指定的钩已被注册,或如果它可以判定钩已经运行或已被运行
IllegalStateException -- 如果虚拟机已经是在关闭的过程中
SecurityException -- 如果存在安全管理器并且它拒绝的RuntimePermission(“shutdownHooks”)
代码示例:
使用Timer模拟一个工作线程,该线程重复工作十次,使用System.exit()退出,在清理现场代码CleanWorkThread 中,取消timer运行,并输出必要的日志信息。
1 package com.netease.test.java.lang; 2 3 import java.util.Timer; 4 import java.util.TimerTask; 5 import java.util.concurrent.atomic.AtomicInteger; 6 7 /** 8 * Date: 14-6-18 9 * Time: 11:01 10 * 测试ShutdownHook 11 */ 12 public class TestShutdownHook { 13 14 //简单模拟干活的 15 static Timer timer = new Timer("job-timer"); 16 17 //计数干活次数 18 static AtomicInteger count = new AtomicInteger(0); 19 20 /** 21 * hook线程 22 */ 23 static class CleanWorkThread extends Thread{ 24 @Override 25 public void run() { 26 System.out.println("clean some work."); 27 timer.cancel(); 28 try { 29 Thread.sleep(2 * 1000);//sleep 2s 30 } catch (InterruptedException e) { 31 e.printStackTrace(); 32 } 33 } 34 } 35 public static void main(String[] args) throws InterruptedException { 36 //将hook线程添加到运行时环境中去 37 Runtime.getRuntime().addShutdownHook(new CleanWorkThread()); 38 System.out.println("main class start ..... "); 39 //简单模拟 40 timer.schedule(new TimerTask() { 41 @Override 42 public void run() { 43 count.getAndIncrement(); 44 System.out.println("doing job " + count); 45 if (count.get() == 10) { //干了10次退出 46 System.exit(0); 47 } 48 } 49 }, 0, 2 * 1000); 50 51 } 52 }
运行后,可以模拟以上五种场景进行测试,只有kill -9 pid不会执行Hook里面的代码。
使用shutdownHook需注意:
1.不一定总是被执行,例如,JVM崩溃了。
2.shutdownHook
可以在完成前强行停止
虽然shutdownHook
开始执行,但是在操作系统关闭的情况下,任然可以在完成之前终止它。在这种情况下,一旦SIGTERM
被提供,O/S
等待一个进程终止指定的时间。如果进程在该时间限制内没有终止,则O/S
通过发出SIGTERM(或Windows中的对等方)强制终止进程。所以有可能这是在shutdownHook
中途执行时发生的。
需要保证其调用的服务不会被其他Hook影响。否则会出现当前Hook所依赖的服务被另外一个Hook终止了的情况。
因此,建议谨慎地编写shutdownHook
,确保它们快速完成,并且不会造成死锁等情况。另外特别注意的是,不应该执行长时间计算或等待用户I/O操作在钩子。
3.可以有多个shutdownHook
,但其执行顺序无法保证,因为它是存储在IdentityHashMap
中的,JVM并不能保证其执行顺序。但是可以同时执行所有的shutdownHook。
4.关闭顺序开始后,无法注册/取消注册shutdownHook
一旦关闭顺序是由JVM发起的,将不在允许添加或删除任何现有的shutdownHook
,否则抛出IllegalStateException
异常。
5.关闭顺序开始后,只能由Runtime.halt()
停止
关闭顺序开始后,只能通过Runtime.halt()(强制终止JVM)
,可以停止关闭顺序的执行(外部影响除外,如SIGKILL)。
6.使用shutdownHook
需要安全权限
如果我们使用Java Security Managers
,则执行添加/删除shutdownHook
的代码需要在运行时
具有shutdownHooks
权限。否则会导致SecurityException
。
在程序当中保持事务原子性的一个思路:
在开发中,遇到这种情况,多个线程同时工作,突然一个线程遇到了fetal的错误,需要立即终止程序,等人工排查解决了问题之后重新启动。但是这样会有一个问题,程序终止时,其他线程可能正在进行重要操作,比如发一个message到另一个模块,并更新数据库状态。突然终止,可能会让这个操作只完成一半,从而导致数据不一致。
解决方案是:参考数据库Transaction原子性的概念,将这一系列重要操作看作一个整体,要么全部完成,要么全部不完成。为方便表述,我们把这一系列重要操作记为操作X。
当程序即将退出时,查看当前是否有操作X在执行中,
- 如果有,等待其完成然后退出。且期间不再接受新的操作X。如果操作X执行之间过长,终止并回滚所有状态。
- 如果没有,则可以立即退出。
在程序退出的时候,做一些Check,保证已经开始的操作X的原子性,这里就用到了Runtime.ShutdownHook
。
假如在系统中通过 Runtime.getRuntime().exec(String command) 或 new ProcessBuilder(List<String> command) 启动了子进程(Process),这个子进程一直在运行中,在当前进程退出时,子进程未必会退出,但此时业务上希望将它退出,就可以利用 ShutdownHook 。
Java的Signal在
Linux下支持的信号:
SEGV, ILL, FPE, BUS, SYS, CPU, FSZ, ABRT, INT, TERM, HUP, USR1, USR2, QUIT, BREAK, TRAP, PIPE
Windows下支持的信号:
SEGV, ILL, FPE, ABRT, INT, TERM, BREAK