一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
我所在公司主力开发语言为 Java, 编写的程序运行于 K8S 环境之中,运行时环境为 Docker,自动发布的流水线已经完成许久,可以由开发人员自主执行发布操作,有一天我在发布一个紧急更新的时候,发现整个发布过程非常缓慢,于是我对整个发布流水线的代码进行了梳理,最后发现瓶颈在容器的更新阶段,当发布系统执行 kubectl apply new_version.yaml进行更新操作时,容器的 Terminate 时间非常长,大概需要 60 秒,于由容器更新的 maxSurge 值设置为 1,那么多个容器更新使用的时间就会更长,对此我展开了进一步的研究。
先看一下当前容器的启动方式
Dockerfile中先调用start.sh脚本:
FROM xxxx
.....
WORKDIR /
CMD /start.sh
start.sh脚本中的内容:
java ${JAVA_OPTS} -Dspring.profiles.active="${SPRING_APP_MODE}" -Djava.security.egd=file:/dev/./urandom -jar app.jar
通过这里的配置可以发现,容器中的进程是 CMD 命令运行 start.sh 脚本间接执行的,那么最直观的问题是:容器进程无法直接收到来自K8S的SIGTERM信号,所以K8S会等待terminationGracePeriodSeconds指定的时间后 kill 掉 pod,我们的时间配置为 60s,所以这里可以解释为什么我们每个容器的关闭时间都需要等待 60s。
正常情况下,建议每个容器中只运行一个进程,但这只是理想状态下的情况,出于很多业务场景和历史原因,一个容器中要运行多个进程,比如我司使用脚本间接运行进程的做法可能也是出于需要执行多个进程的历史原因,后来被改成了运行单个进程。对于容器内运行多个进程的需求,可以考虑使用 Tini 这个项目,它会默认把收到的常用信号量转发给子进程。
所以我的改动是,直接把启动命令写在了 Dockerfile 的 CMD 命令之后,这样K8S发送的SIGTERM信号可直接被Java进程接收到:
FROM xxxx
.....
WORKDIR /
CMD java ${JAVA_OPTS} -Dspring.profiles.active="${SPRING_APP_MODE}" -Djava.security.egd=file:/dev/./urandom -jar app.jar
JAVA进程需要对接收到的信号量进行相应处理
单纯的把信号量传给 Java 进程是无用的,还需要在 Java 代码中对收到的信号量做进一步的处理,我们使用的是Spring boot 框架,高版本的 Spring boot 提供了方便的信号量处理机制,直接使用即可,如果Spring boot 版本过低,则可能需要先进行升级。