先选择发布的形状,再调整速度。新旧版本能否同时处理请求?能否安全共享存储和任务所有权?如果新版本起不来,如何恢复?这些问题决定 Deployment 应该用 RollingUpdate 重叠运行,还是用 Recreate 先停旧版、再起新版。姊妹篇讨论滚动的节奏和现场观察;本篇讨论兼容性、停机与恢复边界。
逐步替换,还是先停后起
RollingUpdate 是 Deployment 的默认策略。surge 和 unavailable 两个预算控制替换过程,允许新旧 ReplicaSet 同时存在。就绪状态参与可用副本的计算,但不代表业务逻辑正确,更不代表每个请求都成功。不能因为看到 Ready,就推断订单、支付或后台任务已经通过发布验收。
Recreate 在 Deployment 升级时等待旧 Pod 终止,再创建新版本,因此应计划一段没有可用服务副本的时间。选择这个策略时,要移除 rollingUpdate 配置块;其中的字段只属于滚动更新。停机窗口要通过测量得到,不能顺手把三十秒当成保证。旧进程退出慢、新镜像下载慢、存储挂载慢,都可能拉长用户实际感受到的中断。
兼容性才是选择依据
允许版本重叠,意味着接口、持久化数据以及后台工作都能兼容。前端健康检查可能正常,旧 worker 却读不懂新 worker 刚写入的记录。调整副本数之前,应测试混合版本的读写、任务领取和重试行为。副本预算能安排进退顺序,不能修复不兼容的数据库结构变化。
有状态并不自动意味着必须选择 Recreate:协调正确的副本式应用,也可能支持新旧版本同时运行。反过来,Recreate 不是分布式锁。它的先停后起约束适用于 Deployment 升级;如果人工删除一个 Pod,ReplicaSet 可能在旧 Pod 仍处于终止状态时创建替代者。真正的单写者需要应用层协调,必要时还要有隔离旧所有者的机制,确保失去资格的进程不能继续写入。不能把发布策略当成这种机制的替代品。
存储术语也不能混淆。ReadWriteOnce 限制的是一个节点以读写方式挂载,并不等于只能有一个 Pod;同一节点上的多个 Pod 仍可能使用同一卷。对于支持的 CSI 卷,ReadWriteOncePod 才提供单 Pod 访问约束。应检查实际驱动、卷的访问模式与工作负载需求,而不是看到一个存储标签,就认定已经解决了所有权问题。
回滚也是一次变更
保留可以重新启动的旧镜像和修订历史。kubectl rollout undo 恢复记录过的 Pod 模板;只把镜像地址改回去,可能漏掉已经变化的启动参数或环境引用。这两种操作都不会撤销数据库迁移,也不会恢复独立修改过的 ConfigMap 内容。旧模板继续引用同一个 ConfigMap 名称时,重新启动的旧程序仍可能读到新数据。
发布前要记录镜像、配置和数据结构之间的兼容关系。如果需要混合运行或回退,应优先安排向后兼容的迁移顺序。恢复流程必须演练,包括谁执行、按什么顺序做、需要多久,以及如何判断旧版已经恢复业务。只留着旧 ReplicaSet,不能证明恢复是安全的。使用 Recreate 时,回到旧版也可能再经历一次停机和启动;恢复时间不能只按正常升级的一半估算。
坏掉的样子
没有正确协调的 worker,可能在所有 Pod 都 Ready 时重复执行任务或破坏数据。此时需要检查任务所有权与版本兼容性,而不只是看容器是否健康。另一类故障是 Recreate 的冷启动超出预算:镜像拉取或卷挂载很慢,应用真正开始启动之前,停机时间就已经被耗尽。因此演练要包含真正的冷启动和旧版退出耗时,不能只测缓存齐全时的进程启动。
四个副本设置 maxUnavailable: 50%,预算允许减少到两个可用副本,不会自动变成一个。新替代 Pod 崩溃可能让进度停滞;另外发生的节点故障等独立事件,才可能把现有容量继续压低。决定采用这个预算之前,先验证两个实例能否承担预期流量,而不是把「更新仍在允许范围内」等同于业务容量足够。
零 unavailable 也无法防止无关故障。另一个极端是允许全部副本不可用:这样的配置可以合法存在,只是通常不符合服务目标。若团队要禁止它,应明确这是自己的发布政策,而不是 Kubernetes API 的校验规则。真正不能同时为零的是 surge 和 unavailable,否则替换过程没有可推进的空间。
快速验证
把下面保存为 strategy_window.py,运行 python3 strategy_window.py。这是只使用标准库的本地模型,仅接受正整数副本数与非负整数预算。它计算规划量,不模拟控制器,也不是完整的 API 校验器;百分比换算、自动扩缩容和副本数为零的情况不在这个模型内。Kubernetes 允许把 Deployment 缩容到零;程序拒绝它,只因为本练习讨论的是有副本需要替换的场景。
"""Integer rollout-budget arithmetic; no cluster or traffic is exercised."""
def rolling_budget(replicas, surge, unavailable):
values = (replicas, surge, unavailable)
if any(type(value) is not int for value in values):
raise ValueError("this model accepts integers only")
if replicas <= 0 or surge < 0 or unavailable < 0:
raise ValueError("positive replicas and nonnegative budgets required")
if surge == 0 and unavailable == 0:
raise ValueError("surge and unavailable cannot both be zero")
planned_floor = max(0, replicas - unavailable)
target_ceiling = replicas + surge
return planned_floor, target_ceiling
def main():
assert rolling_budget(6, 1, 1) == (5, 7)
assert rolling_budget(4, 0, 2) == (2, 4)
assert rolling_budget(4, 1, 4) == (0, 5)
for invalid in [(4, 0, 0), (4, -1, 1), (0, 1, 0)]:
try:
rolling_budget(*invalid)
except ValueError:
pass
else:
raise AssertionError(f"invalid model input accepted: {invalid}")
print("rolling: planned availability floor=5, replica target ceiling=7")
print("full-unavailability budget: planned floor=0 (permitted)")
print("recreate: expect a zero-serving interval during replacement")
print("PASS: budget arithmetic only; runtime availability untested")
if __name__ == "__main__":
main()
预期输出四行,最后一行明确写着只通过预算算术、未测试运行时可用性。六副本、一 surge、一 unavailable 的结果是五和七;允许四个副本全部不可用的例子,故意得到零,而不是抛出「非法配置」。两项预算同时为零、负预算、零副本等不属于本模型有效输入的情况,则通过断言检查拒绝路径。
正在终止的 Pod、独立故障、调度以及真实请求处理都没有进入模型。因此,五不是实际观测到的服务能力下限,七也不是所有运行进程的绝对上限。程序只帮助检查算术和策略假设,不能给线上可用性出具证明。把数字换成自己的副本配置之前,也要先处理百分比和实际需求变化,而不是直接复制结论。
本次修订在 Linux 的 Python 3.12.14 中执行了断言,没有执行 Kubernetes 集成实验。采用某个策略前,应在测试环境演练升级和回滚,同时持续发请求、检查存储使用,并运行混合版本业务测试。把这些现场结果与本地算术结果分开记录,才知道哪些结论有真实环境的证据支持,哪些还只是规划假设。
延伸阅读
来源
- Kubernetes Deployment,升级顺序与修订回退,已核验 2026-10-15。
- Deployment API,策略字段与更新预算,已核验 2026-10-15。
- Kubernetes 持久卷,节点级与 Pod 级访问模式,已核验 2026-10-15。
- Kubernetes ConfigMap,配置引用与传播,已核验 2026-10-15。
- Kubernetes StatefulSet 删除注意事项,身份与重复进程风险,已核验 2026-10-15。