前言
在SpringBoot应用需要停机时,如果直接关闭应用,会导致部分正在处理中的请求被强制中断,这在某些业务场景中会产生脏数据。
为了解决这一问题,SpringBoot 2.3中引入了“优雅停机”的新特性。
14.4 SpringBoot 2.3新特性:优雅停机
“优雅停机”的新特性,是指在SpringBoot应用被关闭时(注意此处的关闭可以是kill -2
,但不能是kill -9
),会预留一小段时间,使应用内部的业务线程执行完毕。此时嵌入式Web容器不允许有新的请求进入,以此达到优雅停机的效果。
14.4.1 测试优雅停机场景
编写一个Controller方法,使线程休眠10s来模拟业务逻辑的处理时间很长:
@RestController
public class GracefulTestController {
public String test() throws InterruptedException {
System.out.println("开始时间:" + new Date());
TimeUnit.SECONDS.sleep(10);
System.out.println("结束时间:" + new Date());
return "success";
}
}
默认情况下,SpringBoot的停机方式是立即停机(immediate),若想启用优雅停机,需要在application.properties中配置server.shutdown=graceful。此外还可以通过配置spring.lifecycle.timeout-per-shutdown-phase属性,自定义缓冲时间(默认30s)。
编写主启动类,借助Scanner实现控制台软退出:
@SpringBootApplication
public class WebMvcApp {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(WebMvcApp.class, args);
// 借助Scanner实现控制台软退出
// 当在控制台手动输入exit字符串时,退出应用
Scanner scanner = new Scanner(System.in);
while (true) {
String input = scanner.nextLine();
if ("exit".equals(input)) {
break;
}
}
SpringApplication.exit(context);
}
}
启动项目,在浏览器访问http://localhost:8080/test
,正常情况下,浏览器在等待10s后会接收到"success"的字符串响应。
开始时间:Mon Mar 04 22:57:58 CST 2024
结束时间:Mon Mar 04 22:58:08 CST 2024
如果在请求处理的过程中,手动在控制台输入字符串exit以停止SpringBoot应用,可以发现浏览器仍然会等待响应并接收到"success"的字符串响应。随后,SpringBoot应用才会停止。
开始时间:Mon Mar 04 23:00:10 CST 2024
exit
Commencing graceful shutdown. Waiting for active requests to complete
结束时间:Mon Mar 04 23:00:20 CST 2024
Graceful shutdown complete
Shutting down ExecutorService 'applicationTaskExecutor'
从控制台的输出信息可知,优雅停机测试成功。
另外,不同的嵌入式Web容器在优雅停机期间应对客户端新请求的响应策略不同:嵌入式Tomcat和Netty不会接收请求,客户端会响应超时;嵌入式Undertow则会直接响应503错误。
14.4.2 优雅停机的实现原理
以嵌入式Tomcat为例:
源码1:TomcatServletWebServerFactory.java
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown());
}
public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
// 初始化优雅停机的回调钩子
this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
initialize();
}
源码2:AbstractConfigurableWebServerFactory.java
private Shutdown shutdown = Shutdown.IMMEDIATE;
public Shutdown getShutdown() {
return this.shutdown;
}
由 源码1-2 可知,在TomcatServletWebServerFactory创建TomcatWebServer时,第三个参数是getShutdown
方法返回的Shutdown类。而Shutdown类是一个枚举类,getShutdown
方法默认返回立即停机,即Shutdown.IMMEDIATE。
此处的shutdown属性则对应了application.properties文件中的server.shutdown配置。因此上面的测试案例中,该属性已被修改为graceful。
在 SpringBoot源码解读与原理分析(二十七)嵌入式Tomcat 8.3.2 Web容器关闭相关的回调 中提到,WebServerGracefulShutdownLifecycle用于触发嵌入式Web容器优雅停机的核心生命周期回调,它可以在IOC容器销毁阶段回调其stop
方法以触发销毁逻辑。而其stop
方法会回调WebServer的shutDownGracefully
方法实现优雅停机。
源码3:WebServerGracefulShutdownLifecycle.java
@Override
public void stop(Runnable callback) {
this.running = false;
this.serverManager.shutDownGracefully(callback);
}
源码4:WebServerManager.java
void shutDownGracefully(Runnable callback) {
this.webServer.shutDownGracefully((result) -> callback.run());
}
源码5:TomcatWebServer.java
@Override
public void shutDownGracefully(GracefulShutdownCallback callback) {
if (this.gracefulShutdown == null) {
callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
return;
}
// 此处会执行优雅停机
this.gracefulShutdown.shutDownGracefully(callback);
}
由 源码3-5 可知,嵌入式Tomcat容器的优雅停机最终会调用TomcatWebServer的shutDownGracefully
方法。
源码6:GracefulShutdown.java
void shutDownGracefully(GracefulShutdownCallback callback) {
logger.info("Commencing graceful shutdown. Waiting for active requests to complete");
// 启动一个新的线程
new Thread(() -> doShutdown(callback), "tomcat-shutdown").start();
}
private void doShutdown(GracefulShutdownCallback callback) {
// 关闭Connector,服务失去接收请求的能力
List<Connector> connectors = getConnectors();
connectors.forEach(this::close);
try {
for (Container host : this.tomcat.getEngine().findChildren()) {
for (Container context : host.findChildren()) {
// 每隔50s检查一次Container是否停止
while (isActive(context)) {
if (this.aborted) {
// logger ...
callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
return;
}
Thread.sleep(50);
}
}
}
} // catch ...
logger.info("Graceful shutdown complete");
callback.shutdownComplete(GracefulShutdownResult.IDLE);
}
由 源码6 可知,shutDownGracefully
方法完成的工作是延迟关闭嵌入式Web容器。它会在内部启动一个新的线程,执行doShutdown
方法。
而doShutdown
方法首先会关闭Connector,由此Tomcat就失去了接收新请求的能力;随后该方法会提取出嵌入式Tomcat中所有Engine中的所有Container,每隔50s检查一次Container是否停止,当所有Context中的线程全部执行完毕,即Context全部停止时,优雅停机流程执行完毕。
14.5 小结
第14章到此就梳理完毕了,本章的主题是:运行SpringBoot应用。回顾一下本章的梳理的内容:
(四十)基于jar/war包的运行机制
(四十一)SpringBoot 2.3新特性:优雅停机
更多内容请查阅分类专栏:SpringBoot源码解读与原理分析
至此,完结撒花,该专栏的内容全部学习完毕,后续将根据实际情况添加新的内容。