Spring Boot 应用中的优雅停机配置与验证方法
前阵子发版的时候直接 kill 掉了正在跑的服务,结果运营过一会儿找过来说用户下单下到一半页面直接报错,看了一眼网关日志,一堆 connection reset。仔细一想才反应过来,Spring Boot 默认的停机方式就是把内嵌的 Tomcat 直接关掉,正在处理的请求也不管了,连接说断就断,对用户来说就是操作到一半突然报错。这事儿说大不大,说小也不小,所以这次专门花时间研究了一下优雅停机,下面是实测可用的配置和验证方法。
配置方法
其实 Spring Boot 从 2.3 开始就自带优雅停机了,只是默认没开,需要在 application.yml 里加两处配置
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
server.shutdown=graceful 就是开关,开了之后,应用收到停机信号的时候,Tomcat(用 Undertow 也一样)会先不再接收新请求,然后等已经在处理的请求跑完,最多等 timeout-per-shutdown-phase 配置的时间,默认 30s,超过还没跑完的就只能强杀了。这个时间根据自己业务里最长耗时的接口来定,我这里接口都不长,设个 30s 够用了。
注意一下版本,2.3 之前没有这套配置,得自己去实现 SmartLifecycle 手动处理,如果你的项目还停留在 2.2 甚至 1.x,要么升级,要么自己写代码吧,网上方案不少,这里就不展开了。
验证方法
配置加上以后得实际验证一遍,不然你也不知道它到底生效没有。思路很简单:写一个故意耗时的接口,请求发出去的瞬间把应用停掉,看这个请求能不能正常返回。
先写个测试接口
@GetMapping("/test")
public String test() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "ok";
}
然后启动应用,另开一个终端 curl 一下
curl http://localhost:8080/test
curl 发出去之后(接口里 sleep 了 10 秒,时间很充裕),马上对进程执行 kill -15。注意别用 kill -9,-9 是直接把进程干掉,JVM 连执行停机逻辑的机会都没有,优雅停机也就无从谈起了,Spring Boot 靠的就是 SIGTERM 信号触发 shutdown hook。
配置生效的话,应用日志里应该能看到类似这样的输出
Commencing graceful shutdown. Waiting for active requests to complete
Graceful shutdown complete
而 curl 那边等 10 秒过后会正常拿到 ok,不会报错。
作为对比,把 server.shutdown 这行去掉再试一次,同样的操作,curl 大概率直接 Empty reply from server,因为 Tomcat 被直接关掉了,连接都没了。这样一对比,效果就一目了然了。
部署在 Kubernetes 里的注意点
服务如果是跑在 K8s 里的,光配 Spring Boot 还不够。Pod 删除的时候,kubelet 发 SIGTERM 的同时会把 Pod 从 Service 的 endpoints 里摘掉,但这两件事是同时进行的,路由规则刷新有延迟,存在一个窗口期,新请求还是可能被分到正在停机的 Pod 上,直接被拒掉。
常见的做法是加个 preStop 钩子,先 sleep 几秒,等路由刷新完再走停机流程
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"]
另外注意 terminationGracePeriodSeconds,默认 30s,这个时间要大于 preStop 的 sleep 加上优雅停机的 timeout,不然 Pod 还没等请求跑完就被强杀了,前面的配置等于白做。比如 sleep 5s + timeout 30s,这个值至少给到 40s 以上。
其他补充
优雅停机只管 web 请求这一摊,项目里如果有定时任务、MQ 消费者这些,得另外处理。简单的做法是在需要清理逻辑的 Bean 上加 @PreDestroy,容器关闭的时候会调用它来释放资源,比如关闭消费者连接、把没跑完的任务状态落库。线程池的话记得 shutdown() 之后配合 awaitTermination() 等一会儿,别直接 shutdownNow(),不然队列里没跑的任务又丢了。
最后贴一下完整配置,实测 Spring Boot 3.2.x 可用
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
改完发个版,用上面 curl + kill -15 的套路验证一遍,不出问题的话就没有问题了。
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。