---
url: /courses/kubernetes-cluster/06-deployment-rollout/index.md
---
# 第六节 无状态应用发布与回滚

::: tip 项目目标
将独立的 Web Pod 升级为具备声明式自愈、弹性扩缩容、零停机滚动更新与版本回滚能力的 Deployment 对象 `yunfan-web.yaml`；深入理解 **Deployment → ReplicaSet → Pod** 三层层级控制链与 `pod-template-hash` 命名原理；掌握 `maxSurge` / `maxUnavailable` 的数量浮动算式，并完成发布、错误镜像停滞诊断与 `rollout undo` 历史回滚闭环。
:::

## 6.1 架构控制链：Deployment、ReplicaSet 与 Pod 的三层关系

在上一课中，我们部署了独立的 Pod。然而，独立 Pod 一旦遭遇宿主机故障或被误删，**不会自动重建**。在生产环境中，无状态应用（Stateless Applications）需要具备自愈和滚动扩缩的能力。

在 Kubernetes 中，管理无状态应用的标准控制器是 **Deployment**。

### 6.1.1 三层控制链与 `pod-template-hash`

Deployment 并不直接去操控或创建具体的 Pod 容器，而是通过 **ReplicaSet** 进行层级间接控制：

1. **Deployment（发布控制器）**：负责管理 Pod 的模板版本（Pod Template）、发布策略（RollingUpdate）与版本修订历史（Revision History）。
2. **ReplicaSet（副本集控制器）**：负责维护匹配特定的选择器（Selector）的 Pod **期望副本数量（Replicas）**。当某个 Pod 消失时，ReplicaSet 负责补出新 Pod。
3. **Pod（运行实例）**：真正承载业务容器的运行单元。

```mermaid
flowchart TB
  subgraph Deployment["Deployment: yunfan-web (期望副本: 3)"]
    subgraph RS_V1["ReplicaSet v1 (yunfan-web-74b889895)"]
      Pod1["Pod 1: yunfan-web-74b889895-a1b2c"]
      Pod2["Pod 2: yunfan-web-74b889895-d3e4f"]
      Pod3["Pod 3: yunfan-web-74b889895-g5h6i"]
    end
  end
```

**解密 Pod 的命名规则**：
当你运行 `kubectl get pods` 时，会看到 Pod 的名称类似于 `yunfan-web-74b889895-a1b2c`。

* `yunfan-web`: Deployment 的名称。
* `74b889895`: **`pod-template-hash`**。这是根据 Pod 模板（`spec.template`）内容计算出的唯一 Hash 值，也是它所属的 ReplicaSet 的名字！
* `a1b2c`: ReplicaSet 为该特定 Pod 实例生成的随机后缀。

观察下面的交互面板，理解 Deployment 如何通过控制 ReplicaSet 来间接保持 Pod 数量：

***

## 6.2 配置文件编写：声明式 `yunfan-web.yaml`

在项目根目录下直接创建 Deployment 配置文件 `yunfan-web.yaml`。

### 6.2.1 完整配置文件：`yunfan-web.yaml`

```yaml title="yunfan-web.yaml"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: yunfan-web
  namespace: yunfan-shop
  labels:
    app.kubernetes.io/name: yunfan-web
    app.kubernetes.io/part-of: yunfan-mall
spec:
  replicas: 3
  revisionHistoryLimit: 5
  progressDeadlineSeconds: 120
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: yunfan-web
  template:
    metadata:
      labels:
        app.kubernetes.io/name: yunfan-web
        app.kubernetes.io/part-of: yunfan-mall
        tier: frontend
        env: lab
    spec:
      containers:
        - name: nginx
          image: nginx:1.27-alpine
          resources:
            requests:
              cpu: 50m
              memory: 32Mi
            limits:
              cpu: 200m
              memory: 128Mi
          ports:
            - name: http
              containerPort: 80
```

在终端中应用该配置文件：

```bash
kubectl apply -f yunfan-web.yaml
kubectl rollout status deployment/yunfan-web -n yunfan-shop --timeout=120s
```

### 6.2.2 滚动更新策略与关键参数算式解析

