Spring Boot实现优雅关闭
有时候我们需要关闭或重启Java程序,但是可能某个业务流程还未执行结束或占用的资源还未释放,如果使用
kill -9
这种暴力的方式关闭应用,有可能会导致我们的业务逻辑混乱,数据不一致等问题,因此我们希望使用一种温柔的方式来关闭程序
在了解优雅关闭前,先了解下什么是shutdown hook
JVM提供的shutdown hook
创建主程序,添加钩子线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class JVMTest {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
System.out.println("this is a custom shutdown hook");
}
}));
System.out.println("JVM terminated...");
}
}主程序正常结束,shutdown hook正常被调用
主程序主动退出,shutdown hook也会正常被调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
System.out.println("this is a custom shutdown hook");
}
}));
System.out.println("JVM terminated...");
System.exit(0);
}修改主程序,让主程序sleep 20s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class JVMTest {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
System.out.println("this is a custom shutdown hook");
}
}));
System.out.println("JVM terminated after 20 seconds");
try {
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}通过Linux的kill命令终止主程序,使用
kill -9
命令,shutdown hook并不会被调用,使用kill -15
会被调用
简单来说,shutdown hook是jvm在关闭时会调用的一个钩子
JVM处理信号量通知
System#initializeSystemClass()
初始化,调用Terminator.setup()
设置信号量处理1
2
3
4
5
6
7
8// Initialize the system class. Called after thread initialization.
private static void initializeSystemClass() {
...
// 对于程序 挂断、终止、中断的处理
// Setup Java signal handlers for HUP, TERM, and INT (where available).
Terminator.setup();
...
}Terminator#setup()
设置信号量处理1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22static void setup() {
if (handler != null) return;
// 设置信号量处理器
SignalHandler sh = new SignalHandler() {
public void handle(Signal sig) {
// 调用Shutdown.exit()方法
Shutdown.exit(sig.getNumber() + 0200);
}
};
handler = sh;
try {
// 程序中断处理
Signal.handle(new Signal("INT"), sh);
} catch (IllegalArgumentException e) {
}
try {
// 程序终止处理
Signal.handle(new Signal("TERM"), sh);
} catch (IllegalArgumentException e) {
}
}Runtime#addShutdownHook()
添加shutdown hook1
2
3
4
5public void addShutdownHook(Thread hook) {
...
ApplicationShutdownHooks.add(hook);
}ApplicationShutdownHooks#add()
添加钩子线程1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53// 保存钩子线程的集合
private static IdentityHashMap<Thread, Thread> hooks;
// 静态代码块初始化时,向Shutdown添加一个钩子线程
static {
try {
Shutdown.add(1 /* shutdown hook invocation order */,
false /* not registered if shutdown in progress */,
new Runnable() {
public void run() {
runHooks();
}
}
);
hooks = new IdentityHashMap<>();
} catch (IllegalStateException e) {
// application shutdown hooks cannot be added if
// shutdown is in progress.
hooks = null;
}
}
// 添加钩子线程
static synchronized void add(Thread hook) {
...
//
hooks.put(hook, hook);
}
// 运行钩子线程
static void runHooks() {
Collection<Thread> threads;
// 获取所有的钩子线程
synchronized(ApplicationShutdownHooks.class) {
threads = hooks.keySet();
hooks = null;
}
// 执行
for (Thread hook : threads) {
hook.start();
}
// 等待线程执行结束
for (Thread hook : threads) {
while (true) {
try {
hook.join();
break;
} catch (InterruptedException ignored) {
}
}
}
}Shutdown#add()
添加钩子线程1
2
3
4
5
6
7
8
9
10private static final int MAX_SYSTEM_HOOKS = 10;
private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];
static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {
synchronized (lock) {
...
// 添加钩子线程
hooks[slot] = hook;
}
}System#exit()
终止JVM,带程序退出状态code,非0为异常1
2
3public static void exit(int status) {
Runtime.getRuntime().exit(status);
}Runtime#exit()
1
2
3
4public void exit(int status) {
...
Shutdown.exit(status);
}Shutdown#exit()
和Shutdown#shutdown()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57// 由Runtime.exit()触发调用
static void exit(int status) {
...
synchronized (Shutdown.class) {
/* Synchronize on the class object, causing any other thread
* that attempts to initiate shutdown to stall indefinitely
*/
beforeHalt();
sequence();
halt(status);
}
}
private static void sequence() {
...
runHooks();
...
}
// 当最后一个非daemon线程执行结束后,由JNI DestroyJavaVM调用,不会关闭JVM
static void shutdown() {
synchronized (lock) {
switch (state) {
case RUNNING: /* Initiate shutdown */
state = HOOKS;
break;
case HOOKS: /* Stall and then return */
case FINALIZERS:
break;
}
}
synchronized (Shutdown.class) {
sequence();
}
}
// 实际执行勾子线程的方法
private static void runHooks() {
for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
try {
Runnable hook;
synchronized (lock) {
// acquire the lock to make sure the hook registered during
// shutdown is visible here.
currentRunningHook = i;
hook = hooks[i];
}
// 执行勾子线程的run方法
if (hook != null) hook.run();
} catch(Throwable t) {
if (t instanceof ThreadDeath) {
ThreadDeath td = (ThreadDeath)t;
throw td;
}
}
}
}
流程图展示(大致流程)
可以看到,JVM在System
类初始化时会添加信号量处理(如程序中断、程序终止),接收到信号量通知时会触发Shutdown.exit
方法,调用自定义的shutdown hook
Spring Boot中的shutdown hook
SpringApplication#run
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// spring容器启动流程
public ConfigurableApplicationContext run(String... args) {
...
refreshContext(context);
...
}
static final SpringApplicationShutdownHook shutdownHook = new SpringApplicationShutdownHook();
// 刷新容器,如果registerShutdownHook为true,调用SpringApplicationShutdownHook#registerApplicationContext()方法注册应用上下文
private void refreshContext(ConfigurableApplicationContext context) {
if (this.registerShutdownHook) {
shutdownHook.registerApplicationContext(context);
}
refresh(context);
}SpringApplicationShutdownHook#registerApplicationContext()
,SpringApplicationShutdownHook
本身实现了Runnable接口1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76// 注册应用上下文
// 1.注册spring容器关闭事件监听器
// 2.调用Runtime#addShutdownHook()方法添加shutdown hook
void registerApplicationContext(ConfigurableApplicationContext context) {
// 添加shutdown hook
addRuntimeShutdownHookIfNecessary();
synchronized (SpringApplicationShutdownHook.class) {
assertNotInProgress();
// 注册容器关闭事件监听器
context.addApplicationListener(this.contextCloseListener);
this.contexts.add(context);
}
}
private void addRuntimeShutdownHookIfNecessary() {
if (this.shutdownHookAdded.compareAndSet(false, true)) {
addRuntimeShutdownHook();
}
}
// 实际调用Runtime#addShutdownHook()方法添加钩子线程
void addRuntimeShutdownHook() {
try {
Runtime.getRuntime().addShutdownHook(new Thread(this, "SpringApplicationShutdownHook"));
}
catch (AccessControlException ex) {
// Not allowed in some environments
}
}
// 钩子线程最终执行的方法
public void run() {
Set<ConfigurableApplicationContext> contexts;
Set<ConfigurableApplicationContext> closedContexts;
Set<Runnable> actions;
synchronized (SpringApplicationShutdownHook.class) {
this.inProgress = true;
contexts = new LinkedHashSet<>(this.contexts);
closedContexts = new LinkedHashSet<>(this.closedContexts);
actions = new LinkedHashSet<>(this.handlers.getActions());
}
// 关闭容器并等待容器停止
contexts.forEach(this::closeAndWait);
closedContexts.forEach(this::closeAndWait);
// 执行handlers中的action
actions.forEach(Runnable::run);
}
private void closeAndWait(ConfigurableApplicationContext context) {
if (!context.isActive()) {
return;
}
// 关闭容器
context.close();
try {
// 等待一段时间
int waited = 0;
while (context.isActive()) {
if (waited > TIMEOUT) {
throw new TimeoutException();
}
Thread.sleep(SLEEP);
waited += SLEEP;
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
logger.warn("Interrupted waiting for application context " + context + " to become inactive");
}
catch (TimeoutException ex) {
logger.warn("Timed out waiting for application context " + context + " to become inactive", ex);
}
}容器关闭时,会调用
AbstractApplicationContext#close()
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41// 容器关闭
public void close() {
synchronized (this.startupShutdownMonitor) {
doClose();
// 移除shutdown hook
if (this.shutdownHook != null) {
try {
Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
}
catch (IllegalStateException ex) {
// ignore - VM is already shutting down
}
}
}
}
// 实际关闭容器的方法
protected void doClose() {
// Check whether an actual close attempt is necessary...
if (this.active.get() && this.closed.compareAndSet(false, true)) {
...
// 发布容器关闭事件,所有监听了该事件的监听器都会被通知,上面的SpringApplicationShutdownHook类就监听了该事件
publishEvent(new ContextClosedEvent(this));
...
// 销毁BeanFactory中的单例bean,可以通过实现Disposable接口重写destroy方法,会在这里被调用
destroyBeans();
// 关闭BeanFactory
closeBeanFactory();
// 留给子类实现
onClose();
...
}
}
流程展示
可以看到,SpringBoot启动时会添加一个SpringApplicationShutdownHook
对象,该类实现了Runnable接口,run方法中调用context.close来关闭容器,从而可以在JVM关闭前进行一些通知或清理工作
何为优雅关闭
优雅关闭,就是指程序彻底停止前,当前正在处理的任务需要继续执行、服务器不再接受新的请求、应用从注册中心下线等
如何优雅关闭Spring Boot服务
优雅关闭服务的关键在于我们需要找到一个缓冲点,这个缓冲点可以让我们在JVM关闭前执行一系列资源清理工作,根据上面的分析,可以知道SpringBoot给我们提供了两个缓冲点,一个是容器关闭事件监听,一个是实现
Disposable
接口重写destroy()
方法
添加缓冲点
监听Spring容器关闭事件
1
2
3
4
5
6
7
8
9
public class SpringCloseEventListener implements ApplicationListener<ContextClosedEvent> {
public void onApplicationEvent(ContextClosedEvent event) {
System.out.println("执行资源清理工作...");
}
}实现Disposable接口或在方法上使用@PreDestroy注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DisposableClass implements DisposableBean {
public void destroy() throws Exception {
System.out.println("DisposableClass#destroy");
}
}
public class PreDestroyBean {
public void preDestroy() {
System.out.println("执行PreDestroy()方法...");
}
}
关闭服务
通过
System.exit()
直接退出通过
AbstractApplicationContext#close
关闭服务1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ShutdownController implements ApplicationContextAware {
private ApplicationContext context;
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context = context;
}
public String shutdown() {
((ConfigurableApplicationContext) context).close();
return "ok";
}
}通过
ApplicationPidFileWriter
将服务pid写入指定文件,使用脚本kill对应pid的进程1
2
3
4
5
6
7
8
9
10
public class UserApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(UserApplication.class);
application.addListeners(new ApplicationPidFileWriter("/home/service/user-service.pid"));
application.run(args);
}
}1
cat /home/service/user-service.pid | xargs kill
使用actuator实现优雅关闭
- SpringBoot 2.3及以上版本
添加缓冲点,参考这里
引入actuator依赖,添加配置信息
引入依赖
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>修改application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19server:
port: 8090
# 采用优雅停机方式,默认immediate立即停机
shutdown: graceful
# 等待超时时间
spring:
lifecycle:
timeout-per-shutdown-phase: 15s
# 暴露shutdown路径
management:
endpoints:
web:
exposure:
include: shutdown
endpoint:
shutdown:
enabled: true发送shutdown请求(使用POST方式)
1
curl -X POST http://localhost:8090/actuator/shutdown
actuator工作原理
ShutdownEndpoint
类暴露了一个shutdown
端点,当请求该端点时,内部调用了AbstractApplicationContext#close
方法关闭容器1
2
3
4
5
6
7
8
9private void performShutdown() {
try {
Thread.sleep(500L);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
this.context.close();
}
- SpringBoot 2.3版本以下(使用Tomcat容器)
引入actuator依赖,添加配置信息
引入依赖
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>修改application.yml
1
2
3
4
5
6
7
8
9# 暴露shutdown路径
management:
endpoints:
web:
exposure:
include: shutdown
endpoint:
shutdown:
enabled: true
创建优雅关闭配置类
自定义
TomcatCustomizer
类实现TomcatConnectorCustomizer
和ApplicationListener
接口,获取Tomcat连接器和线程池,接收到容器关闭事件后关闭线程池1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class GracefulShutdownConfig {
public TomcatCustomizer tomcatCustomizer() {
return new TomcatCustomizer();
}
public ServletWebServerFactory servletWebServerFactory() {
TomcatServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
// 将自定义的TomcatConnectorCustomizer添加到Tomcat容器中
serverFactory.addConnectorCustomizers(tomcatCustomizer());
return serverFactory;
}
public class TomcatCustomizer implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
private volatile Connector connector;
private int timeout = 30;
public void customize(Connector connector) {
this.connector = connector;
}
public void onApplicationEvent(ContextClosedEvent event) {
if (null != connector) {
this.connector.pause();
Executor executor = this.connector.getProtocolHandler().getExecutor();
if (executor instanceof ThreadPoolExecutor) {
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
threadPoolExecutor.shutdown();
try {
if (threadPoolExecutor.awaitTermination(timeout, TimeUnit.SECONDS)) {
log.warn("tomcat didn't close gracefully in {} seconds, it turns to shutdown forcibly", timeout);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
}
}
注意:暴露shutdown端点存在风险,需要保证该端点不会被其他人随意调用
总结:
- JVM为我们提供了添加shutdown hook的方法,在JVM关闭前会执行这些hook线程
- JVM在处理信号量通知的过程中使用了shutdown hook
- SpringBoot提供了
SpringApplicationShutdownHook
(实现了Runnable接口)作为shutdown hook,run方法中调用AbstractApplicationContext#close
方法关闭容器 - 容器关闭时会进行一系列资源销毁工作,如发布容器关闭事件、销毁Bean Factory中的bean、关闭Bean Factory、执行子类的onClose方法等
- 通过监听容器关闭事件或实现
DisposableBean
接口可以进行资源清理操作,如线程池的销毁、服务从注册中心下线、MQ消费等
上面只是简单描述了如何优雅关闭SpringBoot服务,实际在开发过程中还需要考虑到很多因素,如不同的注册中心、不同的rpc框架、服务部署方式等
道阻且长,行则将至。