---
url: >-
  /courses/cloud-platform-build-management/16-kubernetes-release-rollback/index.md
---
# 第十六次课：Kubernetes 发布、故障与回滚

## 进入本课环境

* `[Windows PowerShell] wsl ~`：打开本机默认的 WSL2 Ubuntu；本课继续使用本地隔离集群，不需要 SSH 登录 ECS，运行 `kubectl` 前先核对当前集群上下文。

第 15 次课把一个 Deployment 扩到三个副本。现在要回答更接近生产运维的问题：页面升级时，怎样逐步替换旧 Pod；新版本拉不起镜像时，怎样保住正在服务的旧版本；确认故障后，怎样回到最后一个已经验证的修订。

::: tip 关键提醒
提交新清单不等于发布完成。完整结论至少需要修订可追踪、滚动过程结束、副本就绪、Service 路径返回目标版本；失败时还要保留故障证据并完成回滚回归。
:::

学完这一课，做到五件事：

1. 解释 Deployment、ReplicaSet 和修订历史的关系。
2. 读懂 `maxUnavailable`、`maxSurge` 与 readiness 对滚动发布的影响。
3. 发布 v1 和 v2，并用 `rollout status`、`history` 和实际请求验收。
4. 根据 ImagePullBackOff、事件和旧版本可用状态判断故障范围。
5. 选择明确修订执行回滚，并证明镜像、标签、副本和页面均已恢复。

## 16.1 先保留可回退的基线

先发布并验证 v1，再滚动到 v2。每次变更都要留下修订说明、发布状态、副本状态和页面版本，后续才能判断应该回到哪一个修订。

错误镜像案例只作用于本课命名空间。发现发布超时后，先读取 Pod 事件和旧副本状态，再决定是否回滚；不要靠删除 ReplicaSet 清空历史。

## 16.2 在独立隔离集群中开启发布

第 15 次课使用的测试集群已完成清理。本课统一使用全新的 `xpk-lesson16` 集群，独立命名空间为 `cloud-course-16`。前后两次课程互不干扰，不共享 Pod、Service 或历史版本。

先创建隔离集群并将清单文件部署到工作目录：

```bash
[WSL] k3d cluster create xpk-lesson16 --servers 1 --agents 0
[WSL] mkdir -p ~/cloud-course/lab-16
[WSL] cp -r /path/to/lab-16-starter/* ~/cloud-course/lab-16/
[WSL] cd ~/cloud-course/lab-16
```

实训包清单文件在 `manifests/` 子目录中（从智慧职教课程资源区下载 `lab-16-starter.zip` 解压）。

```bash
[WSL] kubectl config current-context
```

核对当前的集群上下文。如果连接的目标不对，先停止操作，确认指向无误后再继续。

```bash
[WSL] kubectl get nodes
```

确认 Node 处于 `Ready` 状态。节点就绪是基础支撑，应用发布是否成功还得继续检查。

```bash
[WSL] kubectl get namespace cloud-course-16
```

首次运行前预期返回 `NotFound`，这正好说明当前环境干净，没有任何历史残留。

::: warning 安全边界
本课仅限定在 `cloud-course-16` 命名空间内部操作。切勿改动 `kube-system`，严禁随意删除其他命名空间，也不要把异常镜像测试带入公共环境。
:::

## 16.3 Deployment 更新时，ReplicaSet 保存什么

Deployment 并不直接逐个创建或销毁 Pod，而是通过管理不同的 ReplicaSet（副本集——负责维护指定数量 Pod 副本的控制器）来控制版本。一旦 Pod 模板发生变更，Deployment 就会拉起一个新的 ReplicaSet：

```txt
Deployment lesson16-web
├── ReplicaSet revision 1 → v1 Pod 模板
├── ReplicaSet revision 2 → v2 Pod 模板
└── ReplicaSet revision 3 → 错误镜像 Pod 模板
```

