前言

kubectl set image 一敲,服务就完成了一次"无感升级"。这篇解剖滚动更新的完整机制——参数怎么调、探针怎么配合、出事怎么秒回滚,以及发布卡住的 N 种原因。

一、滚动更新是怎么转的

初始: [v1][v1][v1]              replicas=3, 假设 maxSurge=1, maxUnavailable=0

步骤1: [v1][v1][v1][v2↑]        起1个v2(总数4=3+maxSurge), 等它 ready
步骤2: [v1][v1]↓[v2][v2↑]       v2 ready 后删1个v1, 再起1个v2...
步骤3: [v1]↓[v2][v2][v2↑]
终态: [v2][v2][v2]              全部替换完成

两个参数控制节奏(Deployment 篇出现过,这里是深水区):

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1                  # 最多超出期望副本数几个(百分比也可: "25%")
    maxUnavailable: 0            # 最多允许几个不可用

四种典型组合:

组合 行为 适用
surge=1, unavailable=0 多起一个再删 → 容量不减 在线业务标配 ✅
surge=0, unavailable=1 先删再起 → 资源省 内部服务/资源紧张
surge=0, unavailable=0 ❌ 死锁(无法开始) 别这么写
surge=50%, unavailable=0 批量多起 副本数大时加速

二、探针是滚动的"裁判"

替换的时机完全由就绪探针裁决:

readinessProbe:
  httpGet: {path: /ready, port: 8080}
  periodSeconds: 5
  failureThreshold: 3
新 Pod: Running ≠ Ready
   └─ ready 探针通过 → Endpoints 加入该 Pod → Deployment 才删下一个旧 Pod
   └─ 一直不 ready → 滚动停滞(卡住时先查它!)

启动慢的应用(JVM/Spring)加 startupProbe,避免 liveness 误杀:

startupProbe:
  httpGet: {path: /healthz, port: 8080}
  failureThreshold: 60        # 60 × 2s = 允许 2 分钟启动
  periodSeconds: 2

三、发布实操全流程

# 1. 触发(三种等价方式)
kubectl set image deploy/api api=reg.local/api:v1.5.0
kubectl edit deploy/api                          # 改 image 字段
kubectl apply -f api.yaml                        # 声明式(推荐, 走 CI)

# 2. 盯进度
kubectl rollout status deploy/api
# Waiting for deployment "api" rollout to finish: 2 of 3 updated...

# 3. 实时看 Pod 更替
kubectl get pods -w

# 4.(可选)金丝雀起手: 先只放一小部分
kubectl patch deploy api -p '{"spec":{"replicas":5}}'  # 扩到5
kubectl set image deploy/api api=reg.local/api:v1.5.0
# 观察 v1.5.0 的 2/5 流量正常后再全量

四、回滚:历史与时光机

# 历史版本(revisionHistoryLimit 控制保留数, 默认10)
kubectl rollout history deploy/api
# deployment.apps/api
# REVISION  CHANGE-CAUSE
# 3         kubectl set image api=v1.5.0
# 4         kubectl set image api=v1.6.0

# 看某版详情
kubectl rollout history deploy/api --revision=3

# 回上一个版本
kubectl rollout undo deploy/api

# 回指定版本
kubectl rollout undo deploy/api --to-revision=3

# 回滚也是一次"滚动"(零中断地滚回去)
kubectl rollout status deploy/api

--to-revision 超过保留数的版本已不可回——重大配置变更前先备份 YAML: kubectl get deploy api -o yaml > api-v1.6.0.yaml

CHANGE-CAUSE 哪来的:kubectl apply 时加注解:

kubectl annotate deploy/api kubernetes.io/change-cause="release v1.6.0: fix payment bug"

五、暂停与恢复:一次改多个东西

改镜像又改环境变量又改资源——不想触发三轮滚动:

kubectl rollout pause deploy/api

kubectl set image deploy/api api=reg.local/api:v1.6.0
kubectl set env deploy/api LOG_LEVEL=debug
kubectl set resources deploy/api -c=api --limits=memory=512Mi

kubectl rollout resume deploy/api      # 恢复后一轮滚动应用全部变更

注意 pause 期间 HPA 扩容仍会生效,但 Deployment 的 spec 更新不触发——用完记得 resume。

六、发布卡住排查地图

# 症状: rollout status 停在 "x of y updated"
kubectl get pods                    # 找到非 Ready 的 Pod
kubectl describe pod <pod>          # 看 Events(九成答案在这)
卡住形态 原因 处理
Pending 资源不够/调度约束 加资源、改 requests、看节点余量
ImagePullBackOff 镜像拉不下来 检查 tag/仓库认证; 国内集群配镜像加速
CrashLoopBackOff 起来就崩 logs --previous;环境变量/配置错误
Running 但不 Ready 就绪探针不过 容器内 curl 探针路径;依赖没起
卡在 Terminating 旧 Pod 删不掉 preStop 卡死/finalizer;优雅关停超时

优雅关停配置(加快滚动、减少请求失败):

spec:
  terminationGracePeriodSeconds: 30
  containers:
  - name: api
    lifecycle:
      preStop:
        exec: {command: ["sleep", "5"]}    # 等 LB/IPVS 规则摘除流量

七、把发布做成"可审计"的

# 谁在什么时候发的什么
kubectl get events --field-selector involvedObject.name=api | tail

# 发布后自动验证(smoke test)
kubectl run smoke --rm -it --image=curlimages/curl --restart=Never -- \
  curl -fsS http://api:8080/healthz

# CI 里的标准段(呼应 CI/CD 篇)
kubectl set image deploy/api api=$IMAGE
kubectl rollout status deploy/api --timeout=180s || \
  (kubectl rollout undo deploy/api && exit 1)    # 失败自动回滚!

小结

需求 命令/配置
零中断 maxUnavailable=0 + readinessProbe
慢启动 startupProbe
回滚 rollout undo [–to-revision=N]
批量改 pause → 改 → resume
卡住 describe 看 Events 分诊
兜底 rollout status 超时 → 自动 undo

本文是「云原生」系列第 14 篇。