第 8 章:状态必须说真话
状态是用户的观察接口
用户无法从控制器内存判断一次更新进行到了哪里,只能读取 status、事件和日志。状态如果过早报告成功,自动化发布系统可能继续推进下一阶段;如果状态长期不变,运维人员也不知道控制器在等待、失败还是已经停止。因此 status 不是装饰字段,而是另一个需要维护兼容性的接口。
本项目刻意把条件命名为 Reconciled,而非 Ready。ChildrenApplied 的含义是该 generation 的托管资源字段已经被应用。Deployment 可用和 HTTP 响应由集群行为及实验独立验证。读者如果要加入 Ready,应明确其检查范围:仅 Pod Ready、至少一个 Endpoint 可用,还是跨真实路由的业务探测。
一个条件通常需要 type、status、reason、observedGeneration 和 lastTransitionTime。reason 应保持便于程序判断的稳定标识,详细错误可放入消息或事件。把完整堆栈、随机错误文本甚至凭据写入 reason,不利于兼容,也可能把敏感信息暴露给所有能够读取对象的人。
observedGeneration 的因果关系
顶层 observedGeneration 和条件中的 observedGeneration 都要指向控制器实际处理的意图。它不是“最近读过哪个对象”,而是“这个结论基于哪个 spec”。读取最新 generation 后直接填值,很容易在并发更新期间制造虚假成功。
真实竞争场景按三个阶段安排:先完成旧配置并记录旧 observedGeneration;接着在子资源已处理、状态尚未提交时改变 spec;最后检查本轮返回重试且旧状态仍保留。新一轮协调才允许把 observedGeneration 推进到新值,并同时验证 Deployment 副本数已更新。这组断言将状态与事实连接起来,单独检查字段数值递增是不够的。
本项目在 generation 不一致时直接放弃本轮状态确认。另一种设计是写入旧 generation 的条件,同时允许用户看到新 spec 尚未处理。两者都能正确表达事实,关键是不能把旧处理结果贴到新版本上。实际产品应根据客户端如何等待条件选择一致的接口规则。
条件变化与时间变化
状态、类型和原因保持一致时,lastTransitionTime 保留原值。每次协调都刷新时间,会让用户误以为状态一直在发生转移,也会造成额外写入。处理新 generation 但结果仍然相同时,可以更新观察版本而保留同一条件的转移时间;如果需要记录每次成功处理时间,应使用另一个语义明确的字段。
当前实现保存单个主条件:正常时为 Reconciled,清理阻塞时为 CleanupBlocked。这是为教学缩小状态表,不是推荐所有生产 Operator 只保留一条条件。更完整的系统通常需要独立表达 Accepted、Progressing、Available 和 Degraded,且不能让一种错误覆盖掉所有其他事实。
将条件设计成列表也不自动实现正确合并。多个控制器分别负责不同条件时,需要按 type 管理自己的项,保留其他项,并解决条件写入冲突。本书实现没有多状态写入者,因此不宣称具备这样的合并协议。
删除中的状态同样重要
外部清理被阻塞时,主对象有 deletionTimestamp,finalizer 仍在,外部登记也存在。CleanupBlocked 与 ExternalUnavailable 让操作者看到删除未完成的原因。即使客户端发现删除请求已经返回成功,也能通过读取对象判断它仍处于待清理阶段。
解除阻塞后,控制器先删除登记再移除 finalizer。主对象可能很快消失,因此不能要求它再保存一份永久的 Deleted 条件。需要长期审计时,应使用事件、日志或外部审计存储,不能把即将删除的对象 status 当成唯一历史记录。
失败报告也要避免放大故障
当 API server 不可用时,控制器可能既无法更新资源,也无法写失败条件。状态因此只能反映最近成功提交的观察,不能保证实时说明所有故障。监控必须同时观察控制器进程和 API 连接,而不能只根据对象条件判定系统健康。
练习时设计一个等待脚本:只有条件 type 为 Reconciled、status 为 True,且条件 observedGeneration 等于当前 metadata.generation 才算本次提交被处理。然后让用户在等待期间再次更新 spec,观察脚本必须追踪新目标,而不能被旧的 True 条件误导。
评审案例:True 条件也可能已经过时
用户看到Reconciled=True后修改message,服务器首先保存新spec,控制器尚未执行。此时旧True仍然是合法历史结论,只是observedGeneration低于当前generation。客户端若只判断布尔值,会把“上一次已处理”错误理解为“这一次已处理”。
因此等待脚本应把版本匹配作为条件的一部分。类似地,删除时的CleanupBlocked=True描述清理障碍,而非数据面是否还在提供服务。不同事实应避免互相覆盖含义,界面也应同时显示意图版本和观察版本,让用户能看出等待发生在哪一层。
状态还可能因为控制器失联而长时间保留旧值。可以借助进程监控或显式的观察时间判断新鲜度,但不能用每秒刷新条件来伪造健康。状态接口应承认它是最近一次持久观察,实时健康则由独立信号补充。