集群里的修订历史并不是完整的代码仓库，它记录的是 Deployment 在不同阶段的 Pod 模板快照，最大保留数量由 `revisionHistoryLimit` 决定。本课设为 5，足够保留演练所需的历史节点。

```yaml
metadata:
  annotations:
    kubernetes.io/change-cause: "lesson16 release v2"
```

`change-cause` 负责给历史版本打上人类可读的备注。发布时应当随清单一同更新，方便日后准确溯源。

```bash
[WSL] kubectl rollout history deployment/lesson16-web -n cloud-course-16
```

查看各版本 Revision 编号与对应的 CHANGE-CAUSE。这能帮我们精准指定要回滚的目标版本。

{{guided-demo:lesson-16-release-gates}}

## 16.4 滚动策略决定新旧 Pod 怎样交接

本课的 Deployment 设置了 3 个 Pod 副本：

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

参数的具体含义如下：

| 参数 | 本课值 | 含义 | 代价 |
|---|---:|---|---|
| `maxUnavailable` | 0 | 更新期间不主动让可用副本少于 3 | 坏版本可能让发布停住 |
| `maxSurge` | 1 | 最多临时多创建 1 个 Pod | 需要额外 CPU、内存和 IP |

readinessProbe 决定新 Pod 何时进入可用副本。若新 Pod 镜像拉取失败或探针不通过，控制器不会把它算作 Ready。

```yaml
readinessProbe:
  httpGet:
    path: /
    port: http
```

每个新 Pod 都要能在容器内通过 `/` 路径的 HTTP 检查。探针成功只说明这条内部就绪路径正常，还要通过 Service 读取发布版本。

::: tip 关键提醒
`maxUnavailable: 0` 可以帮助保留当前可用副本，但它不是“永不中断”保证。节点故障、资源不足、应用共享依赖和错误的探针设计仍可能造成中断。
:::

## 16.5 建立 v1，再滚动到 v2

首先创建 Namespace、v1 内容、Service 与 v1 Deployment：

```bash
[WSL] kubectl apply \
  -f manifests/namespace.yaml \
  -f manifests/configmap-v1.yaml \
  -f manifests/service.yaml \
  -f manifests/deployment-v1.yaml
```

提交 v1 的期望状态。输出的 created/configured 只代表 API 接受了请求。

```bash
[WSL] kubectl rollout status deployment/lesson16-web \
  -n cloud-course-16 --timeout=120s
```

等待三个 v1 副本就绪。

```bash
[WSL] kubectl exec -n cloud-course-16 deploy/lesson16-web -- \
  wget -q -O - http://lesson16-web | grep 'release='
```

通过 Service 读取页面版本，v1 基线应返回 `release=v1`。

准备发布 v2：

```bash
[WSL] kubectl apply \
  -f manifests/configmap-v2.yaml \
  -f manifests/deployment-v2.yaml
```

创建版本化 ConfigMap，并把 Pod 模板的版本标签和挂载改为 v2。Pod 模板改变后，Deployment 创建新 ReplicaSet。

```bash
[WSL] kubectl rollout status deployment/lesson16-web \
  -n cloud-course-16 --timeout=120s
```

观察新 Pod 逐步就绪、旧 Pod 逐步退出，直到显示 `successfully rolled out`。

```bash
[WSL] kubectl rollout history deployment/lesson16-web -n cloud-course-16
```

预期修订 1 为 `lesson16 release v1`，修订 2 为 `lesson16 release v2`。

```bash
[WSL] kubectl get deployment,replicaset,pods \
  -n cloud-course-16 -l app.kubernetes.io/name=lesson16-web -o wide
```

Deployment 应为 3/3；v2 ReplicaSet 维护三个就绪 Pod，v1 ReplicaSet 缩为 0。

## 16.6 错误镜像发生时，先看新旧副本

这里故意使用一个不存在的教学镜像标签来模拟故障。说明和 Pod 模板变更放在同一个 strategic patch 中：

