JAVASpring

Spring Boot 优雅停机——Graceful Shutdown

在系统生命周期中, 免不了要做升级部署, 对于关键服务, 我们应该能做到不停服务完成升级 (perform a zero downtime upgrade), 对于一般系统, 应该做到优雅地停服务。
以前经常使用kill -9 <pid>野蛮粗暴进行停止,可能导致此业务逻辑执行失败,在一些业务场景下会出现数据不一致的情况,事务逻辑不会回滚。我们需要在 web 容器关闭时,web 服务器将不再接收新请求,并将有个缓冲期等待活动请求完成。

如何做到不停服务的升级? 需要做到下面两点:

  • 服务本身应该部署多份, 前面应该有 LVS/Haproxy 层或者服务注册组件.
  • 每一份服务能被优雅停机, 即: 在 kill pid 命令发出后, 程序应该能拒绝新的请求, 但应该继续完成已有请求的处理。

Spring Boot 2.3 新特性优雅停机,其他版本需要编写相关代码来hook 关机事件,可以网上搜索其他文章。

预备知识


============================
Linux kill 命令
============================

kill 命令常用的信号选项:

  • (1) kill -2 pid 向指定 pid 发送 SIGINT 中断信号, 等同于 ctrl+c.
  • (2) kill -9 pid, 向指定 pid 发送 SIGKILL 立即终止信号.
  • (3) kill -15 pid, 向指定 pid 发送 SIGTERM 终止信号.
  • (4) kill pid 等同于 kill 15 pid

SIGINT/SIGKILL/SIGTERM 信号的区别:

  • (1) SIGINT (ctrl+c) 信号 (信号编号为 2), 信号会被当前进程树接收到, 也就说, 不仅当前进程会收到该信号, 而且它的子进程也会收到.
  • (2) SIGKILL 信号 (信号编号为 9), 程序不能捕获该信号, 最粗暴最快速结束程序的方法.
  • (3) SIGTERM 信号 (信号编号为 15), 信号会被当前进程接收到, 但它的子进程不会收到, 如果当前进程被 kill 掉, 它的的子进程的父进程将变成 init 进程 (init 进程是那个 pid 为 1 的进程)

一般要结束某个进程, 我们应该优先使用 kill pid , 而不是 kill -9 pid. 如果对应程序提供优雅关闭机制的话, 在完全退出之前, 先可以做一些善后处理。

官方说明


Graceful shutdown
Graceful shutdown is supported with all four embedded web servers (Jetty, Reactor Netty, Tomcat, and Undertow) and with both reactive and Servlet-based web applications. When enabled using server.shutdown=graceful, upon shutdown, the web server will no longer permit new requests and will wait for a grace period for active requests to complete. The grace period can be configured using spring.lifecycle.timeout-per-shutdown-phase.

本地环境升级


上图可知Spring boot的版本要求2.3及以上,Tomcat版本9.0.33或更新。
我的版本分别为2.2.2和9.0.29不符合要求,版本查看方法见下图:

pom.xml升级<artifactId>spring-boot-starter-parent</artifactId>版本号为:<version>2.3.0.RELEASE</version>
提示错误:Could not find artifact org.springframework.boot:spring-boot-starter-parent:pom:2.3.0.RELEASE in nexus-aliyun(http://maven.aliyun.com/nexus/content/groups/public)
应该是这个阿里云仓库地址找不到最新版本2.3.0.RELEASESpring boot依赖,后在仓库grails-core找到,修改阿里云的maven仓库地址为如下:

  <mirrors>
        <mirror>
            <id>aliyunmaven-public</id>
            <mirrorOf>*</mirrorOf>
            <name>阿里云公共仓库</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </mirror>
         <mirror>
            <id>aliyunmaven-grails</id>
            <mirrorOf>*</mirrorOf>
            <name>阿里云grails仓库</name>
            <url>https://maven.aliyun.com/repository/grails-core</url>
        </mirror>
    </mirrors>

点击Maven窗口的重新导入依赖按钮,重新拉取依赖包。

没有报错,开始同步最新版的依赖项。

再次查看版本,已经符合要求

开启Graceful Shutdown配置


在最新版的Spring Boot 2.3中终于集成了优雅退出(Graceful shutdown),在官方文档中可以看到内置的 web 服务器(Jetty、Reactor Netty、Tomcat 和 Undertow)以及反应式和基于 Servlet 的 web 应用程序都支持优雅退出功能。当server.shutdown=graceful启用时,在 web 容器关闭时,web 服务器将不再接收新请求,并将等待活动请求完成的缓冲期。缓冲期 timeout-per-shutdown-phase 配置
默认时间为 20s, 意味着最大等待 20s,超时无论线程任务是否执行完毕都会停机处理,一定要合理设置缓冲期大小。

使用方式很简单,只需要配置一下yml文件即可:

server:
  shutdown: graceful #开启优雅停机,默认是立即停机IMMEDIATE
spring:
  lifecycle:
    timeout-per-shutdown-phase: 20s #缓冲器即最大等待时间

properties文件方式:

server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=20s

体验


1. 代码,模拟需要15s长时间才能处理完成的业务。

    @ApiVersion(5)
    @RequestMapping(value = "/gracefulshutdown")
    // http://localhost:8555/v5/packageIndex/gracefulshutdown
    public String gracefulShutdown() throws InterruptedException {
        // 模拟业务耗时处理流程
        Thread.sleep(15 * 1000L);
        return "业务处理完毕";
    }

2.打包后上传Linux服务器,启动项目# java -jar bank_router-1.0.0-SNAPSHOT-exec.jar

[root@izuf672oio5mc4fbyj0s0jz mp-springboot]# java -jar bank_router-1.0.0-SNAPSHOT-exec.jar 
2020-05-24 21:17:50.538  INFO 1397 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration' of type [org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration$$EnhancerBySpringCGLIB$$8296e9c7] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.0.RELEASE)

3.调用服务地址:curl localhost:8080/v5/packageIndex/gracefulshutdown,15秒后才有返回结果。

4.关闭服务,执行kill -2或者Ctrl + C

此处执行kill -2 而不是kill -9kill -2 相当于快捷键Ctrl + C会触发 Java 的 ShutdownHook 事件处理。

[root@izuf672oio5mc4fbyj0s0jz ~]# ps aux |grep java
root      1397 40.3 12.0 4651892 948500 pts/0  Sl+  21:17   0:37 java -jar bank_router-1.0.0-SNAPSHOT-exec.jar

5.服务端接收到了指令,进行关闭操作。

注意中间有13秒等待结束所有的请求。

2020-05-24 21:22:07 [SpringContextShutdownHook] INFO  o.s.boot.web.embedded.tomcat.GracefulShutdown:53 -Commencing graceful shutdown. Waiting for active requests to complete
2020-05-24 21:22:07 [tomcat-shutdown] INFO  o.s.boot.web.embedded.tomcat.GracefulShutdown:78 -Graceful shutdown complete
2020-05-24 21:22:07 [SpringContextShutdownHook] INFO  o.s.boot.web.embedded.tomcat.GracefulShutdown:53 -Commencing graceful shutdown. Waiting for active requests to complete
###############
这一段时间间隔是我访问的url还没有返回结果,等处理完我的访问后继续下面的关闭操作。
###############
2020-05-24 21:22:20 [tomcat-shutdown] INFO  o.s.boot.web.embedded.tomcat.GracefulShutdown:78 -Graceful shutdown complete
2020-05-24 21:22:20 [SpringContextShutdownHook] INFO  o.s.scheduling.concurrent.ThreadPoolTaskScheduler:218 -Shutting down ExecutorService
2020-05-24 21:22:20 [SpringContextShutdownHook] INFO  o.s.orm.jpa.LocalContainerEntityManagerFactoryBean:598 -Closing JPA EntityManagerFactory for persistence unit 'default'
2020-05-24 21:22:20 [SpringContextShutdownHook] INFO  com.alibaba.druid.pool.DruidDataSource:1948 -{dataSource-1} closing ...
2020-05-24 21:22:20 [SpringContextShutdownHook] INFO  com.alibaba.druid.pool.DruidDataSource:2020 -{dataSource-1} closed