与其描述步骤,不如描述终态。这条思想支撑起副本、滚动更新与回滚的一切机制。

声明规范胜过过程脚本

一份 Deployment 是「终态」的陈述,而不是一张命令清单:多少副本、哪个镜像、哪条 tag、什么标签。操作者写下「要什么」,控制器这位 reconcile 循环决定「怎么要」。区别在于资源所有权,而不是有没有使用命令行。kubectl run 通常创建独立 Pod,kubelet 可以重启其中容器,但这个 Pod 消失后,没有 ReplicaSet 负责补足一组替代对象。Deployment 同样可以通过一条命令创建,客户端退出之后,存储下来的规范仍会持续指导控制器。中断客户端并不能可靠说明 API 是否已经接纳请求;重试或诊断收敛之前,先查看实际存储的对象,区分请求传递状态与控制器执行状态。

由控制器填补差距

Deployment 控制器管理 ReplicaSet,再由副本集控制器维持 Pod 数量。容器崩溃时,kubelet 可以在同一个 Pod 内重启容器;删除或丢失 Pod 后创建替代对象,则是另一条协调路径。它们会配合工作,但不能都说成同一个控制器每隔几秒创建一个 Pod,否则无法区分实例身份、重启与替换。

协调是异步过程,依赖控制面正常以及容量足够。镜像拉取失败、配额、准入拒绝和存储不可用,都可能长期阻止收敛。应查看条件与事件,确认究竟哪一层受阻。期望副本数是系统追求的目标,不是真实就绪容量的证据,也不是恢复时间上限;控制器反复尝试不等于问题已经得到解决。

一次次发布都是经过步骤的

修改 Pod 模板会触发发布与修订版本,单纯缩放不会。控制器按照更新策略、就绪和可用性要求调整新旧 ReplicaSet 数量。流量由 Service 与对应数据路径另行处理,Deployment 控制器本身不是逐条迁移连接的代理。发布状态和请求成功需要分别观察,不能把模板变化自动等同于无中断流量切换。

回滚恢复先前 Pod 模板,不会恢复 Deployment 所有字段,更不会撤销数据库模式等外部状态。保留的 ReplicaSet 提供回滚历史,revisionHistoryLimit 默认十份,清理从发布完成之后开始。流水线重复应用未变化的模板,并不会只因执行了一次就生成新修订;版本库审计与集群修订解决的是不同范围的历史问题。

坏掉的样子

apps/v1 Deployment 的选择器不可变,Pod 模板必须匹配它。模板违反选择器时会被 API 校验拒绝,不是先接受对象,再持续产生不受管理的游离 Pod。需要改变选择器时,应规划明确迁移。选择器重叠值得审查,但仅仅标签匹配,不会抹掉已有控制所有者引用,不能把所有重叠都解释成控制器互抢副本。

删除受管理 Pod 或直接缩放下层 ReplicaSet,可能被负责的控制器纠正。相比之下,kubectl scale deployment 修改的就是 Deployment 期望副本数,直到其他参与者再次修改才会变化。HPA、后续 apply 或 GitOps 协调器可能覆盖它;Deployment 自己不会秘密记住版本库里的旧数字。应找出真正写入该字段的主体,再制定紧急缩放流程。

受阻发布可能因清理完成条件未满足,保留超过预期数量的 ReplicaSet。宣布历史泄漏前,应查看 rollout 状态。外部变更历史与经验证回滚计划仍应保留;历史上限零会移除正常回滚目标,但数量大本身也不能证明控制面性能已经下降。需要用实际对象状态与系统指标区分暂时保留、策略选择和真实故障。

快速验证

把下面程序存成 desired_state_loop.py,运行 python3 desired_state_loop.py。它只用标准库,每轮比较期望与观测,差一个就补一个动作,直到差距归零,既演示节点丢失后的补齐,也演示手改多出的副本如何被收回。

"""期望态收敛循环模型(仅标准库):期望 vs 观测,控制器补差直到归零。"""

def reconcile_step(desired, observed):
    if observed < desired:
        return "create-one-pod"
    if observed > desired:
        return "delete-one-pod"
    return "in-sync"


def converge(desired, observed, log):
    while observed != desired:
        action = reconcile_step(desired, observed)
        log.append(action)
        if action == "create-one-pod":
            observed += 1
        else:
            observed -= 1
    log.append("in-sync")
    return observed


def main():
    desired = 3
    observed = 1  # 节点丢失后只剩一个
    actions = []
    observed = converge(desired, observed, actions)
    assert observed == desired
    assert actions == ["create-one-pod", "create-one-pod", "in-sync"], actions

    # 手工漂移:期望未变,观测多了两个。
    observed = 5
    drift = []
    observed = converge(desired, observed, drift)
    assert observed == desired
    assert drift == ["delete-one-pod", "delete-one-pod", "in-sync"], drift

    # 在接纳期望状态前,用本地检查模拟选择器与模板的校验。
    selector = {"app": "api"}
    template_labels = {"app": "api-v2"}  # 故意错配,否定对照
    matched = all(template_labels.get(k) == v for k, v in selector.items())
    assert not matched, "mismatched template must fail this validation model"

    print(f"desired={desired} final-observed={observed}")
    print(f"recovery-actions={actions}")
    print(f"observed-count-drift-corrected={drift}")
    print("PASS: controller closes the desired-vs-observed gap; mismatched template is rejected by the local validation check")


if __name__ == "__main__":
    main()

两次整数数量恢复都以同步结尾。另一个标签检查把不匹配模板作为反例拒绝,演示 API 接纳之前要检查的契约,但没有提交真实 Deployment,也没有实现接管、监听、调度和就绪机制。跑通后会打印两次补差动作序列与最终同步,手工多出的副本被删回期望数。若把期望改成你的真实副本数再跑一遍,收敛结论不变,数字跟着变。

本轮在 Linux、Python 3.12.14 上运行两种语言的断言通过。模型只验证收敛算术,不涉及调度探针与存储,发布日之前,还要去真实集群确认选择器、副本数与历史版本数,并检查真正的就绪容量与请求失败率。

延伸阅读

来源