课程目录(第 6 章 / 共 33 章)
课程/最小工作单元

Deployment:副本、滚动更新与回滚

6 章 / 共 33·18 分钟·入门Deploymentkubectl滚动更新

用 Deployment 管理多个 Pod 副本,搞清 selector 与标签的匹配规则,亲手完成一次滚动更新与回滚。

学完这一章,你将能够

  • 说清 Deployment、ReplicaSet、Pod 三层之间的关系
  • 写出一份能跑起来的 Deployment 清单,并保证 selector 与 template 标签一致
  • 用 kubectl rollout 完成一次更新、观察进度并回滚到指定版本

一个 Pod 不够用

前面你已经能亲手创建一个 Pod 了。但单靠一个裸 Pod 有三个现实问题:

  • Pod 挂了不会自动重建,得你手动再创建一个;
  • 只有一个副本,流量一大就撑不住;
  • 要换镜像版本时,只能删掉重建,中间服务是中断的。

Deployment 就是来解决这三件事的:你告诉它「我要 2 个跑着 nginx:1.27 的 Pod,一直保持 2 个」,剩下的重建、替换、保数量它自己负责。

Deployment、ReplicaSet、Pod 的三层关系

写 Deployment 时你只写了一份 Pod 模板,Kubernetes 会再替你生成两层资源:

text
Deployment(要几个副本、用什么镜像、怎么发布)
   │  创建并管理

ReplicaSet(保证当前版本的 Pod 数量)
   │  创建并管理

Pod(真正跑着容器的最小单元)

三个关键点:

  • Deployment 不直接管 Pod,它管 ReplicaSet,ReplicaSet 才管 Pod。所以出问题时两层都要看。
  • 每次你改动 Pod 模板(比如换镜像),Deployment 会新建一个 ReplicaSet,把旧 ReplicaSet 的副本逐步降到 0、新的逐步升到目标值——这就是滚动更新的真实过程。
  • 旧的 ReplicaSet 不会立刻删除,会保留一段时间(默认保留 10 个历史版本)。这正是回滚能瞬间生效的原因。

把「改一次镜像」的时间线摊开看,滚动更新的每一步就清楚了:

text
你改了 template 里的镜像

   ├─► Deployment 新建 ReplicaSet web-6c9f...(新模板),旧 RS web-7d9c... 仍有 2 个副本

   ├─ ① 先起 1 个新 Pod(maxSurge: 1
   ├─ ② 新 Pod 通过就绪探针,加入 EndpointSlice
   ├─ ③ 旧 RS 的 DESIRED 从 2 降到 1 ──► 一个旧 Pod Terminating
   ├─ ④ 再起第 2 个新 Pod ────────────► 旧 RS 的 DESIRED 降到 0
   └─ ⑤ rollout status 输出 successfully rolled out

关键在 ② 和 ③ 的顺序:新 Pod 先就绪、旧 Pod 才下线。只要新 Pod 一直不就绪,更新就停在第 ② 步不动——这是保护,不是卡死。

一条命令就能看到这层结构:

bash
kubectl get deploy,rs,pods -n demo

ReplicaSet 的名字形如 web-7d9c8b6f5c,后半段是 Pod 模板的哈希值。模板一变哈希就变,所以新版本必然落在新的 ReplicaSet 里。

写第一份 Deployment 清单

本课程统一使用 demo 命名空间,先建好:

bash
kubectl create namespace demo

把下面这份清单保存为 web-deployment.yaml

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: demo
  labels:
    app: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports:
            - containerPort: 80

字段逐个解释:

字段作用
replicas期望的 Pod 数量,控制器会一直维持这个数字
selector.matchLabelsDeployment 用哪些标签认领自己该管的 Pod
template.metadata.labels新创建出来的 Pod 会带上的标签
template.specPod 模板,写法与单独写 Pod 完全一样

应用并确认:

bash
kubectl apply -f web-deployment.yaml
kubectl get deploy,rs,pods -n demo
kubectl rollout status deployment/web -n demo

rollout status 输出 deployment "web" successfully rolled out,说明两个副本都就绪了。再用标签筛一次,看看 Pod 实际带的标签:

bash
kubectl get pods -n demo -l app=web --show-labels

最容易错的一点:selector 与 template 标签必须匹配

这是新手最常撞的墙,值得单独讲:`spec.selector.matchLabels` 必须能匹配上 `spec.template.metadata.labels`

Deployment 的逻辑是「我认领所有带 app: web 标签的 Pod」,而它自己创建出来的 Pod 也必须带 app: web。两处不一致时,它创建出来的 Pod 自己都不认,数量就永远对不上,所以 Kubernetes 在 apply 阶段直接拒绝:

text
The Deployment "web" is invalid: spec.template.metadata.labels:
Invalid value: map[string]string{"app":"web-v2"}: `selector` does not match template `labels`

记住三条规则:

  1. selector.matchLabels 里的每个键值对,都必须在 template.metadata.labels 里出现(反过来可以多,Pod 允许带额外标签)。
  2. 匹配的是标签,不是 Pod 名字,也不是镜像名。
  3. selector 在 Deployment 创建后不可修改,改了会报 field is immutable;要换 selector,只能删掉重建,或新建一个 Deployment。

三个高频报错

  • 两处标签不一致 → kubectl apply 直接报 selector does not match template labels,Deployment 根本不会创建。
  • replicas: 2 却只看到 1 个 Pod → 先看 kubectl describe deployment web -n demo 的 Events,再看 kubectl get pods -n demo 里 Pod 的状态;常见原因是镜像拉取失败或节点资源不足。
  • matchLabelsmatchExpressions 混着写错 → 入门阶段只用 matchLabels,它是「标签必须全部相等」的最简形式。

滚动更新:maxSurge 与 maxUnavailable

默认策略是 RollingUpdate,用两个参数控制更新节奏:

参数含义默认值
maxSurge更新过程中最多可以超出 replicas 多少个 Pod25%
maxUnavailable更新过程中最多允许多少个 Pod 不可用25%

两个值都可以写整数或百分比。想让「先起新的、再停旧的」这种不降容量的发布生效,就把 maxUnavailable 设为 0,也就是在 web-deployment.yamlspec 里补上这几行:

yaml
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

此时 2 副本的 Deployment 在更新期间最多存在 3 个 Pod(2 个可用 + 1 个新起的)。反过来设成 maxSurge: 0maxUnavailable: 1,就是先停一个旧的再起一个新的,占用资源更少,但可用副本会短暂下降。

kubectl patch 快速加上这个策略(也可以直接写进 YAML 再 apply):

bash
kubectl patch deployment web -n demo --type=strategic \
  -p '{"spec":{"strategy":{"type":"RollingUpdate","rollingUpdate":{"maxSurge":1,"maxUnavailable":0}}}}'

触发更新、观察滚动过程

把镜像换成 nginx:1.27-alpine(同样内容、体积更小):

bash
kubectl set image deployment/web web=nginx:1.27-alpine -n demo

命令里两个 web 的含义不同:前一个是 Deployment 名,后一个是容器名(即 spec.template.spec.containers[].name)。

开一个终端盯着 Pod 变化(Ctrl+C 退出):

bash
kubectl get pods -n demo -w

你会看到新 ReplicaSet 的 Pod 逐个 ContainerCreating → Running,旧的逐个 Terminating。同时看进度和 ReplicaSet 分布:

bash
kubectl rollout status deployment/web -n demo
kubectl get rs -n demo

kubectl get rs 里应该出现两个 ReplicaSet:新的 DESIRED 2、旧的 DESIRED 0,说明更新完成。想事后看清每个版本是什么,先加一条变更说明,再查历史:

bash
kubectl annotate deployment/web -n demo \
  kubernetes.io/change-cause="image nginx:1.27 -> nginx:1.27-alpine" --overwrite
kubectl rollout history deployment/web -n demo
kubectl rollout history deployment/web -n demo --revision=2

注意 rollout history 默认只列出 revision 编号,加 --revision=2 才会显示那一次的具体信息。

回滚、重启与扩缩容

更新出问题时,回滚到上一版只要一条命令:

bash
kubectl rollout undo deployment/web -n demo
kubectl rollout status deployment/web -n demo

undo 的本质是把 Deployment 的 Pod 模板重新指向目标版本,让控制器把对应的 ReplicaSet 副本重新升上来——所以回滚本身也是一次滚动更新,会完整走一遍上面的 ①→⑤。要回到更早的版本,先看历史再指定:

bash
kubectl rollout history deployment/web -n demo
kubectl rollout undo deployment/web --to-revision=1 -n demo

两条回滚命令的差别不在「能不能用」,而在你要回到哪个版本

命令适用场景说明
kubectl rollout undo deployment/web -n demo刚发的那一版有问题,马上退回上一版等价于「当前 revision - 1」,不用记编号
kubectl rollout undo deployment/web --to-revision=1 -n demo连着发了几版都不行,要退到某个确定的版本编号来自 rollout history,先看清楚再指定

--revision--to-revision 分属两个命令,别写混:kubectl rollout history ... --revision=2 是「看第 2 版的详情」,kubectl rollout undo ... --to-revision=2 才是「回到第 2 版」。互换会直接报 unknown flag

历史版本不是无限的:spec.revisionHistoryLimit 默认 10,超出的旧 ReplicaSet 会被控制器清理,那时就再也回不去了。另外,如果你把镜像手改回与某个历史版本完全一致,Deployment 会直接复用那个旧 ReplicaSet,看起来像「什么都没做」——这是模板哈希匹配的结果,不是命令失效。

不换镜像、只想让 Pod 重建(例如重新读取环境变量)用:

bash
kubectl rollout restart deployment/web -n demo

手动调整副本数:

bash
kubectl scale deployment/web --replicas=4 -n demo
kubectl get pods -n demo -l app=web
kubectl scale deployment/web --replicas=2 -n demo

命令只是应急,YAML 才是事实

kubectl set imagekubectl scalekubectl rollout restart 改的都是集群里的对象,你本地的 YAML 文件并没有变。下次 kubectl apply -f web-deployment.yaml 会把副本数改回 2、镜像改回文件里的值。正确做法是改 YAML 再 apply,命令只用于临时实验。

动手练习:完成一次完整的发布与回滚

  1. 用本章清单创建 web,确认 kubectl get pods -n demo 里两个 Pod 都是 READY 2/2
  2. 执行 kubectl set image deployment/web web=nginx:1.27-alpine -n demo,用 kubectl get pods -n demo -w 观察滚动过程。
  3. 执行 kubectl rollout undo deployment/web -n demo,再用 kubectl get rs -n demo 确认旧 ReplicaSet 的 DESIRED 回到 2。
  4. 最后把本地 YAML 里的镜像改回 nginx:1.27kubectl apply -f web-deployment.yaml,让文件与集群保持一致。

常见坑与排错

Deployment 出问题时,排查顺序是固定的:先看 rollout 进度,再看 ReplicaSet,最后看 Pod。不要一上来就翻日志。

  1. kubectl rollout status deployment/web -n demo —— 卡在哪一步,输出会明说是「几个新副本已更新」。
  2. kubectl get rs -n demo —— 新旧 ReplicaSet 的 DESIRED / READY / CURRENT 是否对得上。
  3. kubectl get pods -n demo --show-labels —— Pod 到底处于什么状态(PendingImagePullBackOffCrashLoopBackOffRunningREADY 0/1)。
  4. kubectl describe pod -n demo -l app=web —— 看 Events,这是第一手报错来源。
  5. kubectl logs -n demo -l app=web --tail=50 —— 只有「容器起来了但进程退出」才需要看日志。

更新卡住不动

rollout status 长时间停在 Waiting for deployment "web" rollout to finish: 1 out of 2 new replicas have been updated,说明新 Pod 起不来。最常见的原因:镜像 tag 写错导致 ImagePullBackOff;就绪探针(readinessProbe)路径或端口配错,Pod 一直 RunningREADY 0/1;节点资源不足导致 Pending。只要新 Pod 不 Ready,滚动更新就会主动停下来保护服务,这是特性不是故障。

apply 之后 Pod 数不对

  • 期望 2 个却起了 4 个:可能有另一个 Deployment 或 HPA 也在管同一批标签的 Pod。用 kubectl get deploy -n demo 看清楚有几个 Deployment。
  • 期望 2 个只有 1 个:Pod 可能卡在 Pending(资源不足)或 CrashLoopBackOff(容器启动即退出)。
  • kubectl applyfield is immutable:你动了 selector,只能删掉 Deployment 重建。
现象原因怎么确认怎么办
rollout status 一直 Waiting for ...新 Pod 没有就绪kubectl get pods -n demo 看新 Pod 的状态按上面的五步顺序往下查
Pod 卡在 Pending节点资源不足,或有污点 / 亲和性限制kubectl describe pod 里找 FailedScheduling调小 requests,或加节点
Pod ImagePullBackOff镜像名或 tag 不存在、私有仓库没凭证kubectl describe pod 看 Events换成存在的镜像 tag,必要时补 imagePullSecrets
Pod RunningREADY 0/1就绪探针失败,Pod 不进 EndpointSlicekubectl describe pod 看 probe 相关 Events修探针的路径、端口与 initialDelaySeconds
Pod 反复重启 CrashLoopBackOff容器里的进程启动即退出kubectl logs -n demo -l app=web --previous 看上一次的日志先修启动命令与依赖
Pod 数比 replicas另一个 Deployment 或 HPA 也在管同一批标签kubectl get deploy,hpa -n demo统一标签,或把 selector 改得更专一
rollout undono last revision to roll back to没有可回滚的历史版本kubectl rollout history deployment/web -n demo--to-revision 指定存在的编号,或先发布一次
apply 报 selector does not match template labelsselectortemplate 两处标签不一致对比清单里两个 app: 的值改成一致
apply 报 field is immutable改了 spec.selector报错里会点名 spec.selector删掉 Deployment 重建,或新建一个
自测:为什么 Deployment 的 selector 不能改?(点击展开答案)

因为 selector 是 Deployment「认领」Pod 的依据,而它自己创建出来的 Pod 又必须带 template 里的标签,两者是同一个契约的两端。如果允许改,改完之后老 Pod 不再匹配新的 selector,Deployment 会认为自己一个副本都没有,于是按新规则再创建一批新 Pod;老 Pod 则变成没人管的孤儿——继续占着资源,却收不到任何流量。所以 Kubernetes 干脆把 selector 设成创建后不可变,要换标签体系就删掉重建。

自测:只改了一个镜像,为什么会多出一个 ReplicaSet?(点击展开答案)

因为 ReplicaSet 是「某一份 Pod 模板」的实例。Pod 模板里任何字段(镜像、env、command、resources)一变,模板哈希就变,Deployment 就会为这份新模板创建新的 ReplicaSet,把旧的副本逐步降到 0 但保留下来。收益是回滚几乎零成本——把旧 ReplicaSet 的副本升回去就行;代价是每次发布都多一个 ReplicaSet,所以要有 revisionHistoryLimit 兜底。反过来,改 replicas 不会产生新 ReplicaSet,因为副本数不属于 Pod 模板。

自测:更新期间旧 Pod 为什么不会立刻被删掉?(点击展开答案)

因为滚动更新的目标就是「不中断服务」。控制器必须等新 Pod 通过就绪探针、进入 EndpointSlice 之后,才允许下线旧 Pod;把 maxUnavailable 设为 0 时,任何时刻可用副本都不会低于 replicas。旧 ReplicaSet 的副本数是一步一步降到 0 的。所以「更新卡住」往往不是控制器坏了,而是新 Pod 一直没 Ready,控制器在等你修好——这是保护机制,不是故障。

小结

  • Deployment 管 ReplicaSet,ReplicaSet 管 Pod;每次改动 Pod 模板都会生成新的 ReplicaSet。
  • selector.matchLabels 必须匹配 template.metadata.labels,且 selector 创建后不可修改。
  • 滚动更新的节奏由 maxSurge / maxUnavailable 控制,默认会同时保留新旧副本。
  • rollout status 看进度、rollout history 看版本、rollout undo 回滚、rollout restart 触发重建。
  • 命令只用于应急,长期正确的做法是改 YAML 再 kubectl apply

练习

  1. replicas 改成 5,再改回 2,观察 kubectl get rs -n demoDESIREDCURRENT 两列的变化。
  2. 故意把 template.metadata.labelsapp 改成 web2 再 apply,把报错信息抄下来,然后改回去。
  3. 加上 maxSurge: 1maxUnavailable: 0 的策略,用 kubectl get pods -n demo -w 对比默认策略下更新期间 Pod 数量的差异。

Pod 有了、副本也有了,但外部用户仍然进不来:Pod 的 IP 每次重建都会变,副本数量也会增减。下一章我们用 第 7 章 Service 给这组 Pod 一个固定入口,并顺带解决集群内的服务发现。

相关章节:Pod 清单的写法见第 5 章 用 YAML 描述 Pod,探针与资源限制的细节见第 11 章 探针与资源管理