討論交流
我的两分钱 2023-01-22 111 0 0 0 0
程序开发,Spring Boot,当您的应用因为某种原因需要关闭的时候,如果不做一些收尾工作而突然关闭的话,那么就可能会导致一些异常。有可能此时它正好还有用户请求没有处理完,突然关闭会导致用户看到一个服务器错误。有可能它此时有一个关键…

当您的应用因为某种原因需要关闭的时候,如果不做一些收尾工作而突然关闭的话,那么就可能会导致一些异常。

有可能此时它正好还有用户请求没有处理完,突然关闭会导致用户看到一个服务器错误。

有可能它此时有一个关键的事务还没有提交,突然关闭会导致用户账户异常。

有可能它有一个复杂的计算即将完成,突然关闭会导致下次重新计算。

所以,我们需要一种方式来『优雅』地结束我们的应用。

因为Spring Boot对『优雅』关闭应用有比较好的支持,所以这里就基于Spring Boot,通过一个具体例子来说明如何实现优雅关闭应用程序。

下面这段代码模拟一个耗时的任务,我们看看它在应用程序关闭时的表现:

...
@GetMapping("/longTask")
public String longTask() {
log.info("starting long task");
doLongTask(log);
log.info("long task finished");
return "longTask";
}

private void doLongTask(Logger log) {
try {
for (int i = 0; i < 30; i ++){
log.info("{}", i + 1);
TimeUnit.SECONDS.sleep(1);
}
}
catch (InterruptedException ex) {
log.warn("long task interrupted");
}
}
...

启动应用后,通过浏览器访问 http://host:port/longTask以触发上述耗时任务,不做任何干扰,在日志中我们看到该任务正常结束:

...
c.e.t.controllers.WebController : starting long task
c.e.t.controllers.WebController : 1
c.e.t.controllers.WebController : 2
...
c.e.t.controllers.WebController : 29
c.e.t.controllers.WebController : 30
c.e.t.controllers.WebController : long task finished
...

现在我们试一试在任务执行过程中,通过kill -15(操作系统命令应用程序立即关闭)来关闭应用程序,我们看到该任务被强行中断:

...
c.e.t.controllers.WebController : starting long task
c.e.t.controllers.WebController : 1
c.e.t.controllers.WebController : 2
c.e.t.controllers.WebController : 3
c.e.t.controllers.WebController : long task interrupted
c.e.t.controllers.WebController : long task finished
...

同时在浏览器中我们看到一个错误页面:

这显然是个很不友好的用户体验,Spring Boot对此早有考虑,我们只需要加入一行配置:

server.shutdown=graceful

然后我们再次在任务执行过程中通过kill -15来关闭应用,在日志中看到:

...
c.e.t.controllers.WebController : starting long task
c.e.t.controllers.WebController : 1
c.e.t.controllers.WebController : 2
o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complet
c.e.t.controllers.WebController : 3
c.e.t.controllers.WebController : 4
...
c.e.t.controllers.WebController : 30
c.e.t.controllers.WebController : long task finished
o.s.b.w.e.tomcat.GracefulShutdown : Graceful shutdown complete
..

我们看到虽然收到了关闭提示,但是耗时任务没有被中断,最终成功执行结束,用户端也看到了正常的页面。但如果在应用等待上述长耗时任务结束的过程中,我们通过浏览器再发一个请求的话,则这个新的请求会被应用拒绝,用户将看到一个错误提示。

细心的同学可能会问,如果这个长耗时任务一直不结束,那么应用就会一直等下去不关闭么?Spring Boot也考虑到了这个问题,我们可以加入一个最长等待时间的配置:

spring.lifecycle.timeout-per-shutdown-phase=10s

然后我们再次试着在长任务执行中关闭应用程序,我们将看到:

...
c.e.t.controllers.WebController : starting long task
c.e.t.controllers.WebController : 1
c.e.t.controllers.WebController : 2
o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
c.e.t.controllers.WebController : 3
...
c.e.t.controllers.WebController : 12
...DefaultLifecycleProcessor : Failed to shut down ... within timeout of 10000ms: [webServerGracefulShutdown]
o.s.b.w.e.tomcat.GracefulShutdown : Graceful shutdown aborted with one or more requests still active
...
c.e.t.controllers.WebController : long task interrupted
c.e.t.controllers.WebController : long task finished
...

在最长等待时间达到后,应用程序强行结束了还未完成的任务。

仔细观察上面的日志,大家会看到"tomcat.GracefulShutdown",从这里可以推断出Graceful Shutdown是针对web server的,如果我们在Spring Boot中还有些其它Component或者是Bean,他们和web server没有关系,但是也需要在应用退出前做些清理工作怎么办?

在Spring Boot,只要让我们需要执行清理工作的Component/Bean实现DisposableBean接口就可以了,下面是个例子:

...
@Component
public class MyComponent implements DisposableBean {
...
@Override
public void destroy() throws Exception {
log.info("shutting down MyComponent");
try {
for (int i = 0; i < 30; i ++){
log.info("{}", i + 1);
TimeUnit.SECONDS.sleep(1);
}
}
catch (InterruptedException ex) {
log.warn("clean up interrupted");
}
}
}
...

然后我们再次测试,为了避免干扰,这次不再从浏览器触发上述长任务,我们在日志中看到:

...
o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
o.s.b.w.e.tomcat.GracefulShutdown : Graceful shutdown complete
c.e.t.controllers.MyComponent : 1
c.e.t.controllers.MyComponent : 2
...
c.e.t.controllers.MyComponent : 29
c.e.t.controllers.MyComponent : 30
...

我们看到因为web server没有请求,graceful shutdown迅速结束,但MyComponent并不受graceful shutdown配置的影响,我们配置的10秒等待时间也没有对其进行干扰。

如果对Spring在背后是如何处理关闭逻辑的,可以在AbstractApplicationContext中找到如下代码:

...
@Override
public void registerShutdownHook() {
if (this.shutdownHook == null) {
this.shutdownHook = new Thread() {
@Override
public void run() {
synchronized (startupShutdownMonitor) {
doClose();
}
}
};
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}

@Override
public void close() {
synchronized (this.startupShutdownMonitor) {
doClose();
if (this.shutdownHook != null) {
Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
}
}
}

protected void doClose() {
if (this.active.get() && this.closed.compareAndSet(false, true)) {
LiveBeansView.unregisterApplicationContext(this);
// 发布应用程序关闭事件
publishEvent(new ContextClosedEvent(this));
// 例子中处理耗时任务的Controller是一个Lifecycle Bean
if (this.lifecycleProcessor != null) {
this.lifecycleProcessor.onClose();
}
// 例子中的MyComponent在这里处理
destroyBeans();
// 关闭应用上下文&BeanFactory
closeBeanFactory();
// 执行子类的关闭逻辑
onClose();
this.active.set(false);
}
}
...

当我们发出kill -15命令后,JVM会收到这一通知,Spring 通过JVM的Runtime.getRuntime().addShutdownHook注册了一个hook,该hook调用close函数,并在doClose方法中完成清理工作,最后再把注册的hook清理掉。

有些同学说如果我用kill -9(操作系统强行关闭应用)来关闭应用程序会怎样?我们将在日志中看到:

...
c.e.t.controllers.WebController : starting long task
c.e.t.controllers.WebController : 1
c.e.t.controllers.WebController : 2
c.e.t.controllers.WebController : 3

可以看到如果强杀应用程序的话,上述所有的清理工作都不会执行,应用程序直接结束。

这就是Spring Boot 『优雅』关闭应用程序的方法,希望对您有帮助。


Tag: 程序开发 Spring Boot
歡迎評論
未登錄,
請先 [ 註冊 ] or [ 登錄 ]
(一分鍾即可完成註冊!)
返回首頁     ·   返回[討論交流]   ·   返回頂部