第 5 章:幂等不只是忽略 AlreadyExists
重复执行要保持目标不变
同一个队列键可能因主对象更新、状态变化、周期复查或进程重启而多次出现。幂等要求重复协调不会创建重复实例、破坏已经正确的资源,或把本来稳定的状态不断改写。它不是保证函数只执行一次,而是让多次执行仍指向同一个目标。
最简单的错误是每次使用 GenerateName 创建 Deployment。即使每次创建都成功,资源数量也会持续增长。稳定命名加 owner UID 检查,使同一次对象生命能够找到已有子资源;而外部登记采用 UID 派生名称,使不同对象生命不会误共享同一个副作用标识。
AlreadyExists 只是服务器报告名称冲突,并没有证明现有对象符合期望。正确流程是读取、核对身份、比较托管字段,再决定是否更新。直接忽略错误会把旧端口、错误镜像或外来对象当成成功收敛。
比较应尊重 API 默认值
构造的期望对象只写了少量字段,服务器返回的对象却含有默认策略、协议、探针参数和终止消息字段。使用完整结构 DeepEqual 往往会认为它们永远不同,导致每秒 Update,产生资源版本增长、watch 事件和额外 API 压力。
随书实现对 Service 明确写入 TCP 协议,对容器只比较所属字段。判断是否需要更新时,不比较服务器自行维护的 resourceVersion、status 或默认终止消息路径。实际更新也基于新读取对象,保留无需改变的属性。代码评审应同时检查“怎么比较”和“怎么写回”,两者不一致仍可能造成反复更新。
有些字段天然不适合每轮生成新值。例如把当前时间写进 Pod 标签或注解,必然触发滚动更新;把协调时间无条件写入 status,也会让状态 watch 永远唤醒自身。时间应记录真正的状态转移,而不是证明进程刚刚运行过。
状态稳定也属于幂等
Reconciled 的类型、布尔状态和原因没有改变时,lastTransitionTime 应保持不变。observedGeneration 在处理新意图后可以推进,但不能仅因为重试就改时间。本项目先构造下一份 status,与已有结构比较,相同则不调用 UpdateStatus。
真实测试重复执行协调,断言 CR 的 resourceVersion 不变、Deployment 的 UID 和 generation 不变。Deployment 自身状态可能由内建控制器更新,所以不能把它所有 resourceVersion 变化都归罪于我们的控制器。测试应选择与自己承诺对应的观察量,避免把其他合法参与者的行为当成失败。
幂等还包含错误后的重入。第一轮创建 Deployment 后创建 Service 失败,第二轮应继续使用同一 Deployment 并补齐 Service。为了说明这种恢复,阅读代码时可以在每次 API 写入后画一条崩溃线:下一轮从主对象重新读取,是否仍能识别前一次已经完成的动作?
幂等不能替代并发控制
两个进程可能同时读取旧 Deployment,并分别改变不同字段。即使单独看每个操作都是幂等,后写者仍可能覆盖先写者。resourceVersion 的条件更新提供乐观并发保护,但冲突后必须重读;重复发送同一份过时对象不会变得正确。
同样,外部 API 的“同名查询后创建”可能有竞争窗口。真实系统需要服务端支持稳定幂等键,或者有可信的结果查询和唯一约束。本项目用同一个 Kubernetes API 的名称唯一性简化了这一点,并没有证明任意云 API 调用都能照搬。
故意制造漂移来检验承诺
测试会改坏 Service selector、端口、目标端口,以及 Envoy 参数和部分 Pod 标签,然后执行协调,确认声明托管的字段恢复。它与删除自愈互补:删除检验资源是否可重建,漂移检验已有资源是否可修正。只验证首次创建,会漏掉绝大部分持续托管错误。
练习时增加一个“不应被改变”的注解,再运行相同漂移测试。恢复所属字段和保留无关信息应同时成立。若修复方案是每次删除重建全部子资源,虽然容易得到正确配置,却破坏稳定入口、放大停机与更新成本,必须作为明确的产品策略评估。
推演案例:已有资源不等于已完成
初次协调在创建Service后崩溃,尚未写入状态。下一进程发现Deployment与Service都存在,但仍需要核对端口、选择器与代理参数,再确认本次generation。若仅凭两个名字存在就写成功,可能把其他操作者停机期间改坏的配置当成有效结果。
相反,如果每轮都无条件Update,即使内容相同,也会产生事件和资源版本变化。事件又唤醒主对象或相关控制器,构成不必要的反馈。幂等的实用目标不仅是不重复创建,还包括稳定配置时尽可能不写入,让变化信号更有诊断价值。
可以把一轮协调分成读取成本、必要写入和可避免写入三类。优化时先消除无意义写入,再考虑减少读取,因为过早缓存权威对象可能引入更难解释的并发窗口。当前项目优先可推理性,使用直接读取保证重试基于新对象,规模优化留给有明确负载证据的后续版本。