第 10 章:删除是一段需要恢复的流程
删除请求成功之后
有 finalizer 的对象收到删除请求后,API server 设置 deletionTimestamp,但保留对象,等待清理者移除相应 finalizer。客户端收到成功响应,不代表外部责任已经消失。它只表示删除流程已经被接受,后续可能因依赖不可用而停留很久。
本项目在非删除路径最前面持久化自身 finalizer,再创建没有 owner reference 的外部登记 ConfigMap。这样清理责任先于副作用出现,进程崩溃后仍能通过主对象恢复。若创建登记后才添加 finalizer,用户恰好在两次写入之间删除对象,就可能没有任何记录要求清理登记。
finalizer 是责任声明,不是工作进程的锁。它不会自动调用清理代码,也不会保证只有一个清理者活动。程序仍要使用稳定身份、幂等删除和明确错误处理,并确保只移除自己管理的那一项。
正常清理的顺序
删除分支读取外部登记,核对 ownerUID 与当前主对象 UID 一致,然后删除登记。NotFound 可以视为清理已完成,因为前一次调用可能已经成功。随后重新读取主对象,在冲突重试闭包中移除自己的 finalizer,再让对象真正删除。
若主对象还存在其他团队添加的 finalizer,我们必须保留它们。把 finalizers 整个置空,会绕过其他清理责任。当前代码逐项过滤自己的名称,正是为了保持这一边界。即使这样,也不能根据自己移除成功就断言主对象马上消失,因为其他清理者可能尚未完成。
Deployment 和 Service 不在手工外部清理路径里,它们通过 owner reference 交给垃圾回收器。测试分别检查登记已经不存在,以及主对象删除后子资源最终被回收。把这两种断言分开,可以发现“主对象删了但外部责任遗漏”的错误。
有意卡住,再证明能恢复
真实集群测试通过专用 cleanup-blocked 注解模拟外部服务不可用。程序保留 finalizer,并写 CleanupBlocked 条件。测试确认 deletionTimestamp 存在、登记仍在、对象没有提前删除,再移除阻塞注解并重新协调,最后确认清理完成。
这里模拟的是应用层依赖失败,没有伪造 Kubernetes API。它证明控制流能保留责任并在条件解除后继续,但没有模拟真实云服务的连接超时、权限吊销或结果未知。这些问题要用真实外部接口和专门故障实验继续验证,不能把一条注解开关扩大为完整灾难恢复证据。
测试开关不应该未经限制进入生产 API。真实产品若需要运维暂停删除,应设计带权限控制和审计的流程。教学注解的价值是确定性制造清理窗口,方便阅读和复现,不是建议任何租户都能阻塞平台清理。
手工移除 finalizer 的代价
面对卡住的对象,直接 patch 掉 finalizer 很诱人。这样对象可能立即消失,但外部资源、账单或权限仍存在,而且控制器丢失最主要的清理入口。操作手册应先确认外部责任,保存 UID 和资源标识,执行补偿,再决定是否移除。
如果确实需要强制删除,要把“对象已从 Kubernetes 移除”和“业务责任已完成”作为两个结论分别记录。前者可以由 API GET NotFound 证明,后者需要外部系统证据。一个绿色 kubectl 命令不能把两者合并。
删除不能与自愈互相打架
进入删除分支后,控制器停止创建缺失 Deployment 和 Service。否则用户删子资源、控制器补资源、垃圾回收再删除,可能形成噪声和延迟。生命周期状态应该先于日常配置修复判断,使删除成为明确的单向流程。
练习时在对象已标记删除且清理被阻塞的期间,手工删掉 Service,再运行协调。观察 Service 不会被补回,而登记和 finalizer 仍保留。这个场景检验的不是正常创建能力,而是程序是否尊重用户已经表达的终止意图。
评审案例:清理后再写状态失败
登记删除成功后,移除finalizer的Update因为竞争失败。对象仍保留,下一轮再次GET登记得到NotFound,应继续移除finalizer。若把NotFound当成严重错误,清理流程反而会永久卡在已经成功的动作上。
另一方面,若登记存在但ownerUID不匹配,就不能把它视作等价的已清理结果。身份冲突和不存在是两种不同事实:前者需要调查,后者通常表示本次责任已完成。错误分类直接决定恢复安全性,不能只根据HTTP状态是否属于四百范围统一处理。
对多个外部对象,清理顺序还需要考虑依赖。例如先撤销公网入口再删除后端,可避免入口继续指向无效目标。每一步都应可重复,并保存足够身份。finalizer只保留入口,不替我们决定这张依赖图。