* **`strategy.type: RollingUpdate`（滚动更新）**：
  * 在更新应用时，逐步创建新版本的 Pod 并逐步销毁旧版本的 Pod，实现 **零停机时间（Zero-Downtime）**。相比之下，`type: Recreate` 会先杀光所有旧 Pod 再创建新 Pod，会导致服务中断。
* **`maxSurge: 1` 与 `maxUnavailable: 1` 算式**：
  * **`maxSurge`（最大超额量）**：发布过程中，**允许临时比期望副本数（`replicas: 3`）多出的 Pod 数量上限**。本例中最高允许总 Pod 数达到 `3 + 1 = 4` 个。
  * **`maxUnavailable`（最大不可用量）**：发布过程中，**允许临时处于非就绪状态或被销毁的 Pod 数量上限**。本例中最低保证可用 Pod 数不低于 `3 - 1 = 2` 个。
* **`revisionHistoryLimit: 5`**：
  * 保留最近 5 个历史 ReplicaSet 修订版本（Revision），以便于后续执行 `kubectl rollout undo` 一键回滚。
* **`progressDeadlineSeconds: 120`**：
  * 判定发布卡死的超时阀值。如果发布过程超过 120 秒仍未完成，Deployment 会在 Status 中标注 `ProgressDeadlineExceeded`。

运行以下命令，同时查看三层对象的运行状态：

```bash
kubectl get deployment,replicaset,pod -n yunfan-shop -o wide
```

***

## 6.3 运维实训：弹性扩缩容与声明式自愈

### 6.3.1 副本动态扩缩容 (Scale)

当流量突增或降低时，可以随时调整 Deployment 的副本数量。

**1. 命令式调整副本**：

```bash
# 将副本数缩容到 2
kubectl scale deployment/yunfan-web -n yunfan-shop --replicas=2
kubectl get pod -n yunfan-shop -l app.kubernetes.io/name=yunfan-web

# 将副本数扩容会 3
kubectl scale deployment/yunfan-web -n yunfan-shop --replicas=3
kubectl rollout status deployment/yunfan-web -n yunfan-shop
```

**原理解析**：
运行 `kubectl scale` 调整副本数时，只改变了 ReplicaSet 的 `spec.replicas` 属性，**没有修改 Pod 模板（`spec.template`）**，因此**不会触发新 Revision 历史版本的创建**。

### 6.3.2 声明式自愈能力测试

手动删除其中的一个 Pod，验证 ReplicaSet 的自愈机制：

```bash
# 获取当前 Pod 列表
kubectl get pod -n yunfan-shop -l app.kubernetes.io/name=yunfan-web

# 随意删除其中一个 Pod
kubectl delete pod -n yunfan-shop <其中一个-pod-name>

# 实时观察 Pod 状态变化
kubectl get pod -n yunfan-shop -l app.kubernetes.io/name=yunfan-web -w
```

**关键观察点**：
被删除的旧 Pod 状态变为 `Terminating`，而 ReplicaSet 会在毫秒级时间内检测到期望副本数不满足，并立即创建一个全新的 Pod！新 Pod 的名称后缀与 UID 会发生改变，但 Deployment 与 ReplicaSet 依然保持稳定性。

***

## 6.4 版本发布：滚动更新与 ReplicaSet 交接

当业务需要升级镜像或修改环境变量时，通过更新 Deployment 的 Pod 模板来触发滚动更新。

观察下面的滚动更新动画，理解新 ReplicaSet 如何逐步放大、旧 ReplicaSet 如何逐步缩小：

### 6.4.1 滚动更新实操

**1. 添加变更注解并更新镜像**：

```bash
# 添加变更原因注解 (记录在 history 中)
kubectl annotate deployment/yunfan-web \
  -n yunfan-shop \
  kubernetes.io/change-cause="升级 Nginx 镜像到 1.28-alpine" \
  --overwrite

# 修改镜像触发滚动更新
kubectl set image deployment/yunfan-web \
  -n yunfan-shop \
  nginx=nginx:1.28-alpine
```

**2. 观察滚动更新过程与历史记录**：

```bash
# 观察发布状态
kubectl rollout status deployment/yunfan-web -n yunfan-shop --timeout=120s

# 查看发布历史版本记录
kubectl rollout history deployment/yunfan-web -n yunfan-shop

# 查看 ReplicaSet 列表
kubectl get replicaset -n yunfan-shop
```

