Kubernetes 优雅升级---Spring-boot应用
Kubernetes 优雅升级---Spring-boot应用
一.什么是优雅升级?
优雅升级即在对业务和用户无感知的情况下,对系统进行升级
在需要对线上应用做升级或者版本更新时,我们一般要对应用实例做到有计划而且平滑的切换,即对业务无感,不产生任何业务上的中断。更具体的, 是应用实例在收到重启/停机信号后, 马上对调用端隐藏, 同时处理完所有已经收到的请求后, 再重启。 如今互联网基于微服务架构部署越来越流行,且随着kubenetes 越来越成熟,越来越多的原生部署项目逐渐转入容器部署,并通过k8s进行编排。所以我们需要充分利用k8s给我们的资源(configMap),并结合相应相应技术选型来实现应用的优雅升级。此文基于spring-boot项目和k8s-configmap相结合实现应用的优雅升级。
二. spring-boot应用端
1.准备
在最新的 spring boot 2.3 版本,内置此功能,不需要再自行扩展容器线程池来处理, 目前 spring boot 嵌入式支持的 web 服务器(Jetty、Reactor Netty、Tomcat 和 Undertow)以及反应式和基于 Servlet 的 web 应用程序都支持优雅停机功能。 我们来看下如何使用: 当使用 server.shutdown=graceful
启用时,在 web 容器关闭时,web 服务器将不再接收新请求,并将等待活动请求完成的缓冲期。 如下,在配置文件application.yml添加如下配置:
到此应用端的准备就结束了,我们需要写个demo来验证。
2.验证
首先我们新建一个基于spring-boot 2.3.0 的项目,pom.xml文件中spring-boot版本如下,
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
添加web依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
application.yml中添加如下:
server:
shutdown: graceful
port: 8080
现在在启动类中添加controller,如下
@RestController
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
@RequestMapping("/get/{str}")
public Object test(@PathVariable("str") String str)throws InterruptedException{
System.out.println("[start] - > "+str);
Thread.sleep(30*1000L);
System.out.println("[end] - > "+str);
return str;
}
}
源码很简单,就是一个get请求,请求中停滞30s。
现在我们开始验证,验证是否达到以下要求
- 程序收到中断信号后,是否能给予已在程序中执行的任务继续执行的能力,即避免产生业务上的中断异常
- 程序在接收到中断信号后,新的请求是否能够进入程序
执行程序,我们在浏览器上输入: http://localhost:8080/get/1 此时控制台上如下输出:
因为需要30s返回,所以我们此时触发中断,Ctr+C 或者直接点击程序停止按钮,或者Kill -2 进程id 控制台输出如下
可见还没有完全停止,现在我们再请求新的任务, http://localhost:8080/get/2 会看到如下无法请求的页面,
现在回到控制台,由于需要等待30s,所以控制台还没有输出 "[end] - > 1",过几秒后控制台终于输出
可以多测试,最终的结果还是可观的。
3.为什么
到这里你可能会问为什么执行 kill -2 而不是 kill -9 ? kill -2 相当于快捷键 Ctrl + C 会触发 Java 的 ShutdownHook 事件处理(优雅停机或者一些后置处理可参考以下源码)
//ApplicationContext
@Override
public void registerShutdownHook() {
if (this.shutdownHook == null) {
// No shutdown hook registered yet.
this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
@Override
public void run() {
synchronized (startupShutdownMonitor) {
doClose();
}
}
};
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
复制代码
- kill -9,暴力美学强制杀死进程,不会执行 ShutdownHook
三. 结合k8s pod生命周期实现优雅升级
1. k8s pod 终止生命周期
我们来看看 Kubernetes 终止生命周期的各个步骤。
1.1. 将 pod 设置为“正在终止”状态,并将其从所有服务的端点列表中移除
此时,pod 停止获取新流量,Pod 中运行的容器不受影响。
1.2. 执行 preStop 钩子
preStop 钩子是向 pod 中的容器发送的特殊命令或 http 请求。 如果您的应用程序在收到 SIGTERM 后未正常关闭,可使用此钩子触发正常关闭。大多数程序在收到 SIGTERM 后都会正常关闭,但如果您使用的是第三方代码或管理的系统不受您控制,preStop 钩子将是一个不错的方案,可帮您在不修改应用程序的情况下触发正常关闭。
1.3. 向 pod 发送 SIGTERM 信号
此时,Kubernetes 将向 pod 中的容器发送 SIGTERM 信号。此信号通知容器它们即将被关闭。 您的代码应侦听此事件,并在此时开始“干净地”关闭。这可能包括停止所有长时间连接(如数据库连接或 WebSocket 流)、保存当前状态或类似任务。 即使您现在已经在使用 preStop 钩子,也有必要测试一下应用程序在您向它发送 SIGTERM 信号后的反应,以免在实际使用时对实际情况感到惊讶!
1.4. Kubernetes 等待片刻(宽限期)
此时,Kubernetes 将等待片刻,此时间称为终止宽限期,具体值可指定。默认值为 30 秒。需要注意的是,这与 preStop 钩子和 SIGTERM 信号并行发生。Kubernetes 不会等待 preStop 钩子完成。 如果您的应用在 terminationGracePeriod 完成之前完成关闭并退出,Kubernetes 将立即转到下一步。 如果您的 pod 通常需要 30 秒以上的时间才能关闭,请务必延长宽限期。您可以通过在 Pod YAML 中设置 terminationGracePeriodSeconds 选项来实现此目的。例如,可将该值改为 60 秒:
1.5. 向 pod 发送 SIGKILL 信号,pod 随即被移除
如果宽限期结束后容器仍在运行,将向容器发送 SIGKILL 信号并强制将它们移除。此时,所有 Kubernetes 对象将被一同清理。
2.使用 preStop 钩子触发 spring-boot 应用关闭
上文有说到,我们只需要触发执行kill -2 app-pid 就可以优雅关闭spring-boot应用,由于spring-boot项目是运行在docker容器里,我们需要一个触发器,当pod升级时自动触发容器里执行相关的shell命令。prestop钩子在pod终止前会触发,我们可以通过它来触发脚本。
2.1 准备脚本 spring-boot.sh
#!/bin/bash
app=app #app名称
function stop(){
PID=`ps aux | grep ${app} | grep -v grep | grep java | awk '{print $2}'`
echo "查询到相关进程列表ID为:${PID}"
# shellcheck disable=SC2206
pidArray=(${PID// /})
if [ ${pidArray[1]} ];then
echo "开始停止项目: ${app}!!!"
kill -2 ${pidArray[1]} #向应用发送SIGIN信号
sleep 2
else
echo "${app} 没有启动"
fi
}
stop
echo "执行结束!"
exit 0
此脚本需要放入应用所在容器中,有什么方法呢?k8s 里 configmap可以简单实现绑定。
2.2 configmap 脚本映射到容器
首先我们通过rancher界面创建一个configmap(配置映射),如下:
命名空间尽量和应用所在命名空间一致, 将configmap与应用pod绑定,如下:
注意,模式即为权限,设置可执行shell脚本的权限。
2.3 根据需要,设置pod终止最大容忍时间
k8s 默认pod最大容忍时间为30s,我们可以根据自己的项目需要,设置其值,保证不影响业务中断。 在rancher上设置,如下:
保存。
2.4 编辑容器编属yml,添加preStop钩子
很简单,就是执行/home/spring-boot.sh脚本。
2.5 测试升级 当升级应用时,我们会在老应用看到如下日志:
Graceful shtdown 说明优雅升级,读者可以自行验证上文所说的两个优雅升级的要求。