滚动更新是一种受控替换,不是服务绝不中断的承诺。它的价值在于引入新副本时可以保留部分旧副本;用户能否平稳完成请求,还取决于资源余量、健康检查是否可信,以及应用如何退出。本文讨论已经选择滚动更新之后怎样调节节奏、怎样观察过程;姊妹篇讨论更靠前的问题:两个版本到底能不能同时运行。

浪涌与缺口

十个期望副本,配 maxSurge: 2,更新目标上限是十二,不是十一。maxUnavailable: 1 给控制器的可用性预算是九个。这不是「每一波最多删除一个旧 Pod」:只要已经有足够的新副本可用,控制器就能继续撤掉多个旧副本。正在终止的 Pod 还可能保留一段时间并继续消耗资源,因此十二也不是所有 Pod 对象或进程数量的绝对上限。

百分比必须先换算整数:surge 向上取整,unavailable 向下取整。三个副本使用默认的百分之二十五,实际就是一个 surge、零个 unavailable。这里有一个边界例外:surge 为零、非零的 unavailable 百分比却向下取成零时,控制器会把有效 unavailable 调整为一,让替换能够推进。因此,不能从这个换算得到的零,推断更新一定不会主动减少可用副本。

两个参数都不会替你预留节点资源。选择速度之前,要把资源请求量与可调度容量、命名空间配额、放置约束以及旧 Pod 的退出耗时一起检查。账面上多出的两个位置,并不一定真有节点能承接;新 Pod 全部 Pending 时,调大 surge 也不会凭空生出机器。

健康是换班的门槛

首先确认清单中确实配置了就绪探针。就绪检查应该回答「这个实例现在能否处理预期的流量」。只检查端口监听可能漏掉应用故障;检查必要的后端依赖可能更准确,但共享依赖短暂失败也可能让全部副本一起退出就绪状态。是否应该停止接流量,要根据业务的降级能力决定,而不是机械地把所有依赖都塞进探针,更不能一看到失败就放宽条件。

minReadySeconds 要求新 Pod 持续就绪一段时间,之后才计入 Deployment 的可用副本。它不会把 Service 流量也延迟到这段时间结束。启动慢的容器可以用 startup probe,避免还在初始化就被存活检查重启。这些检查仍不能证明订单处理正确,也不能证明实例能持续承受生产负载。

退出过程需要单独验证。端点状态传播、旧连接排空、应用处理终止信号、终止宽限期必须配合。应用即使一直把就绪端点返回成功,也可能在退出时丢掉正在处理的请求。把「Pod 数量看起来充足」当成「用户请求没有失败」,会漏掉这一整类问题。

选一个有意为之的节奏

对于有额外容量的小规模服务,一个 surge、零个 unavailable 是可以考虑的起点。它限制更新过程主动削减可用副本,却不能阻止节点故障、依赖故障或请求错误。PodDisruptionBudget 也不会约束 Deployment 自己执行的滚动升级;不要把节点维护时的驱逐保护,误当成应用发布的额外保险。

人工暂停适合停下来检查,但它不是精确的单 Pod 金丝雀发布,也不锁定固定的流量比例。暂停生效前,控制器可能已经推进;暂停后,现有副本仍会运行并接流量。如果要求新版严格只接少量流量,就需要独立控制的金丝雀副本与路由。发布前应写明错误率、延迟、重启和关键业务检查的通过线,并规定完成后的观察时长,不能等看见曲线才临时决定标准。

坏掉的样子

滚不完时,先找故障发生在哪一层。调度事件指向资源或放置问题;拉取镜像失败指向镜像引用或仓库访问;容器日志与探针事件用来区分进程崩溃和就绪失败。先确认探针是否准确发现了故障,再决定要修应用、修配置,还是修检查本身。仅凭一个超时就降低健康标准,可能把尚未承接流量的错误推进到全量。

坏版本可能先消耗允许的不可用额度,然后才停滞,并不保证旧副本全部留下。progressDeadlineSeconds 会报告进度停滞,但不会自动回滚,也不会把所有协调动作冻结。应为该条件配置告警,再根据证据决定修复或回退。滚动顺利结束也不等于业务验收完成:慢内存泄漏、连接耗尽与真实流量下的错误,可能在后续观察期才出现。观察通过以前,应保留可用的修订历史和旧镜像,并确认旧版仍能启动。

快速验证

把下面的标准库练习保存为 rounding_window.py,运行 python3 rounding_window.py。程序用整数算术检查百分比换算,包括小规模副本、恰好整除的边界以及百分之百。它不是 Deployment 控制器模拟器,也不是完整的 API 校验器;不能把函数接受的所有输入都理解为有效的发布配置。百分之零到百分之一百的输入范围只是本练习的限制,并不是 maxSurge 的 API 上限。

"""Check percentage rounding only; no Kubernetes API calls."""


def round_percent(replicas, percent, *, up):
    if type(replicas) is not int or replicas <= 0:
        raise ValueError("this exercise requires positive integer replicas")
    if type(percent) is not int or not 0 <= percent <= 100:
        raise ValueError("this exercise accepts integer percentages 0..100")
    numerator = replicas * percent
    return (numerator + 99) // 100 if up else numerator // 100


def main():
    cases = [(1, 1, 0, 1), (3, 25, 0, 1), (4, 50, 2, 2),
             (10, 20, 2, 2), (10, 10, 1, 1), (4, 100, 4, 4)]
    for replicas, percent, down, up in cases:
        assert round_percent(replicas, percent, up=False) == down
        assert round_percent(replicas, percent, up=True) == up
        print(f"replicas={replicas}, percent={percent}: down={down}, up={up}")
    print("PASS: percentage rounding only; no cluster exercised")


if __name__ == "__main__":
    main()

预期先输出六行案例,再输出一行明确限定为百分比换算的 PASS。三个副本乘百分之二十五,向下取整为零、向上取整为一;十个副本乘百分之二十,两种方向都是二。百分之百的案例确认换算结果可以覆盖全部副本,并不因服务目标不允许停机就自动变成数学错误。本次修订在 Linux 的 Python 3.12.14 中执行了这些断言。通过只证明算术一致,没有证明服务可用性。

把数字用到真实发布前,先选择正确的测试集群上下文与命名空间,并确认 Deployment 实际配置。如果已有 api Deployment、Pod 标签为 app=api,可用 kubectl rollout status deployment/api --timeout=120s 观察收敛,在另一个终端用 kubectl get pods -l app=api -w 观察替换。客户端等待超时,只表示命令不再等待,并没有取消更新。如果可能同时发起多个版本,使用 --revision=N 指定要检查的修订号。观察结束时按 Ctrl-C。

这里没有执行这些集群命令。仍需在测试环境升级期间收集 Pod 事件、日志和 Deployment 状态,同时持续从真实 Service 或入口路径发请求,把错误和退出期间的延迟一起记录。事件中的对象名称、发生时间要与这次发布对应,不能把其他服务的故障当成当前更新的证据。这些才是尚待完成的集成验证;只看最终成功的一行输出,无法证明零停机。

延伸阅读

来源