**预期观察**：
你会看到系统保留了两个 ReplicaSet。旧的 ReplicaSet 副本数被缩小为 `0`，全新的 ReplicaSet 副本数增长为 `3`。

***

## 6.5 故障演练：错误镜像停滞与 `rollout undo` 回滚

在实际生产中，发布可能会因镜像 tag 写错或代码崩溃而停滞。我们需要掌握如何诊断卡住的发布并执行一键回滚。

### 6.5.1 触发错误镜像发布

故意更新为一个不存在的镜像版本：

```bash
# 记录故障变更原因
kubectl annotate deployment/yunfan-web \
  -n yunfan-shop \
  kubernetes.io/change-cause="故意引入错误镜像进行回滚测试" \
  --overwrite

# 更新为一个不存在的镜像 tag
kubectl set image deployment/yunfan-web \
  -n yunfan-shop \
  nginx=nginx:not-exist
```

**观察发布停滞**：

```bash
# 查看发布状态 (会在 60 秒后超时)
kubectl rollout status deployment/yunfan-web -n yunfan-shop --timeout=60s
```

此时查看 Pod 和 ReplicaSet 状态：

```bash
kubectl get pod,replicaset -n yunfan-shop
kubectl describe deployment yunfan-web -n yunfan-shop
```

**原理分析**：
由于 `maxUnavailable: 1` 的保护，系统最多允许 1 个 Pod 不可用。旧的 ReplicaSet 依然保持着 2 个健康的旧 Pod 在对外提供服务；而新 ReplicaSet 创建出的 1 个 Pod 会卡在 `ImagePullBackOff` 状态。发布过程安全地卡住了，**不会将所有 Pod 一下子全部弄挂**。

### 6.5.2 执行一键版本回滚 (`rollout undo`)

确认发布故障后，使用 `rollout undo` 撤销发布，恢复到上一个稳定版本：

```bash
# 撤销本次失败的发布，回滚到上一个 Revision
kubectl rollout undo deployment/yunfan-web -n yunfan-shop

# 验证回滚状态
kubectl rollout status deployment/yunfan-web -n yunfan-shop --timeout=120s
```

验证回滚后的当前镜像：

```bash
kubectl get deployment yunfan-web -n yunfan-shop \
  -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
```

**预期输出**：镜像恢复为之前的 `nginx:1.28-alpine`。

若要回滚到特定的历史版本，可以先通过 `kubectl rollout history` 查看版本号，再指定 `--to-revision`：

```bash
# 回滚到指定的历史版本 (如 Revision 1)
kubectl rollout undo deployment/yunfan-web -n yunfan-shop --to-revision=1
```

***

## 6.6 高级技巧：发布暂停、恢复与重启

### 6.6.1 暂停与恢复发布 (`pause` / `resume`)

当你需要对 Deployment 的 Pod 模板进行多项修改（如同时修改镜像、环境变量和资源限制）时，可以先暂停发布，避免每次修改都触发一次不必要的滚动更新：

```bash
# 1. 暂停 Deployment 的发布响应
kubectl rollout pause deployment/yunfan-web -n yunfan-shop

# 2. 连续执行多项修改 (此时不会触发更新)
kubectl set resources deployment/yunfan-web \
  -n yunfan-shop \
  -c nginx \
  --requests=cpu=60m,memory=40Mi \
  --limits=cpu=200m,memory=128Mi

# 3. 恢复发布，一次性生效所有变更
kubectl rollout resume deployment/yunfan-web -n yunfan-shop
kubectl rollout status deployment/yunfan-web -n yunfan-shop
```

### 6.6.2 触发平滑重启 (`rollout restart`)

如果需要刷新所有 Pod（如业务应用需要重新读取 ConfigMap），无需修改镜像，直接运行重启命令：

```bash
kubectl rollout restart deployment/yunfan-web -n yunfan-shop
kubectl rollout status deployment/yunfan-web -n yunfan-shop
```

**底层原理**：`rollout restart` 会自动在 Pod 模板的 `metadata.annotations` 中注入当前时间戳（`kubectl.kubernetes.io/restartedAt`），由于 Pod 模板发生了改变，系统会无缝地进行一次零停机平滑滚动重启。

***

## 6.7 阶段验收与排错速查

### 6.7.1 发布事件线模板

