第 4 / 23 篇

第 3 章:协调函数处理当前事实

事件是唤醒信号

控制器不能假定收到创建事件时对象一定还存在。事件在队列中等待期间,用户可能已经删除或更新对象;网络重连还可能让观察方重新列出同一批对象。因此事件只告诉程序“这个键值得再看一次”,并不授予对历史事件内容的信任。

随书 Run 方法把命名空间和名称组成队列键。worker 取出键后调用 Reconcile,第一步始终是从 API server 读取当前 GatewayInstance。NotFound 表示当前没有工作对象,可以成功结束。程序不会按照旧 Add 事件携带的 spec 重新创建已删除的实例。

这种设计把控制器从命令消费者变成状态修复器。即使创建事件和更新事件被合并,只要最终键还能被处理,程序就能根据最新目标收敛。它也解释了为何不能把“每次事件都必须执行一次”当成业务保证;事件队列的目标是推进状态,不是充当不可丢失的交易日志。

一轮协调的基本顺序

非删除路径先确保 finalizer 存在,再确保外部登记,再创建或修复 Deployment、Service,最后写状态。每一步都可能失败,前面已经完成的 API 写入不会自动回滚。下一轮必须能够识别已有结果并继续,不能因为某个对象已经存在就把整轮当作失败。

顺序对恢复尤其重要。外部登记发生前先持久化 finalizer,确保控制器崩溃后对象删除仍会保留清理机会。反过来,先创建外部对象再添加 finalizer,会留下一个危险窗口:用户在两次写入之间删除主对象,后续没有任何 API 记录要求清理外部责任。

这并不构成跨系统原子事务。本项目的“外部登记”仍是同集群 ConfigMap,用它演示没有 owner reference 的资源如何交给 finalizer 管理。真实云 API 可能存在请求超时但创建成功的情况,需要额外的幂等键和权威查询。后续章节会明确区分已经验证的 Kubernetes 行为与外部系统扩展设计。

一次只推进可重做的步骤

Reconcile 返回 nil 不意味着业务永久完成,只意味着本轮没有遇到需要重试的错误。即使资源刚刚收敛,用户或其他控制器也可能在下一秒改变它。成功后保留周期复查,是本项目补足子资源事件观察的方式。

协调函数不应长时间阻塞等待 Deployment 可用。它可以应用资源并报告 Reconciled,让数据面就绪在另一种状态或独立验证中体现。若在单 worker 内等待五分钟,其他实例的删除和修复也会停止。把一个对象的缓慢进展隔离为重新排队,比占住处理线程更公平。

随书 HTTP 实验为了得到完整证据,会等待数据面达到可验证状态;这是测试控制器的行为,不是生产 Reconcile 内部的等待逻辑。评审时要分清测试驱动程序和被测控制器,否则很容易把测试里的长轮询错误地搬进业务循环。

错误必须保留可解释性

AlreadyExists、Conflict、Forbidden 和 NotFound 并不是同一类失败。创建子资源遇到 AlreadyExists,需要读取并核对所有权;更新遇到 Conflict,需要重读后重新计算;权限不足通常需要运维修复;主对象消失则意味着不必继续应用期望状态。简单地把所有错误吞掉,会让队列认为工作已成功,用户只能看到长期不变的状态。

当前教学实现把多数错误交给队列退避重试,没有完整事件和指标系统。书中后续给出生产演进方向,但不会声称程序已经实现全部可观测性。错误信息保留对象类别和冲突原因,是这一步最基本的诊断基础。

删除是另一条状态机

一旦 deletionTimestamp 出现,程序先进入删除分支,不再创建缺失的数据面资源。删除分支只负责清理外部登记,再移除自身 finalizer;Deployment 和 Service 的关联删除由 owner references 交给垃圾回收器处理。这个分工避免控制器在用户删除实例时不断把子资源补回来。

练习时可以在调用删除后手动移除 Service,再执行 Reconcile。期望结果是删除继续推进,而不是恢复 Service。把“正在删除”作为生命周期的高优先级状态,比在每一个创建函数里额外打补丁更容易推理。

推演案例:第二步失败后怎样继续

假设 finalizer 和登记已写入,Deployment 创建成功,Service 创建因权限不足失败。此时不能把 Deployment 删除当成通用回滚:用户可能已经看到它,下一轮权限恢复后还要重新启动数据面,而且删除操作本身也可能失败。更稳定的恢复是保留可识别结果,在下一轮补齐缺失部分。

这要求每一步的成功能够从当前事实重新判断。登记通过固定名字与UID确认,Deployment通过名称与owner确认,Service同样需要归属验证。一个只有内存布尔变量的“已经完成”标记无法跨进程恢复,写进status也不能替代对实际资源的检查。

可以在纸上对每次API调用分别列出成功、明确失败、结果未知三个分支。Kubernetes名称唯一性和查询能力解决了很多恢复问题;外部接口未必具备同样语义。把这张表作为接入评审材料,有助于识别哪一步能自动重试,哪一步需要查询或人工处理,而不是把所有故障交给一个统一重试循环。