故事背景
多个项目一起发布,经常出现部分请求失败或者超时,影响用户体验。造成这个情况的原因是容器收到退出信号直接关闭了,此时Java程序还有部分请求还没处理完,然后网关就会报请求超时。为了避免这个情况,提高发布期间服务的可用性,我们需要实现优雅关闭(graceful shutdown)。
优化方案
在讨论方案之前先要了解一下在发布期间容器的生命周期。
图中可以发现K8S其实是支持自定义钩子,在容器停止前有30秒的时间处理未完成的请求。
上图是SpringBoot官方文档对K8S容器如何实现优雅关闭的描述,总结一下我们需要做以下2点:
- 定义preStop操作
- 处理SIGTERM信号
preStop
结合云平台的配置,我们需要在镜像里增加一个脚本,如/opt/preStop.sh。
参考脚本
#!/bin/bash # 退出容器前的预处理 # 获取待kill的进程ID,根据自己的业务需要填写这个匹配规则 pids=`pgrep -f main.jar` if [ "$pids" != "" ]; then # 注意kill指令是非阻塞的 echo "$pids" | xargs kill fi # 由于kill指令是非阻塞的,所以需要sleep一段时间来等待程序处理完成,sleep时长需要结合业务需要调整 sleep 15 |
因为这个脚本一退出容器就会重启,所以一定要阻塞等待,sleep是一种方式,也可以使用循环来检查进程是否都退出完毕 除了增加preStop脚本,还有一个更简单的方法,Java进程改为1号进程,在启动脚本的java命令前加上exec,使关闭信号能被jvm接收到。
SIGTERM
SpringBoot2.3以上版本开始支持优雅关闭,需要配置如下:
server: shutdown: graceful #启用优雅停止 spring: lifecycle: timeout-per-shutdown-phase: 20s #强制终止的宽限时间,要小于30秒 |
Tomcat、Undertow等WEB容器都支持优雅关闭。
Nacos需要使用alibaba的starter才能支持,关闭动作包括注销实例和停止心跳。
目前的SpringBoot是2.1版本,升级成本太高,官方Issue提供一个简单的实现:https://github.com/spring-projects/spring-boot/issues/4657#issuecomment-161354811。
大家可能好奇Java是如何处理关闭信号的,这里简单介绍一下实现原理,JVM提供了关闭钩子,在关闭时可以利用一个线程来做一些收尾工作,所以信号实际是JVM处理的。
// JVM关闭前会先调用之前注册的所有钩子 Runtime.getRuntime().addShutdownHook(); |
容器重启效果图
SmartLifeCycle
除了Nacos,还需要实现优雅关闭的场景如下:
- Kafka
- 其他定时任务
可以通过实现SpringBoot的SmartLifeCycle接口实现。
服务秒级上下线通知
使用的网关是SpringCloudGateway,负载均衡的组件是Ribbon,它的拉取服务列表并刷新缓存的默认实现是定时30秒,当服务下线后,请求进来有可能转发到已经下线的服务。
解决方案:
Eureka的实现是订阅服务的变更,及时刷新缓存,我们可以参考它的实现,结合已有的定时刷新,推拉结合,实现实时的上下线通知。
官方的issue,nacos的spring-cloud-starter不支持实时上下线通知的问题存在已久,https://github.com/alibaba/spring-cloud-alibaba/issues/1554
版权声明:《 Java程序在K8S下实现优雅关闭 》为Saber原创文章,转载请注明出处!
最后编辑:2022-6-1 06:06:53