在运维实践中，整理发布事件线有助于复盘：

| 时间节点 | 操作动作 | 系统状态与 ReplicaSet 表现 | 运维决策与结果 |
| --- | --- | --- | --- |
| **T0** | 初始部署 `1.27-alpine` | 创建 RS-1，3/3 Pod 就绪 | 建立稳定基线 Revision 1 |
| **T1** | 升级到 `1.28-alpine` | 创建 RS-2，RS-2 扩容至 3，RS-1 缩容至 0 | 发布成功，生成 Revision 2 |
| **T2** | 误操作升级 `not-exist` | 创建 RS-3，1 Pod `ImagePullBackOff`；旧 RS-2 保持 2 Pod | 触发 `maxUnavailable` 拦截保护 |
| **T3** | 执行 `rollout undo` | RS-3 被删除，RS-2 重新扩容至 3 | 成功回滚至 Revision 2 稳定状态 |

### 6.7.2 本课提交成果清单

完成本课实训后，提交以下三项成果即可：

1. **配置文件**：项目根目录下的 `yunfan-web.yaml`。
2. **第一张截图 `06-control-chain.png`**：同一终端画面中包含 `kubectl get deployment,replicaset,pod -n yunfan-shop -o wide` 展示三层层级控制链。
3. **第二张截图 `06-rollout-undo.png`**：同一终端画面中包含 `kubectl rollout history`、错误镜像卡住的 Pod 状态以及执行 `rollout undo` 成功恢复的结果。

{{reflection-checkpoint:deployment-rollout-check}}

### 6.7.3 常见错误与排错矩阵

| 故障现象 | 可能原因 | 修复排查路径 |
| --- | --- | --- |
| `rollout status` 长时间卡住 | 镜像不存在、`limits` 资源不足或探测失败 | 运行 `kubectl get pods` 找出卡住的 Pod；用 `describe pod` 查看 Events 确切原因。 |
| `rollout undo` 提示找不到 Revision | `revisionHistoryLimit` 设置过小导致旧 RS 被清理 | 确认 `rollout history`；若旧 RS 已清空，使用 `kubectl apply -f` 重新应用备份的 YAML。 |
| 两个控制器争抢同一个 Pod | 不同 Deployment 的 `spec.selector` 标签重叠 | 检查各 YAML 文件的 `selector.matchLabels`，确保不同应用的标签相互独立。 |
| 修改了配置但未触发滚动更新 | 仅修改了 `replicas` 数量或非模板字段 | 确认是否改动了 `spec.template`；若需强制重启，运行 `kubectl rollout restart`。 |

::: details 挑战任务：探秘 pod-template-hash 的作用
运行 `kubectl get rs -n yunfan-shop --show-labels`，观察每一个 ReplicaSet 上的标签。尝试解释：为什么每个 ReplicaSet 和它创建的 Pod 上都会自动被注入一个 `pod-template-hash` 标签？如果没有这个 Hash 标签，当两个 Deployment 使用相同的业务标签时会发生什么？
:::

***

## 本章小测

{{assessment:deployment-rollout}}

## 本章小结

完成本课后，你已经掌握了：

1. **三层控制链架构**：理解了 **Deployment → ReplicaSet → Pod** 的层级控制关系，以及 `pod-template-hash` 保证模板与副本集对应的机制。
2. **滚动更新与算式逻辑**：掌握了 `strategy.type: RollingUpdate` 的工作流，能够通过 `maxSurge` 和 `maxUnavailable` 精确计算发布过程中的 Pod 数量上限与下限。
3. **弹性扩缩与声明式自愈**：理解了 `kubectl scale` 不增加 Revision 的原因，验证了 ReplicaSet 维持期望副本数自动补齐 Pod 的自愈能力。
4. **版本发布与历史回滚**：熟练掌握了 `kubectl set image`、`rollout history` 以及 `kubectl rollout undo`（含 `--to-revision`）在故障时的止损回滚。
5. **批处理与平滑重启**：掌握了 `rollout pause`/`resume` 合并变更与 `rollout restart` 通过修改模板注解平滑重建 Pod 的高级技巧。

下一课中，我们将为 `yunfan-web` 引入 **Service 服务发现与负载均衡**，解决 Pod IP 动态变化时的稳定访问与 NodePort 外部接入问题！
