为什么需要优雅关闭
最常用的关闭应用的方法是kill -9 PID 暴力关闭,但是暴力关闭会带来很多问题,例如会造成数据的不完整性。我们公司需要做一个异步同步考勤记录的功能,同步完成后会更新redis的相关key的值为完成状态,如果此时应用被暴力关闭了,会导致此状态不会更新,进度条会一直卡在同步中,需要等待超时后重试,如果正好更新到最后一个考勤记录被强制kill了,必须要重新同步一次,对用户来说体验非常差。
优雅关闭的原理
调用spring上下文close函数关闭容器,在此函数中进行spring bean的移除和tomcat线程池的释放等操作,但是不能对代码中自定义的线程或者线程池的关闭,需要自己去释放,释放的契机是相关的类需要实现以下三种的一种
@PreDestroy注解
destory-method方法
DisposableBean接口
结合例子来说明
1.通过 actuator 实现优雅停机
引入maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
然后将shutdown节点打开,也将/actuator/shutdown暴露web访问也设置上,除了shutdown之外还有health, info的web访问都打开的话将management.endpoints.web.exposure.include=*就可以。将如下配置设置到application.properties里边,设置一下服务的端口号为6666。
server.port=6666
management.endpoint.shutdown.enabled=true
management.endpoints.web.exposure.include=shutdown
编写controller类
package com.hqs.springboot.shutdowndemo.controller;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PreDestroy;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author huangqingshi
* @Date 2019-08-17
*/
@RestController
public class ShutDownController implements ApplicationContextAware {
private ApplicationContext context;
private static final ThreadPoolExecutor TRACK_LOG_EXECUTORS;
static {
// 初始化线程池
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("track-log-%d").build();
TRACK_LOG_EXECUTORS = new ThreadPoolExecutor(3, 5, 0L, TimeUnit.SECONDS, new LinkedBlockingDeque<>(200), threadFactory, new ThreadPoolExecutor.AbortPolicy());
}
@PostMapping("/shutDownContext")
public String shutDownContext() {
ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) context;
ctx.close();
return "context is shutdown";
}
@GetMapping("/")
public String getIndex() {
return "OK";
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
@PreDestroy
public void preDestroy() {
System.out.println(getCurrentDate()+":ShutDownController is destroyed");
}
private String getCurrentDate() {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return simpleDateFormat.format(new Date());
}
}
启动程序,调用curl -X POST http://localhost:6666/actuator/shutdown,控制台输出:
2020-05-21 23:30:36.459 INFO 67909 --- [ main] c.h.s.s.ShutdowndemoApplication : Starting ShutdowndemoApplication on xianchengs-MacBook-Pro.local with PID 67909 (/Users/ding/Downloads/shutdowndemo-master/target/classes started by ding in /Users/ding/Downloads/shutdowndemo-master)
2020-05-21 23:30:36.462 INFO 67909 --- [ main] c.h.s.s.ShutdowndemoApplication : No active profile set, falling back to default profiles: default
2020-05-21 23:30:37.581 INFO 67909 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 6666 (http)
2020-05-21 23:30:37.600 INFO 67909 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-05-21 23:30:37.600 INFO 67909 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.22]
2020-05-21 23:30:37.674 INFO 67909 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-05-21 23:30:37.674 INFO 67909 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1168 ms
2020-05-21 23:30:38.110 INFO 67909 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-05-21 23:30:38.299 INFO 67909 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint(s) beneath base path '/actuator'
2020-05-21 23:30:38.371 INFO 67909 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 6666 (http) with context path ''
2020-05-21 23:30:38.375 INFO 67909 --- [ main] c.h.s.s.ShutdowndemoApplication : Started ShutdowndemoApplication in 2.222 seconds (JVM running for 2.917)
2020-05-21 23:30:38.847 INFO 67909 --- [)-192.168.0.102] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-05-21 23:30:38.848 INFO 67909 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-05-21 23:30:38.852 INFO 67909 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet : Completed initialization in 4 ms
2020-05-21 23:30:45.165 INFO 67909 --- [ Thread-16] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
2020-05-21 23:30:45:ShutDownController is destroyed
Process finished with exit code 0
为了试验一下优雅关闭只会关闭springboot托管的tomcat的线程池,不会关闭自定义的线程池,我们增加两个接口:
/**
* 测试容器线程池
* @return
* @throws Exception
*/
@GetMapping("/testTomcatThreads")
public String testTomcatThreads() throws Exception{
try {
while (true){
}
}catch (Exception ex){
ex.printStackTrace();
}
return "OK";
}
/**
* 测试自定义线程池
* @return
* @throws Exception
*/
@GetMapping("/testOwnerThreads")
public String testOwnerThreads() throws Exception{
TRACK_LOG_EXECUTORS.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(30000L);
}catch (Exception ex){
ex.printStackTrace();
}
System.out.println(getCurrentDate()+":自定义线程池执行完毕");
}
});
return "OK";
}
先测试容器线程池
curl -X GET http://localhost:6666/testTomcatThreads 后立马调用
curl -X POST http://localhost:6666/actuator/shutdown,控制台输出:
2020-05-21 23:32:30.018 INFO 67924 --- [ main] c.h.s.s.ShutdowndemoApplication : Starting ShutdowndemoApplication on xianchengs-MacBook-Pro.local with PID 67924 (/Users/ding/Downloads/shutdowndemo-master/target/classes started by ding in /Users/ding/Downloads/shutdowndemo-master)
2020-05-21 23:32:30.021 INFO 67924 --- [ main] c.h.s.s.ShutdowndemoApplication : No active profile set, falling back to default profiles: default
2020-05-21 23:32:31.215 INFO 67924 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 6666 (http)
2020-05-21 23:32:31.235 INFO 67924 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-05-21 23:32:31.235 INFO 67924 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.22]
2020-05-21 23:32:31.307 INFO 67924 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-05-21 23:32:31.307 INFO 67924 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1248 ms
2020-05-21 23:32:31.724 INFO 67924 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-05-21 23:32:31.913 INFO 67924 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint(s) beneath base path '/actuator'
2020-05-21 23:32:31.986 INFO 67924 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 6666 (http) with context path ''
2020-05-21 23:32:31.990 INFO 67924 --- [ main] c.h.s.s.ShutdowndemoApplication : Started ShutdowndemoApplication in 2.323 seconds (JVM running for 2.906)
2020-05-21 23:32:32.366 INFO 67924 --- [)-192.168.0.102] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-05-21 23:32:32.366 INFO 67924 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-05-21 23:32:32.372 INFO 67924 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet : Completed initialization in 5 ms
2020-05-21 23:32:41.034 INFO 67924 --- [ Thread-16] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
2020-05-21 23:32:41:ShutDownController is destroyed
Process finished with exit code 0
应用结束了死循环被强制关闭,应用/testTomcatThreads该接口使用的是容器创建的线程,容器close时会强制关闭容器线程池的所有线程的任务。
再测试一下自定义线程池
curl -X GET http://localhost:6666/testOwnerThreads 后立马调用
curl -X POST http://localhost:6666/actuator/shutdown,控制台输出:
2020-05-21 23:34:07.266 INFO 67942 --- [ main] c.h.s.s.ShutdowndemoApplication : Starting ShutdowndemoApplication on xianchengs-MacBook-Pro.local with PID 67942 (/Users/ding/Downloads/shutdowndemo-master/target/classes started by ding in /Users/ding/Downloads/shutdowndemo-master)
2020-05-21 23:34:07.268 INFO 67942 --- [ main] c.h.s.s.ShutdowndemoApplication : No active profile set, falling back to default profiles: default
2020-05-21 23:34:08.419 INFO 67942 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 6666 (http)
2020-05-21 23:34:08.439 INFO 67942 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-05-21 23:34:08.439 INFO 67942 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.22]
2020-05-21 23:34:08.516 INFO 67942 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-05-21 23:34:08.516 INFO 67942 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1216 ms
2020-05-21 23:34:08.883 INFO 67942 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-05-21 23:34:09.074 INFO 67942 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint(s) beneath base path '/actuator'
2020-05-21 23:34:09.145 INFO 67942 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 6666 (http) with context path ''
2020-05-21 23:34:09.149 INFO 67942 --- [ main] c.h.s.s.ShutdowndemoApplication : Started ShutdowndemoApplication in 2.158 seconds (JVM running for 2.626)
2020-05-21 23:34:09.214 INFO 67942 --- [)-192.168.0.102] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-05-21 23:34:09.214 INFO 67942 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-05-21 23:34:09.301 INFO 67942 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet : Completed initialization in 86 ms
2020-05-21 23:34:24.075 INFO 67942 --- [ Thread-15] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
2020-05-21 23:34:24:ShutDownController is destroyed
2020-05-21 23:34:54:自定义线程池执行完毕
从日志中看到
2020-05-21 23:34:24:ShutDownController is destroyed
2020-05-21 23:34:54:自定义线程池执行完毕
这两个相差30秒,正好是sleep的秒数,但是我们一直等待发现未正常关闭容器,始终没出现Process finished with exit code 0,明明线程已经执行完毕,为啥程序不能退出,经过定位发现,自定义线程池设置的最小核心线程个数为3:TRACK_LOG_EXECUTORS = new ThreadPoolExecutor(3, 5, 0L, TimeUnit.SECONDS, new LinkedBlockingDeque<>(200), threadFactory, new ThreadPoolExecutor.AbortPolicy()),虽然业务线程执行完毕,但是核心线程还在,将最小核心线程个数设置为0后,再测试一次容器正常关闭,或者自己手动去调用线程池的关闭接口
@PreDestroy
public void preDestroy() {
/**
* 关闭线程池
*/
TRACK_LOG_EXECUTORS.shutdown();
System.out.println(getCurrentDate()+":ShutDownController is destroyed");
}
重新测试后容器正常关闭退出
分析原理:
先找到定义/actuator/shutdown接口的类
@Endpoint(id = "shutdown", enableByDefault = false)
public class ShutdownEndpoint implements ApplicationContextAware {
@WriteOperation
public Map<String, String> shutdown() {
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();
}
// 此处close 逻辑和上边 shutdownhook 的处理一样
this.context.close();
}
}
this.context.close()调用的是AbstractApplicationContext类的close函数
public void close() {
synchronized(this.startupShutdownMonitor) {
this.doClose();
if (this.shutdownHook != null) {
try {
Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
} catch (IllegalStateException var4) {
}
}
}
}
protected void doClose() {
if (this.active.get() && this.closed.compareAndSet(false, true)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Closing " + this);
}
LiveBeansView.unregisterApplicationContext(this);
try {
this.publishEvent((ApplicationEvent)(new ContextClosedEvent(this)));
} catch (Throwable var3) {
this.logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", var3);
}
if (this.lifecycleProcessor != null) {
try {
this.lifecycleProcessor.onClose();
} catch (Throwable var2) {
this.logger.warn("Exception thrown from LifecycleProcessor on context close", var2);
}
}
this.destroyBeans();
this.closeBeanFactory();
this.onClose();
if (this.earlyApplicationListeners != null) {
this.applicationListeners.clear();
this.applicationListeners.addAll(this.earlyApplicationListeners);
}
this.active.set(false);
}
}
2. 获取程序启动时候的context,然后关闭主程序启动时的context
curl -X GET http://localhost:6666/shutDownContext
控制台输出结果:
...
020-05-22 00:03:33.922 INFO 68220 --- [)-192.168.0.102] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-05-22 00:03:33.922 INFO 68220 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-05-22 00:03:34.013 INFO 68220 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet : Completed initialization in 91 ms
2020-05-22 00:03:43.607 INFO 68220 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
2020-05-22 00:03:43:ShutDownController is destroyed
Process finished with exit code 0
3.通过钩子实现
springboot启动时,AbstractApplicationContext类已经注册好钩子:
public void registerShutdownHook() {
if (this.shutdownHook == null) {
this.shutdownHook = new Thread() {
public void run() {
synchronized(AbstractApplicationContext.this.startupShutdownMonitor) {
AbstractApplicationContext.this.doClose();
}
}
};
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
钩子函数里面也是调用的doClose函数,所以所有的方法底层原理都是一样的,只是触发的方式不同。
这种方法原理是:在springboot启动的时候将进程号写入一个app.pid文件,生成的路径是可以指定的,可以通过命令 cat /Users/dxc/app.id | kill -15 命令直接停止服务(kill -9 不会触发钩子线程),这个时候bean对象的PreDestroy方法也会调用的,而且会自动调用钩子线程,控制台输出为:
...
2020-05-22 00:11:59.248 INFO 68380 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet : Completed initialization in 80 ms
2020-05-22 00:12:43.286 INFO 68380 --- [ Thread-6] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
2020-05-22 00:12:43:ShutDownController is destroyed
Process finished with exit code 143 (interrupted by signal 15: SIGTERM)
总结
优雅关闭的底层原理一致,调用springboot容器的close方法释放回收所有bean和容器创建的系统线程池applicationTaskExecutor,自定义线程池通过在代码类的@PreDestroy注解或者destory-method方法或者DisposableBean接口进行释放操作,来优雅地关闭容器
参考文章:
https://www.cnblogs.com/huangqingshi/p/11370291.html
https://cloud.tencent.com/developer/article/1629897