如何确保服务平稳发布?
Spring Event
的线上事故,原因就在于进程退出时,有应用线程从Spring GetBean
,然而Spring 不允许BeanFactory
销毁期间获取bean,导致请求处理失败、数据不一致的未知结果。1JVM退出方式
正常关闭
shutdown hook
钩子程序。钩子提供了java代码响应进程退出的机制,例如Spring等框架通过注册shutdownHook
钩子程序,监测到进程要退出的信号,然后关闭Spring上下文。-
所有非守护线程退出 -
System.exit(0) -
Ctrl+ C 命令行中退出进程 -
kill Pid 通知进程退出。
Thread.setDaemon
也可修改为守护线程。@Override
public void run() {
int cnt = 4;
for (int i = 0; i < cnt; i++) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println(“被中断”);
break;
}
System.out.println(“子线程执行中”);
}
System.out.println(“子线程退出”);
}
});
thread.start();
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“主线程提出”);

System.exit()
退出进程,从而不会等待子线程结束再退出进程。所以会造成一个假象 —— junit方法代表的主线程退出后,进程就退出。System.exit()
或Kill pid
方式退出进程。强制退出
shutdown hook
钩子程序,应用程序无法释放资源,无法等待剩余请求执行完毕,无法优雅退出。-
Kill -9 关闭进程 -
Runtime.halt()
-
机器断电、操作系统关机 -
操作系统强制杀死一个进程 -
进程Crash
linux kernel
)优先kill最大内存的进程。异常关闭
-XX:+HeapDumpOnOutOfMemoryError
参数表示当JVM发生OOM时,自动生成DUMP文件。-XX:HeapDumpPath= 目录
,也可以指定文件名称,例如:−XX:HeapDumpPath={目录}
,也可以指定文件名称,例如:- XX:HeapDumpPath=目录
,也可以指定文件名称,例如:−XX:HeapDumpPath={目录}/java_heapdump.hprof
-XX:OnOutOfMemoryError
在程序发生OOM异常时,执行指定命令,该参数接下来会详细介绍,也是JVM优雅退出的关键参数-XX:+ExitOnOutOfMemoryError
在程序发生OOM异常时,强制退出-XX:+CrashOnOutOfMemoryError
在程序发生OOM异常时,强制退出,并生成Crash日志
OnOutOfMemberError
配置脚本,通过脚本kill进程。可以使用以下参数配置,在OOM发生后,kill进程,如果进程在60s未退出,执行kill-9强制关闭。HeapDumpOnOutOfMemoryError
,OOM后打印heap dump
,然后强制退出进程。由于无法同时配置这两个参数,所以需要选择一个。更关心heapDump文件排查问题还是优雅退出,需要各位开发者根据业务实际场景决策。 可以参考这个博客分析为什么推荐使用 ExitOnOutOfMemoryError[2]OnOutOfMemoryError
的OOM场景可能触发优雅退出。这两种情况JVM都会执行shutdownHook
程序2ShutdownHook钩子程序
@Override
public void run() {
System.out.println(“shutdown hook started”);
}
}));
addShutdownHook
方法上提供了详细的注释,我翻译一下当虚拟机开始关闭时,会以不确定的顺序启动所有的钩子程序,它们并发执行 当所有的钩子完成后,虚拟机才会关闭。在此期间守护线程和非守护线程都会继续运行。 如果通过调用 System.exit()
方法关机,一旦开始执行关闭程序,只能通过System.halt()
方法强制终止虚拟机。一旦开始执行关闭程序,不得再新建钩子或注销钩子,尝试以上两个操作会导致抛出 IllegalStateException
。shutdown钩子在一个微妙时刻执行,应该进行防御编码,要写的线程安全,尽量避免死锁。 shutdown钩子应该尽快完成(虚拟机需要钩子程序全部执行完成,才能关闭)。
System.exit
,这会阻塞中虚拟的关闭,使用Kill或者System.exit触发虚拟的关闭程序,钩子程序中再次调用System.exit
会导致虚拟机被阻塞,无法被关闭。所以严禁在钩子程序中调用 System.exit方法。shutdown hook
,我们只需要实现Spring提供的关闭扩展点即可。无需自己再新增shutdown hook
,避免了和spring 并发关闭可能导致的不可知结果。
3Spring提供的关闭扩展点
if (this.active.get() && this.closed.compareAndSet(false, true)) {
// …(省略)
publishEvent(new ContextClosedEvent(this));
this.lifecycleProcessor.onClose();
destroyBeans();
closeBeanFactory();
onClose();
this.active.set(false);
}
}
-
发布 ContextClosedEvent
,可以使用EventListener注解 监听该事件 -
执行 Lifecycle
类型Bean 的所有stop
方法。 -
关闭 BeanFactory
,单例Bean中声明init-method
和destroy-method
的会被销毁。如果单例bean未声明两个方法,则不会被销毁。
void start();
void stop();
boolean isRunning();
}
BenaFactory
还未关闭,Bean资源都还未销毁。这是一个绝佳的关闭资源的地方。Spring销毁bean的顺序

DependOn
注解声明依赖顺序。dependOn
注解声明的方式,肯定会有疏漏。有什么好的办法呢?BeanFactory
之前,通过ContextCloseEvent
切断线上流量,服务请求的入口一般分为 http、Rpc、MQ。4关闭请求入口
关闭MQ入口
close
或者destroy
方法。通过在 ContextCloseEvent
消费逻辑中 关闭所有的消费者,切断所有的消费逻辑。但是要注意,因为BeanFacotry
阶段还要销毁bean,所以要保证close方法的幂等。可通过状态字段控制。关闭RPC入口
关闭Http入口
beanFactory bean
被销毁之前。所以不存在入口未关闭,bean被销毁的情况。# 设置关闭方式为优雅关闭
shutdown: graceful
spring:
lifecycle:
# 优雅关闭超时时间, 默认30s
timeout-per-shutdown-phase: 30s
ContextCloseEvent
中 关闭各种线程池,例如Rpc线程池、业务逻辑中创建的线程池。5线程池的优雅关闭
shutdown
和shutdownNow
。-
拒绝新任务提交 -
待执行的任务不会取消 -
正在执行的任务也不会取消,将继续执行
-
拒绝新任务提交 -
取消待执行的任务 -
尝试取消执行中的任务
shutdownNow
强行关闭,而是推荐使用shutdown
优雅关闭。但是如果任务长时间无法结束,要一直等待吗?不能。可以考虑使用awaitTermination
等待指定时间后。如果依然无法关闭,那么放弃等待。shutdown hook
就执行完成。此时虽然 线程池可能还没有关闭,但是Java进程也会关闭。6总结
-
Java一共三种进程退出方式。 -
只有正常退出和OOM配置 OnOutOfMemberError
=”kill脚本”才会执行优雅关闭逻辑 -
优雅退出时,会并发执行 shutdown
钩子,小心并发和不要执行太久 -
Spring 接入了钩子。可以通过 ContextCloseEvent
中 切断http/mq/rpc等入口流量。shutdown关闭线程池。 -
要始终保证服务在关闭期间,不会出现 被依赖的资源、被依赖的bean已经被销毁或回收。否则服务无法优雅关闭,一定会出现未知的调用异常。
微信赞赏
支付宝扫码领红包
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。侵权投诉:375170667@qq.com