为什么 spring 要做优雅停机
我们现在的服务一般都是在 spring 容器运行,如果不做优雅停机,会有以下问题
1、程序中的任务运行到一半,被强行结束,影响到正常业务
2、出现 spring 容器已经关闭,但任务仍在运行的情况,这个时候用到 spring 的部分就会报错
所以理想状态下,停机的时候,先停止我们自己的任务,然后再关闭 spring 的容器
spring 怎么做优雅停机
在用 kill pid 进行停机时,会触发 jvm 钩子函数,spring 很好的利用了这个特性,来看下源码
注册一个钩子函数
// 注册一个钩子,org.springframework.context.support.AbstractApplicationContext#registerShutdownHook
public
void
registerShutdownHook() {
if
(
this
.shutdownHook ==
null
) {
// No shutdown hook registered yet.
this
.shutdownHook =
new
Thread() {
@Override
public
void
run() {
synchronized
(startupShutdownMonitor) {
doClose();
}
}
};
Runtime.getRuntime().addShutdownHook(
this
.shutdownHook);
}
}
在 doClose 方法里面,会先发布 ContextClosedEvent 事件,然后关闭 spring 容器
// org.springframework.context.support.AbstractApplicationContext#doClose
protected
void
doClose() {
// Check whether an actual close attempt is necessary...
if
(
this
.active.get() &&
this
.closed.compareAndSet(
false
,
true
)) {
// 省略相关代码
try
{
// Publish shutdown event.
// spring 的事件默认是同步执行
publishEvent(
new
ContextClosedEvent(
this
));
}
catch
(Throwable ex) {
logger.warn(
"Exception thrown from ApplicationListener handling ContextClosedEvent"
, ex);
}
// 省略关闭 spring 容器相关代码 。。。
}
}
因此,我们可以利用 ContextClosedEvent 事件,对应用的一些任务做优雅关停
@Component
public
class
SpringContextClosedListener
implements
ApplicationListener<ContextClosedEvent> {
@Override
public
void
onApplicationEvent(ContextClosedEvent event) {
// 关闭应用的任务,比如关闭 kafka 消费,关闭定时任务等等
}
}
spring-boot 优雅停机
spring boot 的 actuator 组件提供了shutdown 端点,可以让我们通过调接口的方式提前对容器进行关闭,而不必等到 jvm 关闭的钩子函数触发时再关闭
直接通过 http 调用 curl localhost:8080/actuator/shutdown 即可
我们来看下源码
// org.springframework.boot.actuate.context.ShutdownEndpoint
public
Map<String, String> shutdown() {
if
(
this
.context ==
null
) {
return
NO_CONTEXT_MESSAGE;
}
try
{
return
SHUTDOWN_MESSAGE;
}
finally
{
Thread thread =
new
Thread(
this
::performShutdown);
thread.setContextClassLoader(getClass().getClassLoader());
thread.start();
}
}
private
void
performShutdown() {
try
{
Thread.sleep(500L);
}
catch
(InterruptedException ex) {
Thread.currentThread().interrupt();
}
// 主动调用 ApplicationContext 的关闭方法
this
.context.close();
}
主动调用 ApplicationContext 的关闭方法, 如果之前注册过关闭事件的钩子函数,会取消掉
// org.springframework.context.support.AbstractApplicationContext#close
public
void
close() {
synchronized
(
this
.startupShutdownMonitor) {
doClose();
// If we registered a JVM shutdown hook, we don't need it anymore now:
// We've already explicitly closed the context.
if
(
this
.shutdownHook !=
null
) {
try
{
Runtime.getRuntime().removeShutdownHook(
this
.shutdownHook);
}
catch
(IllegalStateException ex) {
// ignore - VM is already shutting down
}
}
}
}
可以看到和上面 spring 钩子函数执行的方法一样,因此,我们依然可以用 ContextClosedEvent 监听到,然后自定义自己的逻辑
我可以直接用 Runtime.getRuntime().addShutdownHook 自定义自己的关停逻辑吗
可以,但不建议,因为 jvm 的钩子函数是 并发+ 无序 执行的,你保证不了你的钩子函数和 spring 钩子函数的顺序
除非你的任务逻辑不依赖任何上下文,也不依赖 spring ,否则还是建议放到 spring 的 ContextClosedEvent 事件
多个 ContextClosedEvent 事件的顺序问题
如果有多个 ContextClosedEvent 事件,并且事件有相互依赖关系,请使用 spring 的 order 指定顺序
之前的血泪教训 20200928-上线后cdimond动态配置不生效bug
ContextClosedEvent 事件为什么不生效
1、首先 springboot 的话都会自己注册 spring 关闭事件钩子,所以都是生效的
2、但非 springboot 并且非 web 项目默认没有注册 spring 的关闭钩子,因此需要自己注册下,否则 ContextClosedEvent 事件不会生效
Core Technologies