让控制器把正在运行的副本数量,推向你所声明的目标。

Kubernetes Deployment 描述期望状态

与其描述步骤,不如描述终态。这条思想支撑起副本、滚动更新与回滚的一切机制。 声明规范胜过过程脚本 一份 Deployment 是「终态」的陈述,而不是一张命令清单:多少副本、哪个镜像、哪条 tag、什么标签。操作者写下「要什么」,控制器这位 reconcile 循环决定「怎么要」。它与一条命令的本质差别在于:一条 kubectl run 是命令一样「去执行一下」,而 Deployment 说「这个状态要一直保持」。命令被中途打断可能留下半截结果;控制器则会反复收敛,直到与现实相符。这正是把启动、升级这些动作,从一锤子买卖变成系统持续盯守的事情的能力来源。 由控制器填补差距 reconcile 循环正是 Deployment 值得信任的原因。每次控制面看到现实与声明不相同——副本少了、Pod 被驱逐、节点丢失——它就做一步修正,再把现实推回声明。不是靠人每隔一会去手工补一个副本,而是控制器自己每几秒对一次账。这是它与「打一条命令」的根本差异:漂移不是你发现之后去修,而是系统自行察觉并回位。也正因此,被驱逐的、崩掉的 Pod 会自己回来,而不需要有人在半夜醒着盯。 一次次发布都是经过步骤的 变化也以同样的方式被描述:一次滚动更新就是换一份新的 Deployment,控制器用新副本逐步接管、尊重 ready 状态,让整支队伍在受控的节奏里切换。因为每一次改动都会生成一个新的 Revision 并留下记录,所以发布过程本身是可回溯的;回滚,不过就是再指向上一版 spec,而不是写一段「不知道自己在做什么」的脚本。这让集群在大规模下依然可以维护:意图只写一次,由控制器永久去追索,而每一次改动都把历史留下。 坏掉的样子 不可变的选择器会把一次随手的标签改动变成长期漂移。选择器在创建之后就不能再改,如果只改模板标签而不改选择器,新起的副本就会落在选择器之外:副本集一边报告缺副本,一边却有游离的副本在旁边空转。对称的错误是两份选择器重叠的负载,各自的控制器都把对方的副本算成自己的观测状态,轮流扩缩,谁也收敛不了。解法朴素而严格:把选择器加模板标签当成一份原子契约,对着两者逐行比对,一分钟就能定位漂移;凡是必须换选择器,就新建一份负载,而不是原地修补旧对象。 手改观测对象注定被循环打回。直接改下层副本集的副本数、顺手删一个副本省资源,或者只在集群里扩容却不改声明里的期望副本,都只能生效几秒,控制器下一次同步就会把副本数和模板按声明重写回去。上了版本同步的集群下一次对账就会直接覆盖回去,手工扩容看起来像是没有执行过。表象像是接口没有生效,看事件会发现控制器每轮都在补同一个差。如果紧急扩容是真的,就去改声明与版本库,而不是改眼前的观测对象。 历史版本也会从审计变成负担。每次改动都会留下一份旧副本集,保留多少由历史上限决定,持续集成的流水线每次提交都下发一次,若把上限设到几十,集群里很快攒下几十份僵尸版本,拖慢查询也撑大键值存储。用查询旧版本的数量与存储增长来验证清理效果,而不是只看发布成功。反过来把上限设成零,则连可回滚的目标都不剩。没有实测理由就保留默认的十份,把真正的审计交给版本库,而不是堆在集群里。 快速验证 把下面程序存成 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 labels must not count as observed" print(f"desired={desired} final-observed={observed}") print(f"recovery-actions={actions}") print(f"manual-scale-reverted={drift}") print("PASS: controller closes the desired-vs-observed gap; label mismatch stays visible") if __name__ == "__main__": main() 两次恢复都以同步结尾,而标签错配是否定对照:选择器之外的副本永远不计入观测,循环不会自作主张把契约错误也治好,而是让它一直可见。跑通后会打印两次补差动作序列与最终同步,手工多出的副本被删回期望数。若把期望改成你的真实副本数再跑一遍,收敛结论不变,数字跟着变。 ...

八月 19, 2027 · 1 分钟 · XB Field Notes