课程目录(第 6 章 / 共 33 章)
Deployment:副本、滚动更新与回滚
用 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 会再替你生成两层资源:
Deployment(要几个副本、用什么镜像、怎么发布)
│ 创建并管理
▼
ReplicaSet(保证当前版本的 Pod 数量)
│ 创建并管理
▼
Pod(真正跑着容器的最小单元)三个关键点:
- Deployment 不直接管 Pod,它管 ReplicaSet,ReplicaSet 才管 Pod。所以出问题时两层都要看。
- 每次你改动 Pod 模板(比如换镜像),Deployment 会新建一个 ReplicaSet,把旧 ReplicaSet 的副本逐步降到 0、新的逐步升到目标值——这就是滚动更新的真实过程。
- 旧的 ReplicaSet 不会立刻删除,会保留一段时间(默认保留 10 个历史版本)。这正是回滚能瞬间生效的原因。
把「改一次镜像」的时间线摊开看,滚动更新的每一步就清楚了:
你改了 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 一直不就绪,更新就停在第 ② 步不动——这是保护,不是卡死。
一条命令就能看到这层结构:
kubectl get deploy,rs,pods -n demoReplicaSet 的名字形如 web-7d9c8b6f5c,后半段是 Pod 模板的哈希值。模板一变哈希就变,所以新版本必然落在新的 ReplicaSet 里。
写第一份 Deployment 清单
本课程统一使用 demo 命名空间,先建好:
kubectl create namespace demo把下面这份清单保存为 web-deployment.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.matchLabels | Deployment 用哪些标签认领自己该管的 Pod |
| template.metadata.labels | 新创建出来的 Pod 会带上的标签 |
| template.spec | Pod 模板,写法与单独写 Pod 完全一样 |
应用并确认:
kubectl apply -f web-deployment.yaml
kubectl get deploy,rs,pods -n demo
kubectl rollout status deployment/web -n demorollout status 输出 deployment "web" successfully rolled out,说明两个副本都就绪了。再用标签筛一次,看看 Pod 实际带的标签:
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 阶段直接拒绝:
The Deployment "web" is invalid: spec.template.metadata.labels:
Invalid value: map[string]string{"app":"web-v2"}: `selector` does not match template `labels`记住三条规则:
selector.matchLabels里的每个键值对,都必须在template.metadata.labels里出现(反过来可以多,Pod 允许带额外标签)。- 匹配的是标签,不是 Pod 名字,也不是镜像名。
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 的状态;常见原因是镜像拉取失败或节点资源不足。- 把
matchLabels和matchExpressions混着写错 → 入门阶段只用matchLabels,它是「标签必须全部相等」的最简形式。
滚动更新:maxSurge 与 maxUnavailable
默认策略是 RollingUpdate,用两个参数控制更新节奏:
| 参数 | 含义 | 默认值 |
|---|---|---|
| maxSurge | 更新过程中最多可以超出 replicas 多少个 Pod | 25% |
| maxUnavailable | 更新过程中最多允许多少个 Pod 不可用 | 25% |
两个值都可以写整数或百分比。想让「先起新的、再停旧的」这种不降容量的发布生效,就把 maxUnavailable 设为 0,也就是在 web-deployment.yaml 的 spec 里补上这几行:
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0此时 2 副本的 Deployment 在更新期间最多存在 3 个 Pod(2 个可用 + 1 个新起的)。反过来设成 maxSurge: 0、maxUnavailable: 1,就是先停一个旧的再起一个新的,占用资源更少,但可用副本会短暂下降。
用 kubectl patch 快速加上这个策略(也可以直接写进 YAML 再 apply):
kubectl patch deployment web -n demo --type=strategic \
-p '{"spec":{"strategy":{"type":"RollingUpdate","rollingUpdate":{"maxSurge":1,"maxUnavailable":0}}}}'触发更新、观察滚动过程
把镜像换成 nginx:1.27-alpine(同样内容、体积更小):
kubectl set image deployment/web web=nginx:1.27-alpine -n demo命令里两个 web 的含义不同:前一个是 Deployment 名,后一个是容器名(即 spec.template.spec.containers[].name)。
开一个终端盯着 Pod 变化(Ctrl+C 退出):
kubectl get pods -n demo -w你会看到新 ReplicaSet 的 Pod 逐个 ContainerCreating → Running,旧的逐个 Terminating。同时看进度和 ReplicaSet 分布:
kubectl rollout status deployment/web -n demo
kubectl get rs -n demokubectl get rs 里应该出现两个 ReplicaSet:新的 DESIRED 2、旧的 DESIRED 0,说明更新完成。想事后看清每个版本是什么,先加一条变更说明,再查历史:
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 才会显示那一次的具体信息。
回滚、重启与扩缩容
更新出问题时,回滚到上一版只要一条命令:
kubectl rollout undo deployment/web -n demo
kubectl rollout status deployment/web -n demoundo 的本质是把 Deployment 的 Pod 模板重新指向目标版本,让控制器把对应的 ReplicaSet 副本重新升上来——所以回滚本身也是一次滚动更新,会完整走一遍上面的 ①→⑤。要回到更早的版本,先看历史再指定:
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 重建(例如重新读取环境变量)用:
kubectl rollout restart deployment/web -n demo手动调整副本数:
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 image、kubectl scale、kubectl rollout restart 改的都是集群里的对象,你本地的 YAML 文件并没有变。下次 kubectl apply -f web-deployment.yaml 会把副本数改回 2、镜像改回文件里的值。正确做法是改 YAML 再 apply,命令只用于临时实验。
动手练习:完成一次完整的发布与回滚
- 用本章清单创建
web,确认kubectl get pods -n demo里两个 Pod 都是READY 2/2。 - 执行
kubectl set image deployment/web web=nginx:1.27-alpine -n demo,用kubectl get pods -n demo -w观察滚动过程。 - 执行
kubectl rollout undo deployment/web -n demo,再用kubectl get rs -n demo确认旧 ReplicaSet 的DESIRED回到 2。 - 最后把本地 YAML 里的镜像改回
nginx:1.27并kubectl apply -f web-deployment.yaml,让文件与集群保持一致。
常见坑与排错
Deployment 出问题时,排查顺序是固定的:先看 rollout 进度,再看 ReplicaSet,最后看 Pod。不要一上来就翻日志。
kubectl rollout status deployment/web -n demo—— 卡在哪一步,输出会明说是「几个新副本已更新」。kubectl get rs -n demo—— 新旧 ReplicaSet 的DESIRED/READY/CURRENT是否对得上。kubectl get pods -n demo --show-labels—— Pod 到底处于什么状态(Pending、ImagePullBackOff、CrashLoopBackOff、Running但READY 0/1)。kubectl describe pod -n demo -l app=web—— 看 Events,这是第一手报错来源。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 一直 Running 但 READY 0/1;节点资源不足导致 Pending。只要新 Pod 不 Ready,滚动更新就会主动停下来保护服务,这是特性不是故障。
apply 之后 Pod 数不对
- 期望 2 个却起了 4 个:可能有另一个 Deployment 或 HPA 也在管同一批标签的 Pod。用
kubectl get deploy -n demo看清楚有几个 Deployment。 - 期望 2 个只有 1 个:Pod 可能卡在
Pending(资源不足)或CrashLoopBackOff(容器启动即退出)。 kubectl apply报field 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 Running 但 READY 0/1 | 就绪探针失败,Pod 不进 EndpointSlice | kubectl 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 undo 报 no last revision to roll back to | 没有可回滚的历史版本 | kubectl rollout history deployment/web -n demo | 用 --to-revision 指定存在的编号,或先发布一次 |
apply 报 selector does not match template labels | selector 与 template 两处标签不一致 | 对比清单里两个 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。
练习
- 把
replicas改成 5,再改回 2,观察kubectl get rs -n demo里DESIRED与CURRENT两列的变化。 - 故意把
template.metadata.labels的app改成web2再 apply,把报错信息抄下来,然后改回去。 - 加上
maxSurge: 1、maxUnavailable: 0的策略,用kubectl get pods -n demo -w对比默认策略下更新期间 Pod 数量的差异。
Pod 有了、副本也有了,但外部用户仍然进不来:Pod 的 IP 每次重建都会变,副本数量也会增减。下一章我们用 第 7 章 Service 给这组 Pod 一个固定入口,并顺带解决集群内的服务发现。
相关章节:Pod 清单的写法见第 5 章 用 YAML 描述 Pod,探针与资源限制的细节见第 11 章 探针与资源管理。