```bash
[WSL] kubectl patch deployment lesson16-web -n cloud-course-16 \
  --type=strategic \
  -p '{"metadata":{"annotations":{"kubernetes.io/change-cause":"lesson16 fault invalid image"}},"spec":{"template":{"spec":{"containers":[{"name":"web","image":"nginx:lesson16-missing"}]}}}}'
```

这条命令只修改本课 Deployment。它会创建新的错误修订，不改变 v2 ConfigMap，也不删除旧 ReplicaSet。

```bash
[WSL] kubectl rollout status deployment/lesson16-web \
  -n cloud-course-16 --timeout=15s
```

本课预期命令会超时并返回非 0。超时本身就是故障证据，不要试图通过延长等待时间来假装问题会自行消失。

```bash
[WSL] kubectl get deployment,pods \
  -n cloud-course-16 -l app.kubernetes.io/name=lesson16-web -o wide
```

真实结果中 Deployment 显示 READY 仍为 3/3、UP-TO-DATE 为 1、AVAILABLE 为 3；三个旧 v2 Pod 仍 Running，新 Pod 会先出现 `ErrImagePull`，重试后进入 `ImagePullBackOff`。说明：

* 新修订没有完成；
* `maxUnavailable: 0` 让三个已就绪 v2 Pod 保持服务；
* Deployment 模板已经指向错误镜像，不能因为页面暂时可用就忽略故障。

先定位那个拉镜像失败的 Pod。如果你能直接从 `kubectl get pods` 的输出中认出异常 Pod 的名字，直接用名字即可。想用命令自动筛选的话，下面是 jsonpath 的拆解——先看懂每一步在做什么，不要整段照抄：

```bash
[WSL] fault_pod="$(kubectl get pods -n cloud-course-16 \
  -l app.kubernetes.io/name=lesson16-web \
  -o jsonpath='{range .items[?(@.status.containerStatuses[0].ready==false)]}{.metadata.name}{"\n"}{end}' \
  | head -n 1)"
[WSL] kubectl describe pod -n cloud-course-16 "$fault_pod"
```

jsonpath 表达式分四步读：① `range .items[]` 遍历 Pod 列表；② `[?(@.status.containerStatuses[0].ready==false)]` 筛选第一个容器尚未就绪的 Pod；③ `.metadata.name` 输出 Pod 名；④ `end` 结束遍历。`-l` 标签选择器已经把范围限定在本课 Deployment 的 Pod，所以最终取到的是本课那个拉镜像失败的 Pod。Events 中的 `Failed to pull image`、`not found` 能把范围进一步缩小到镜像引用或仓库访问。

```bash
[WSL] kubectl rollout history deployment/lesson16-web -n cloud-course-16
```

预期出现第三条 `lesson16 fault invalid image`，为后续选择回滚目标提供依据。

{{chat-lab:lesson-16-rollout-evidence}}

## 16.7 回滚前先写清目标修订

当前历史为：

| 修订 | 说明 | 案例结论 |
|---:|---|---|
| 1 | release v1 | 曾通过基线 |
| 2 | release v2 | 最后一个完整验收版本 |
| 3 | fault invalid image | 发布失败 |

本课明确回到修订 2：

```bash
[WSL] kubectl rollout undo deployment/lesson16-web \
  -n cloud-course-16 --to-revision=2
```

`--to-revision=2` 避免只凭“上一版”猜测目标。命令返回 rolled back 只说明回滚请求已提交。

```bash
[WSL] kubectl rollout status deployment/lesson16-web \
  -n cloud-course-16 --timeout=120s
```

等待回滚后的 Deployment 完成。

```bash
[WSL] kubectl get deployment lesson16-web -n cloud-course-16 \
  -o jsonpath='image={.spec.template.spec.containers[0].image}{" version="}{.spec.template.metadata.labels.app\.kubernetes\.io/version}{"\n"}'
```

检查当前模板恢复为 `nginx:1.27-alpine` 和 `version=v2`。

