关闭应用分为两种方式,一种方式强杀用户进程,导致应用没有机会保证正在处理工作的完整性,具体表现在丢失数据、数据损坏、数据重复等问题。那么我们关闭应用就应该采用稳定靠谱的方式关闭应用,等待应用完全处理完毕当前的任务并无新任务进来在关闭应用,这种方式叫优雅关机。 下面我们从JVM优雅关机慢慢过渡到Netty的优雅关机。
一、导致JVM关闭的原因
分析JVM关闭前,我们需要了解什么情况下JVM会关闭。
JVM强制关闭:系统关机[系统会通知JVM关闭,如果JVM关闭超时会强制关闭]、kill -9、Runtime.halt()、断电、系统宕机
JVM正常关闭:JVM关闭前会调用已注册的ShutdownHook(),我们可以将扫尾的工作放在shutdown hooks中使应用程序安全的退出。基于平台通用性的考虑推荐System.exit(0)方式退出JVM。
二、JVM优雅关机
方法一、Runtime.getRuntime().addShutdownHook()方法钩子
/**
* 注册关闭钩子
* */
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run() {
// 在关闭钩子中执行收尾工作
// 注意事项:
// 1.在这里执行的动作不能耗时太久
// 2.不能在这里再执行注册,移除关闭钩子的操作
// 3 不能在这里调用System.exit()
System.out.println("do shutdown hook");
}
});
方法二、实现SignalHandler信号量判断
Signal sg = new Signal("TERM"); // kill -15 pid
// 监听信号量
Signal.handle(sg, new SignalHandler() {
@Override
public void handle(Signal signal) {
System.out.println("signal handle: " + signal.getName());
System.exit(0);
}
});
钩子实现与信号量实现区别
- 第一种钩子方法在进程被kill -15 pid时候main函数就已经结束了,仅会运行shutdownHook中run()方法的代码。此方法不管是程序异常关闭还是外界主动关闭都会进入钩子执行。
- 第二种信号量方法函数会在进程被kill -15 pid 会收到TERM信号,对main函数的运行不会有任何影响。我们需要在里面实现优雅关机的代码处理完毕等待程序关闭。此方法只会受到外界的信号量执行,程序自身的OOM、Exception都不会执行,所以只有第一种实现比较靠谱。Github JVM优雅关机实现
三、Netty优雅关机
Netty作为很多框架及高并发应用通讯模块的基础框架,往往应用或者中间件退出的时候,作为其中一部分的Netty也需要优雅退出,Netty在优雅退出的时候需要做到下面几点。
- NIO线程退出、资源释放
- 等待发送队列中的待发送消息发送完成
- 正在读、写的消息需要继续处理
- 设置在NioEventLoop 线程调度器中的定时任务,需要执行或者清理
下面我们看下 Netty 优雅退出涉及的主要操作和资源对象
下面我们通过调试Netty优雅退出分析其实现原理
通过调试分析Netty shutdownGracefully只能分析到boss、work线程池的关闭过程、唤醒Select队列处理正在进行的任务,但是无法分析出Channel释放过程及多路复用器的注册关闭等。shutdownGracefully是一个看起来简单,但是比较复杂的方法,此处TODO待后续分析。
//boss、work工作组关闭流程
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
//第一步:AbstractEventExecutorGroup.java 初始化优雅关闭参数
@Override
public Future<?> shutdownGracefully() {
return shutdownGracefully(DEFAULT_SHUTDOWN_QUIET_PERIOD, DEFAULT_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS);}
//第二步:MultithreadEventExecutorGroup.java quietPeriod=2、timeout=15、unit=SECONDS
//EventExecutor本质是一个线程,工作组声明几个线程就会关闭几次
@Override
public Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {
for (EventExecutor l: children) {
l.shutdownGracefully(quietPeriod, timeout, unit);
}
return terminationFuture();
}
//第三步:SingleThreadEventExecutor.java 关闭worke、boos线程组、唤醒Selecter线程继续处理正常进行的任务
@Override
public Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {
if (quietPeriod < 0) {
throw new IllegalArgumentException("quietPeriod: " + quietPeriod + " (expected >= 0)");
}
if (timeout < quietPeriod) {
throw new IllegalArgumentException(
"timeout: " + timeout + " (expected >= quietPeriod (" + quietPeriod + "))");
}
if (unit == null) {
throw new NullPointerException("unit");
}
//判断此线程词是否关闭,注意AtomicIntegerFieldUpdater<SingleThreadEventExecutor> STATE_UPDATER的精妙设计,为啥这样设计?
if (isShuttingDown()) {
return terminationFuture();
}
boolean inEventLoop = inEventLoop();
boolean wakeup;
int oldState;
//自选方式标记线程池状态
for (;;) {
if (isShuttingDown()) {
return terminationFuture();
}
int newState;
wakeup = true;
oldState = STATE_UPDATER.get(this);
if (inEventLoop) {
//修改boos线程池状态设置为不在接收状态
newState = ST_SHUTTING_DOWN;
} else {
switch (oldState) {
case ST_NOT_STARTED:
case ST_STARTED:
newState = ST_SHUTTING_DOWN;
break;
default:
newState = oldState;
wakeup = false;
}
}
if (STATE_UPDATER.compareAndSet(this, oldState, newState)) {
break;
}
}
//关闭等待时间单位转化
gracefulShutdownQuietPeriod = unit.toNanos(quietPeriod);
gracefulShutdownTimeout = unit.toNanos(timeout);
if (oldState == ST_NOT_STARTED) {
//关闭线程池
scheduleExecution();
}
if (wakeup) {
//唤醒柱塞在Selector.wakeup的线程
wakeup(inEventLoop);
}
return terminationFuture();
}
四、jvm和spring钩子区别
spring钩子就是spring销毁坚挺函数,其特点是依赖spring环境,更多用于spring环境的项目中间件.JVM理论可以用于任何java环境.不过如果项目是spring构建的推荐使用spring钩子.