前置知识: Deployment、ReplicaSet、Pod 的基本关系, kubectl applykubectl get 的用法。


一、Deployment 与 ReplicaSet 的版本管理体系

理解滚动更新之前,必须先弄清楚 Deployment 管理 ReplicaSet 的方式。

一个 Deployment 每执行一次 kubectl apply,就会创建一个新的 ReplicaSet,而旧 ReplicaSet 不会被立即删除——它作为历史快照被保留下来,这就是回滚的物理基础。

Deployment (revision=3, 当前)

Deployment (revision=2)

Deployment (revision=1)

保留历史快照
revisionHistoryLimit=10

保留历史快照
revisionHistoryLimit=10

ReplicaSet-v1

Pod: nginx-7d9fb8c9-x1

Pod: nginx-7d9fb8c9-x2

ReplicaSet-v2

Pod: nginx-8d9fb8c9-x1

Pod: nginx-8d9fb8c9-x2

ReplicaSet-v3

Pod: nginx-9d9fb8c9-x1

Pod: nginx-9d9fb8c9-x2

保留

保留

活跃

每个 ReplicaSet 上都有一个 Annotation,记录创建它时的 Pod 模板快照:

kubectl get rs nginx-7d9fb8c9 -o jsonpath='{.metadata.annotations}'
# 输出示例:
# {"deployment.kubernetes.io/revision": "1",
#  "kubernetes.io/change-cause": "kubectl apply --filename=deployment-v1.yaml"}
kubectl get rs nginx-7d9fb8c9 -o jsonpath='{.spec.template}'

这个 spec.template 就是回滚时恢复的完整 Pod 定义——不是增量 diff,而是完整的快照。

revisionHistoryLimit 默认为 10,表示最多保留 10 个历史 ReplicaSet。设置为 0 时不保留历史,无法回滚。


二、滚动更新策略:maxSurge 与 maxUnavailable 的精确计算

2.1 两个参数的实际含义

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%        # 最多比期望副本数多出多少
      maxUnavailable: 0    # 最多允许多少个 Pod 不可用
参数 含义 取值范围
maxUnavailable 滚动更新期间允许不可用的 Pod 上限 绝对数或百分比,可为 0
maxSurge 滚动更新期间最多超出期望副本数的 Pod 上限 绝对数或百分比,可以为 0

关键点:两个参数不能同时为 0——那样滚动更新就没有任何操作空间,K8s 会直接拒绝。

2.2 精确计算:任意时刻的 Pod 数量