```bash
[WSL] kubectl exec -n cloud-course-16 deploy/lesson16-web -- \
  wget -q -O - http://lesson16-web | grep 'release='
```

沿原 Service 路径应再次返回 `release=v2`。

```bash
[WSL] kubectl rollout history deployment/lesson16-web -n cloud-course-16
```

回滚后，原修订 2 的模板会成为新的修订 4，因此历史通常显示 1、3、4，而不是直接抹除中间的修订记录。修订 4 的 CHANGE-CAUSE 仍是 release v2。

## 16.8 回滚能恢复什么，不能恢复什么

Deployment 回滚主要恢复 Pod 模板：

| 内容 | 本课是否由 Deployment 回滚 | 说明 |
|---|---|---|
| 容器镜像 | 是 | 位于 Pod 模板 |
| Pod 标签 | 是 | 位于 Pod 模板 |
| 探针与资源限制 | 是 | 位于 Pod 模板 |
| 版本化 ConfigMap 引用 | 是 | volume 引用位于 Pod 模板 |
| ConfigMap 对象内容 | 否 | 独立 API 对象，需要版本化或另行恢复 |
| Service | 否 | 独立 API 对象 |
| 数据库结构与数据 | 否 | 需要独立迁移和恢复方案 |

因此“Deployment 已回滚”不能直接写成“整个系统已经回滚”。真实项目还要检查数据库兼容、缓存、消息、外部 API 和配置版本。

```bash
[WSL] bash verify.sh
```

自动脚本检查当前上下文、三个就绪副本、v2 标签、正确镜像、三个端点、修订历史和 Service 页面。

{{reflection-checkpoint:lesson-16-rollout-ready}}

## 16.9 清理与提交

```bash
[WSL] bash cleanup.sh
```

脚本只删除 `cloud-course-16` 命名空间，并确认查询该空间时返回 NotFound。案例结束后再删除本课 k3d 集群。

```bash
[WSL] kubectl get namespaces
```

确认其他命名空间仍在。不要清理所有 ReplicaSet、镜像或集群系统组件。

完整步骤见[实训 16：Kubernetes 滚动发布、故障与回滚](./lab-16.md)。

提交文件名：

```txt
班级_学号_姓名_第16次课_Kubernetes发布回滚报告.docx
```

只提交一个 Word 到智慧职教“第16次课”。不要上传 kubeconfig、令牌、证书、完整地址、完整事件全集或其他命名空间数据。

## 16.10 第十六次课小测

{{assessment:lesson-16-check}}

## 16.11 小结

* Deployment 通过 ReplicaSet 保存有限的 Pod 模板修订。
* `maxUnavailable: 0` 和 `maxSurge: 1` 控制滚动交接，但不承诺任何场景都无中断。
* `rollout status` 成功、修订历史、三个副本就绪和 Service 版本请求共同构成发布证据。
* 新 Pod ImagePullBackOff 时，要同时看新旧 ReplicaSet、事件和仍在服务的版本。
* 回滚前先读历史并明确目标修订；回滚请求提交后仍需同路验收。
* Deployment 回滚不自动恢复 ConfigMap 对象、Service 或数据库。

下一次课将从 WSL 通过 Ansible 管理云主机 B，并用重复执行与健康检查验证自动化是否幂等。

## 16.12 资料来源

以下资料在 2026-07-28 核对：

* [Kubernetes：Deployments](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/)
* [Kubernetes：kubectl rollout status](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_rollout/kubectl_rollout_status/)
* [Kubernetes：kubectl rollout history](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_rollout/kubectl_rollout_history/)
* [Kubernetes：kubectl rollout undo](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_rollout/kubectl_rollout_undo/)
* [Kubernetes：Pod Lifecycle 与镜像拉取](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/)
* [Kubernetes：Debug Running Pods](https://kubernetes.io/docs/tasks/debug/debug-application/debug-running-pod/)
* [Kubernetes：Liveness、Readiness 与 Startup Probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)

{{assistant-invite:lesson-16-finish}}
