Kubernetes 滚动更新与回滚实战——零中断部署的完整操作手册
Kubernetes 滚动更新与回滚实战——零中断部署的完整操作手册
适读人群:负责 K8s 上线的工程师,想搞定零中断部署的人 | 阅读时长:约 15 分钟 | 核心价值:完整掌握 K8s 滚动更新机制和回滚操作,再也不怕上线
上周三,我们有一次高风险上线——核心用户服务要做一次涉及数据库 schema 变更的重大版本升级,流量高峰期就在晚上 8 点到 10 点,而产品坚持要在 9 点半上线。
这种情况在 K8s 之前,我们通常要准备至少 15 分钟的维护窗口。但这次我们做到了零中断,9 点 31 分版本上线,用户无感知。
这篇文章把我们用的那套部署流程完整写下来。
滚动更新的工作机制
K8s 的滚动更新(Rolling Update)是 Deployment 的默认更新策略,核心逻辑是:
- 按照配置的比例,逐步停掉旧版本 Pod,启动新版本 Pod
- 新版本 Pod 通过健康检查后,才停掉下一批旧版本 Pod
- 整个过程中始终保证有足够的 Pod 在运行
关键配置参数:
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 6
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 2 # 最多可以超出 replicas 的 Pod 数
maxUnavailable: 0 # 最多允许不可用的 Pod 数maxSurge: 2 意思是更新过程中最多同时存在 6+2=8 个 Pod。 maxUnavailable: 0 意思是更新过程中必须始终保持 6 个可用 Pod。
这个组合(maxUnavailable=0)是零停机更新的关键:先启动新 Pod,新 Pod 健康检查通过后再停旧 Pod,始终有 6 个 Pod 在服务。
代价是需要额外的资源来运行新旧并存的 Pod(最多多 2 个)。
让滚动更新真正"零中断"的配置
仅仅设置 maxUnavailable=0 还不够,还需要配置好以下几点。
1. 健康检查必须准确
containers:
- name: user-service
image: user-service:v2.3.1
# 就绪探针:Pod 何时可以接收流量
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20 # 等待 20s 再开始检查(Spring Boot 启动慢)
periodSeconds: 5
failureThreshold: 3
successThreshold: 1
# 存活探针:Pod 是否需要重启
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
failureThreshold: 3
# 启动探针:处理慢启动应用(K8s 1.16+)
startupProbe:
httpGet:
path: /actuator/health
port: 8080
failureThreshold: 30 # 最多等 30 × 10s = 300s
periodSeconds: 10startupProbe 是为慢启动应用设计的——在 startupProbe 通过之前,liveness 和 readiness 都暂停检查,避免应用还在初始化时就被杀死。
readinessProbe 配置错误是零中断部署最常见的失败原因。
2. 优雅关闭配置
旧 Pod 被终止时,K8s 先发送 SIGTERM 信号,然后等待 terminationGracePeriodSeconds(默认 30s),超时后发 SIGKILL 强杀。
应用需要在收到 SIGTERM 后:
- 停止接收新请求
- 等待当前请求处理完
- 释放数据库连接等资源
- 正常退出
spec:
terminationGracePeriodSeconds: 60 # 给足时间优雅退出
containers:
- name: user-service
lifecycle:
preStop:
exec:
# 等待 15s,让 kube-proxy 更新 iptables,流量不再路由到这个 Pod
command: ["/bin/sh", "-c", "sleep 15"]preStop hook 里 sleep 15 这个技巧很重要:Pod 被标记为 Terminating 后,kube-proxy 更新 iptables 规则(从 Service 的 endpoints 里移除这个 Pod)需要一点时间,如果应用立即退出,这段时间内还会有请求路由进来,导致 502 错误。sleep 15s 让 kube-proxy 先更新完。
完整的上线操作流程
#!/bin/bash
# deploy.sh - 标准化部署脚本
set -euo pipefail
APP_NAME="user-service"
NAMESPACE="production"
NEW_VERSION="${1:?'Usage: deploy.sh <version>'}"
TIMEOUT=300s
echo "==> [1/5] 检查当前状态"
kubectl rollout status deployment/${APP_NAME} -n ${NAMESPACE} --timeout=30s
CURRENT_REPLICAS=$(kubectl get deployment ${APP_NAME} -n ${NAMESPACE} -o jsonpath='{.spec.replicas}')
echo "当前副本数: ${CURRENT_REPLICAS}"
echo "==> [2/5] 记录当前镜像版本(用于快速回滚)"
CURRENT_IMAGE=$(kubectl get deployment ${APP_NAME} -n ${NAMESPACE} \
-o jsonpath='{.spec.template.spec.containers[0].image}')
echo "当前镜像: ${CURRENT_IMAGE}"
echo "==> [3/5] 执行滚动更新"
kubectl set image deployment/${APP_NAME} \
${APP_NAME}=registry.example.com/${APP_NAME}:${NEW_VERSION} \
-n ${NAMESPACE} \
--record # 记录到 rollout history
echo "==> [4/5] 等待滚动更新完成"
if ! kubectl rollout status deployment/${APP_NAME} -n ${NAMESPACE} --timeout=${TIMEOUT}; then
echo "!!! 滚动更新超时或失败,自动触发回滚"
kubectl rollout undo deployment/${APP_NAME} -n ${NAMESPACE}
kubectl rollout status deployment/${APP_NAME} -n ${NAMESPACE} --timeout=${TIMEOUT}
echo "回滚完成"
exit 1
fi
echo "==> [5/5] 验证新版本"
kubectl get pods -n ${NAMESPACE} -l app=${APP_NAME} -o wide
echo ""
echo "部署成功!新版本: ${NEW_VERSION}"踩坑实录一:滚动更新卡在 50% 进度
现象:执行更新后,kubectl rollout status 输出一直停在 "3 out of 6 new replicas have been updated",不继续了。
原因:新 Pod 的 readinessProbe 没通过。查看 Pod 日志发现,应用启动时调用了数据库 schema 迁移,迁移时间超过了 readinessProbe 的 initialDelaySeconds,被判定为不健康,然后 Pod 被重启,陷入 CrashLoopBackOff。
解法:
- 延长
initialDelaySeconds到足够容纳 schema 迁移的时间 - 或者把 schema 迁移和应用启动分离——用 Init Container 专门做迁移:
spec:
initContainers:
- name: db-migrate
image: user-service:v2.3.1
command: ["python", "manage.py", "migrate"]
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: DB_HOST
containers:
- name: user-service
image: user-service:v2.3.1
# 这时候 init container 已经跑完迁移,应用直接启动Init Container 是 K8s 设计用来做启动前初始化的,它运行完成后才会启动主容器,完美解决这个问题。
回滚操作
# 查看 rollout 历史
kubectl rollout history deployment/user-service -n production
# 输出:
# REVISION CHANGE-CAUSE
# 1 kubectl set image... user-service=...v2.2.0
# 2 kubectl set image... user-service=...v2.3.0
# 3 kubectl set image... user-service=...v2.3.1
# 查看特定版本的详情
kubectl rollout history deployment/user-service -n production --revision=2
# 回滚到上一个版本
kubectl rollout undo deployment/user-service -n production
# 回滚到指定版本
kubectl rollout undo deployment/user-service -n production --to-revision=2
# 监控回滚进度
kubectl rollout status deployment/user-service -n production踩坑实录二:--record 被弃用,rollout history 没有 CHANGE-CAUSE
现象:查看 rollout history,CHANGE-CAUSE 列全是 <none>,出事了不知道每个版本部署了什么。
原因:kubectl set image --record 在 K8s 1.25+ 里被标记为废弃,但还能用;从 1.28 开始会完全移除。
解法:手动给 Deployment 加 annotation 记录变更原因:
kubectl annotate deployment/user-service -n production \
kubernetes.io/change-cause="deploy v2.3.1: fix payment timeout bug"或者在 CI/CD 流水线里自动设置:
# GitLab CI 示例
deploy:
script:
- kubectl set image deployment/user-service user-service=${IMAGE}:${CI_COMMIT_SHORT_SHA} -n production
- kubectl annotate deployment/user-service -n production --overwrite
kubernetes.io/change-cause="deploy ${CI_COMMIT_SHORT_SHA}: ${CI_COMMIT_MESSAGE}"踩坑实录三:Deployment 更新了但 Pod 还是旧镜像
现象:明明 kubectl set image 成功了,但查看 Pod 发现用的还是旧镜像,应用行为没变化。
原因:镜像 tag 没有更新,还是用的 :latest。K8s 默认的 imagePullPolicy 取决于 tag:如果 tag 是 latest,每次都 Pull;如果是固定 tag(如 v2.3.1),默认 IfNotPresent(有本地缓存就不拉)。
结果就是:镜像仓库里的 latest 更新了,但节点上有旧的 latest 缓存,K8s 不会重新 Pull。
解法:
- 永远不要在生产用
:latesttag,使用 Git commit hash 或语义化版本号 - 如果必须用 latest,设置
imagePullPolicy: Always
containers:
- name: user-service
image: registry.example.com/user-service:v2.3.1 # 固定版本
imagePullPolicy: IfNotPresent # 默认值,可以不写部署策略进阶:蓝绿部署与金丝雀发布
滚动更新是最简单的策略,K8s 也支持更复杂的部署模式:
金丝雀发布:先把少量流量(如 5%)切到新版本,观察一段时间无问题后再全量切换:
# 同时部署两个 Deployment
kubectl scale deployment user-service-stable --replicas=19 -n production
kubectl scale deployment user-service-canary --replicas=1 -n production
# Service 会把流量按 Pod 数比例分发:19:1 ≈ 95% vs 5%蓝绿部署:准备好完整的新版本环境,通过 Service selector 瞬间切换:
# 切换 Service 指向 green 版本
kubectl patch service user-service -n production \
-p '{"spec":{"selector":{"version":"green"}}}'这些进阶策略在有 Istio/Argo Rollouts 的情况下会更方便,这里只是给个思路。
滚动更新够用的情况下,不必引入额外复杂度。
