在系统生命周期中, 免不了要做升级部署, 对于关键服务, 我们应该能做到不停服务完成升级 (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.RELEASE
的Spring 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 -9
。kill -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