第 12 / 23 篇

第 11 章:把 Kubernetes 之外的责任带进来

外部对象没有天然垃圾回收关系

云负载均衡、DNS 记录和证书都可能由 Operator 创建,但它们不属于 Kubernetes 垃圾回收器理解的对象图。删除主资源后,平台仍需要承担撤销这些对象的责任。finalizer 提供保留清理入口的机制,真正的幂等与身份验证仍由应用协议负责。

本项目用无 owner reference 的 ConfigMap 模拟外部登记。它仍由同一个真实 API server 保存,因此不存在网络服务之间的独立故障域。我们使用它观察清理顺序、UID 绑定和卡住恢复,不能据此声称验证了云 API 的创建超时或跨系统原子性。

登记名包含主对象 UID,数据里再次保存 ownerUID。删除前核对二者,避免拿到一个被篡改或意外复用的登记就直接清理。双重身份检查也方便故障诊断:名称帮助定位,内容帮助确认归属,但它不替代真实外部系统的认证和权限控制。

最棘手的窗口是结果未知

控制器调用外部创建接口,连接在返回前中断。外部系统可能尚未执行,也可能已经成功;重试创建可能产生重复,不重试又可能永远无法收敛。Kubernetes 的 resourceVersion 只保护后续对象写入,无法回答远端是否完成副作用。

成熟接入应向外部接口传递稳定幂等键,并在超时后按该键查询权威结果。键可以包含主对象 UID 和操作类型,但不能在每轮随机生成。若远端既不幂等也不可查询,未知结果必须暴露为需要人工判断的状态,而不是靠本地日志猜测。

外部对象标识写回 status 也有提交窗口:远端创建成功后,状态更新可能失败。只根据 status 空白判断“从未创建”会导致重复。恢复路径必须能够通过稳定身份重新发现外部对象;将标识只存在内存里更无法承受进程重启。

不把重试闭包当事务

RetryOnConflict 可能执行多次。把外部创建、计费或通知放在其中,会将一个本地版本冲突变成多次远端动作。可以把可重做的对象条件写入留在闭包内,把外部协议独立成具有幂等和查询能力的步骤,再由持久状态连接各步。

这并不意味着所有操作都要建复杂工作流。有些外部接口原生支持按固定名称更新,且有明确唯一约束,可以用简单的查询和条件写入收敛。选择取决于副作用成本、结果可知性和撤销能力,而不是“Operator 必须无状态”这种口号。

无状态进程的意思是进程退出后可以从持久事实恢复,不表示系统没有状态。主对象、外部资源、幂等键和审计记录本身就是状态载体。把状态藏在某个不被承认的外部角落,只会让恢复更难推理。

安全删除需要核对归属

远端删除接口如果只接受人类名称,可能误删同名新对象。应查询远端对象元数据,核对主对象 UID 或不可变业务标识,再决定是否删除。遇到身份不一致时,保留 finalizer 并报告冲突通常比静默跳过更安全。

本项目遇到登记 ownerUID 不匹配会返回错误,不会覆盖或删除该记录。这个失败意味着需要人工介入,不能通过增加重试次数消除。将此类错误区分为身份冲突,能够帮助操作者避免把配置问题误判成短时 API 抖动。

权限撤销也属于生命周期

实际云接入还要考虑凭据失效、跨租户误用、资源所属账户变化和删除权限不足。清理者需要的权限应在资源存续期间得到维护,而不能等到删除时才发现旧凭据已撤销。运维设计应保存足够的非秘密身份信息,帮助替换凭据后继续清理。

练习是为外部 DNS 接口画出创建、查询、删除三个操作的身份与重试表。分别回答:请求超时如何查结果、同名新实例如何区分、删除权限丢失如何恢复。只有这些答案成立,finalizer 才真正承担了可恢复的外部责任。

评审案例:外部资源被人工修改

假设云控制台里的负载均衡器被改名,但UID标记仍在。控制器应该按稳定身份定位,而不是重新创建同名对象。若运维人员彻底移除归属标记,程序需要明确是拒绝接管、报告丢失,还是允许经过授权的修复,不能在未知归属下直接删除。

这种策略与Kubernetes子资源相似,但外部接口可能缺乏条件更新。先查再写存在竞争窗口,需要外部版本号、ETag或服务端幂等机制补足。如果接口不提供这些能力,应把局限写进托管契约并收紧操作权限,不能假设本地重试就能消除竞争。

随书登记的ownerUID检查演示了最小身份防线。真实系统还要记录账户、区域和资源类型,避免相同标识在不同作用域下含义不同。资源引用应结构化保存,别把多个字段简单拼接成无法消歧的字符串。