设期望副本数 replicas=NmaxUnavailable=PmaxSurge=S

  • 不可用 Pod 上限min(N, P)(若 P 为绝对数,直接取 P;若为百分比,取 ⌊N × P%⌋
  • 总 Pod 数上限N + S(若 S 为绝对数,取 S;若为百分比,取 N + ⌊N × S%⌋

replicas=5, maxUnavailable=2, maxSurge=1 为例:

阶段 旧 Pod 新 Pod 总 Pod 不可用 Pod
开始前 5 0 5 0
滚动中 3 2 5 2(触及上限,暂停)
滚动中 2 3 6(触及 maxSurge=1 上限) 2
完成后 0 5 5 0

删除 1 个旧 Pod

创建 1 个新 Pod

等待新 Pod Ready

删除 1 个旧 Pod

循环直到旧 RS=0

开始

阶段1
旧 RS:5 新 RS:0
总 Pod=5 不可用=0

阶段2
旧 RS:4 新 RS:1
总 Pod=5 不可用=1

阶段3
旧 RS:4 新 RS:2
总 Pod=6 (触及maxSurge)
不可用=1

阶段4
新 Pod Ready

阶段5
旧 RS:3 新 RS:2
总 Pod=5
不可用=2(触及maxUnavailable)

阶段6
旧 RS:0 新 RS:5

完成

两种极端配置的业务含义

  • maxUnavailable=0, maxSurge=1:滚动期间总 Pod 数最多为 N+1,适合对可用性要求极高的服务(如在线交易系统)
  • maxUnavailable=1, maxSurge=0:始终保持 N 个 Pod,但会短暂出现不可用窗口,适合对资源敏感的场景(如开发测试环境)

三、kubectl rollout undo 内部机制

3.1 执行流程分解

kubectl rollout undo deployment/nginx

这行命令背后发生了四件事:

kubectl rollout undo

查找上一版本 RS
annotation: deployment.kubernetes.io/revision=N-1

将新 RS 的 revision 标记为 N+1
(不是 N!每次操作都产生新 revision)

将 Deployment.spec.template
替换为旧 RS 的 spec.template

Deployment Controller 创建
以新 template 为基准的 RS

滚动更新启动
逐步用新 RS 替换旧 RS

一个容易忽略的细节:回滚操作本身也会生成一个新的 revision。这意味着 kubectl rollout history 中会看到回滚操作作为一条新记录,而不是覆盖原来的 revision。

# 查看 revision 历史
kubectl rollout history deployment/nginx

# 输出示例:
# deployment/nginx
# REVISION  CHANGE-CAUSE
# 3         kubectl apply --filename=deployment-v3.yaml
# 2         kubectl apply --filename=deployment-v2.yaml
# 1         kubectl apply --filename=deployment-v1.yaml
# 4         kubectl rollout undo --to-revision=2  ← 回滚操作产生的 revision

3.2 回滚到指定版本

# 回滚到 revision=2
kubectl rollout undo deployment/nginx --to-revision=2

不指定 --to-revision 时,默认回滚到上一个版本(即 revision 列表中的 revision-1)。

3.3 回滚失败的场景

# 查看回滚状态
kubectl rollout status deployment/nginx

常见回滚失败原因:

场景 表现 根因
revision 历史被清空 error: no rollout history is available revisionHistoryLimit=0
指定版本不存在 error: unable to find specified revision revision 编号不在保留范围内
Pod 模板语法错误 滚动更新卡在 Progressing 上一版本的 YAML 本身有缺陷

四、金丝雀发布的原生实现

4.1 为什么不用 Argo Rollouts

Argo Rollouts 是功能完整的金丝雀控制器,但引入门槛较高。原生 K8s 方式不需要任何额外 CRD,通过两个 Deployment + Service 标签选择器即可实现基础的灰度发布。

4.2 实现方式

第一步:部署稳定版本(v1)

# deployment-v1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-stable
spec:
  replicas: 10
  selector:
    matchLabels:
      app: nginx
      version: v1
  template:
    metadata:
      labels:
        app: nginx
        version: v1
    spec:
      containers:
      - name: nginx
        image: nginx:1.24
        ports:
        - containerPort: 80

第二步:Service 指向 v1

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  selector:
    app: nginx      # 匹配所有版本
    version: v1     # 初始时只指向 v1
  ports:
  - port: 80
    targetPort: 80

第三步:部署金丝雀版本(v2)

# deployment-v2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-canary
spec:
  replicas: 2      # 小比例:10个v1 + 2个v2 = 约17%流量到v2
  selector:
    matchLabels:
      app: nginx
      version: v2
  template:
    metadata:
      labels:
        app: nginx
        version: v2
    spec:
      containers:
      - name: nginx
        image: nginx:1.25   # 新版本
        ports:
        - containerPort: 80

第四步:验证金丝雀 Pod 正常工作后,逐步调整 Service 标签切换流量

# 初期:Service 只指向 v1
kubectl patch service nginx -p '{"spec":{"selector":{"version":"v1"}}}'

# 金丝雀验证通过后:将 20% 流量切到 v2
kubectl patch service nginx -p '{"spec":{"selector":{"version":"v2"}}}'
# 此时 nginx Service 的 selector 变为 version=v2
# v1 的 10 个 Pod 仍在运行,但不再接收 Service 流量

# 最终:完全切换到 v2
kubectl scale deployment nginx-stable --replicas=0

完成:v2 全量

Service
selector: version=v2

Pod v2 x10

金丝雀:20% 到 v2

Service
selector: version=v2

Pod v1 x10
(不接收流量)

Pod v2 x2

初期:v1 全量

Service
selector: version=v1

Pod v1 x10

Pod v2 x2
(不接收流量)

4.3 Ingress-Nginx 原生金丝雀注解

如果集群使用 Ingress-Nginx Controller,可以利用注解实现基于权重的流量分配,无需修改 Service:

# 主版本 Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-main
  annotations:
    kubernetes.io/ingress.class: nginx
spec:
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: nginx-stable
            port:
              number: 80

---
# 金丝雀 Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-canary
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "20"   # 20% 流量到金丝雀
spec:
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: nginx-canary
            port:
              number: 80

启用金丝雀注解后,canary-weight 注解按指定百分比将请求转发到金丝雀 Service,其余流量走主版本 Ingress。权重为 0 时所有流量走主版本,权重为 100 时所有流量走金丝雀。


五、滚动更新与 HPA 的并发冲突

这是生产环境里最容易被忽视的问题之一。

5.1 冲突场景复现

假设配置如下:

# Deployment
spec:
  replicas: 5
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

---
# HPA
spec:
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 80

滚动更新进行中时

  • Deployment 正在创建新 Pod,Pod 总数暂时达到 5 + 1 = 6
  • HPA 采样到 CPU 使用率升高,触发扩容到 8 个副本
  • Deployment Controller 感知到期望副本数从 5 变为 8,立即开始扩容新 ReplicaSet
  • 扩容过程中 Pod 总数达到 8 + 1 = 9
API Server HPA Controller Deployment Controller API Server HPA Controller Deployment Controller replicas=5, RollingUpdate 进行中 当前 Pod=6 (触及maxSurge) 当前 Pod=9 (8+1),HPA+maxSurge叠加 检测到 CPU 80%,触发扩容 更新 Deployment replicas=8 检测到 replicas 变更 创建新 RS,目标 Pod=8 达到 maxSurge,暂停滚动 新 Pod Ready,maxSurge 窗口打开 开始滚动更新旧 RS

结果:HPA 扩容 + maxSurge 叠加,Pod 数量短暂超过 maxReplicas + maxSurge,对集群资源造成额外压力。

5.2 解决方案

方案一:禁用 HPA 期间的滚动更新

# 在滚动更新前暂停 HPA 扩容
kubectl autoscale deployment nginx --min=5 --max=5 --cpu-percent=80
# 等价于固定 replicas=5,HPA 不再干预

# 滚动更新完成后再恢复 HPA
kubectl delete hpa nginx
kubectl autoscale deployment nginx --min=3 --max=10 --cpu-percent=80

方案二:使用 rollout pause / rollout resume

# 触发滚动更新后立即暂停
kubectl rollout pause deployment/nginx

# 执行 HPA 相关操作
kubectl autoscale deployment nginx --min=3 --max=10

# HPA 稳定后恢复滚动更新
kubectl rollout resume deployment/nginx

方案三:设置合理的 maxSurge

如果集群资源紧张,将 maxSurge 设为 01,避免因 HPA 扩容叠加产生过多额外 Pod:

spec:
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1   # 允许同时有 1 个不可用

六、生产环境配置决策树

需要滚动更新吗?

对可用性要求极高
(在线业务)

maxUnavailable=0
maxSurge=1

maxUnavailable=1
maxSurge=0

需要回滚能力?

revisionHistoryLimit≥3

revisionHistoryLimit=0

是否启用 HPA?

滚动更新期间
使用 pause/resume

正常滚动更新

是否需要灰度发布?

双 Deployment +
Service 标签切换

单 Deployment
原生滚动更新


七、验证命令清单

# 1. 查看滚动更新状态
kubectl rollout status deployment/nginx -w

# 2. 查看当前 revision 历史
kubectl rollout history deployment/nginx

# 3. 查看具体 revision 的详情
kubectl rollout history deployment/nginx --revision=2

# 4. 回滚到上一版本
kubectl rollout undo deployment/nginx

# 5. 回滚到指定版本
kubectl rollout undo deployment/nginx --to-revision=1

# 6. 查看新旧 ReplicaSet 分布
kubectl get rs -l app=nginx

# 7. 暂停滚动更新(常配合 HPA 使用)
kubectl rollout pause deployment/nginx

# 8. 恢复滚动更新
kubectl rollout resume deployment/nginx

# 9. 查看 Deployment 滚动策略配置
kubectl get deployment nginx -o jsonpath='{.spec.strategy}'

# 10. 强制重启(重新触发滚动更新)
kubectl rollout restart deployment/nginx

八、总结

Deployment 的滚动更新不是"新版本替换旧版本"这么简单。背后的 ReplicaSet 版本快照机制、maxSurgemaxUnavailable 的精确约束、HPA 并发时的资源竞争问题——每个环节都有明确的内部逻辑。

理解这些逻辑,才能在滚动更新卡住时知道查什么、在回滚失败时知道根因在哪、在设计灰度策略时选择合适的实现方式。

关键结论

  • kubectl rollout undo 本质是恢复旧 ReplicaSet 的 Pod 模板快照,每次回滚都生成新 revision
  • maxSurge=1, maxUnavailable=0 适合高可用要求,maxSurge=0, maxUnavailable=1 适合资源受限场景
  • 原生金丝雀发布用两个 Deployment + Service 标签切换即可实现,无需引入额外 CRD
  • HPA 与滚动更新并发时,使用 rollout pause/resume 或固定 replicas 规避资源叠加

觉得这篇文章有收获的话,欢迎点赞、关注。技术创作不易,每一份支持都是坚持下去的动力。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