如何优雅地关闭SpringBoot应用程序?听我给你讲

2,297 阅读6分钟

前言

Hi,大家好,我是麦洛,今天来聊聊如何优雅地关闭SpringBoot应用程序,有需要交流的朋友,可以私信我或者加我微信:miloleex都可以哈

在我们日常开发中,我们如何启停服务?可能下面的命令在熟悉不过了。

ps -ef|grep 8888
kill -9 8888
./startup.sh ;tail -f ../logs/catalina.out

暴利美学式的启停服务真的安全吗?今天我们来了解如何安全、优雅地停止 Spring Boot 应用程序,而不会使任何当前处理的请求失败或不中断正在进行的事务。

什么是正常关机?

优雅和强制关闭是停止应用程序的两种方法。当应用程序运行时,在处理请求的过程中。我们都知道它会消耗各种资源、建立连接、持久化数据或处理事务等。当我们修复了某个bug或者需要发版时,就需要我们停止当前正在运行的应用程序,以便完成我们的工作。这时候,采取合适的手段就显得尤为重要。

为了说明这个现象,我们来举一个例子。我们生活中关闭计算机时。如果遇到未保存的文件或一些程序未关闭,windows会提示我们关机之前完成这些工作。如果我们强制关机,就会丢失一些我们想要的东西,同样的逻辑也适用于我们的应用程序。幸运的是,某些应用程序能够在重新启动时可以恢复任何未完成的任务。这样停止不会造成任何伤害。另一方面,对于某些应用程序,突然关闭可能会导致意外结果。例如,非常大的事务失败,或打开资源等。那么Spring Boot 应用程序我们如何优雅的关机,接下来我们一起来看看,微信搜一搜"爱写Bug的麦洛"公众号关注我,一起努力写Bug,哈哈

未启用正常关机

当我们停止正在运行的应用程序或进程时,底层操作系统会向进程发送终止信号。在没有启用任何优雅关闭机制的情况下,Spring Boot 应用程序将在收到信号后立即终止。

为了演示这种行为,让我们编写一个 Controller 端点,它在返回之前需要更长的时间。或者,我们可以简单地让线程休眠相当长的时间,以便我们有机会在请求处理过程中停止应用程序。

@PostMapping("/students")
public void test(@RequestBody Student student) {
    log.info("新请求新增一个学生");
    studentService.addStudent(student);
    log.info("新增学生成功");
}

首先,我们将启动应用程序并向该端点执行请求。之后,一旦请求开始处理,我们将尝试停止应用程序。最后,我们将观察日志。

11:04:44|INFO | o.s.w.s.DispatcherServlet:547 - Completed initialization in 33 ms
11:04:44|INFO | c.a.s.t.s.w.StudentController:38 - 新请求新增一个学生
11:04:53|INFO | o.s.s.c.ThreadPoolTaskExecutor:218 - Shutting down ExecutorService 'applicationTaskExecutor'
11:04:53|INFO | c.z.h.HikariDataSource:350 - HikariPool-1 - Shutdown initiated...
11:04:53|INFO | c.z.h.HikariDataSource:352 - HikariPool-1 - Shutdown completed.

日志表明在 POST 请求完成之前,我们尝试关闭应用程序。从日志中可以明显看出,应用程序在没有等待请求完成就突然停止。

启用正常关机

Spring Boot 支持自动配置的优雅关机,配置非常简单。下一个配置片段显示了如何在 Spring Boot 中启用优雅关闭。

yaml文件:

server:
  shutdown: graceful

属性文件:

server.shutdown=graceful

启用此功能后,Spring Boot 将在完全关闭应用程序上下文之前等待当前请求完成。此外,在关闭阶段,它将停止接受新请求。Spring Boot 的所有嵌入式服务器都支持优雅终止。但是,拒绝新请求的方式可能会因各个服务器的实现而异。

这里演示如何在停止 Spring Boot 应用程序之前允许当前正在运行的 HTTP 请求完成。现在我们已经启用了正常关闭,我们将重新运行控制器端点并尝试在请求完成之前停止应用程序。

@PostMapping("/students")
public void test(@RequestBody Student student) {
    log.info("新请求新增一个学生");
    studentService.addStudent(student);
    log.info("新增学生成功");
}

让我们执行 POST 端点,并立即停止应用程序

14:14:57|INFO | c.a.s.t.s.w.StudentController:38 - 新请求新增一个学生
14:14:58|INFO | o.s.b.w.e.t.GracefulShutdown:53 - Commencing graceful shutdown. Waiting for active requests to complete
14:15:07|INFO | c.a.s.t.s.w.StudentController:40 - 新增学生成功
14:15:07|INFO | o.s.b.w.e.t.GracefulShutdown:78 - Graceful shutdown complete
14:15:07|INFO | o.s.s.c.ThreadPoolTaskExecutor:218 - Shutting down ExecutorService 'applicationTaskExecutor'
14:15:07|INFO | c.z.h.HikariDataSource:350 - HikariPool-1 - Shutdown initiated...
14:15:07|INFO | c.z.h.HikariDataSource:352 - HikariPool-1 - Shutdown completed.

从日志中可以看出,在调用后立即收到了关闭信号。但是,Spring Boot 仍然允许在完全停止应用程序上下文之前完成请求。

正常关机超时

在正常关闭期间,Spring Boot 允许应用程序在一些宽限期内完成所有当前请求或进程。一旦宽限期结束,未完成的进程或请求就会被终止。默认情况下,Spring Boot 允许 30 秒的正常关闭超时。但是,我们可以使用应用程序属性或 Yaml 文件对其进行配置。

yaml文件:

spring:
  lifecycle:
    timeout-per-shutdown-phase: "10s"

属性文件:

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

不同嵌入式 Web 服务器优雅停机行为区别?

容器停机行为取决于具体的 web 容器行为

web 容器名称行为说明
Tomcat 9.0.33+停止接受网络层的请求,客户端新请求等待超时。
Reactor Netty停止接受网络层的请求,客户端新请求等待超时。
Undertow接受请求,客户端新请求直接返回 503。
Jetty停止接受网络层的请求,客户端新请求等待超时。

特别说明

官网中有这样一段说明,如果你的IDE无法正确发送SIGTERM信号,可能无法在本地测试

总结

在上文中,我们学习了如何优雅地关闭Spring Boot 应用程序。配置启用正常关闭后,Spring Boot 允许应用程序在完全停止应用程序之前完成任何正在进行的请求或进程。此外,它可以防止任何新请求到达应用程序。而且允许在 30 秒的默认宽限期内完成正在进行的请求。但是,我们可以更改此设置以指定自定义超时值或宽限期。