第 13 / 23 篇

第 12 章:进程重启后从事实恢复

不把内存当唯一真相

队列、缓存和局部变量都在控制器进程退出时消失。一个可恢复的实现必须能够重新列出主对象,读取现有子资源与外部登记,再继续协调。若某一步只在 Add 事件回调执行一次,进程停机期间创建的对象就可能永久漏处理。

随书 CLI 启动 informer,等待同步,再处理初始 list 产生的键。Reconcile 根据服务器当前对象判断是否需要创建或更新,不依赖过去执行过哪些函数。外部登记和 finalizer 都持久存在,因此重新启动的进程能够分辨正常资源、待删除资源和已完成的中间步骤。

这里的恢复来源是服务器实际保存的事实,不是恢复旧内存快照。把整个队列写到磁盘通常没有必要,因为它可能含有已经过时的键;更重要的是重新发现所有仍有责任的对象,并让每个步骤可重入。

真正终止一个程序

进程重启测试会编译 cmd/operator 为独立二进制,启动它管理一个测试命名空间,等待 Deployment 出现,然后用操作系统真实终止进程。控制器停止期间,测试把主对象副本数改为三。重新启动新的 CLI 进程后,最终 Deployment 必须达到三副本。

这个测试与在同一进程中重新调用构造函数不同。它丢弃了旧队列、旧 informer 和所有局部对象,实际经历了启动与缓存同步路径。测试保存子进程输出,并在清理时终止自己创建的进程,不影响其他实验任务。

它仍没有验证节点宕机、API server 数据丢失或多个控制器竞争。SIGKILL 终止应用进程,不等于集群断电;新进程能够读取完整 API 数据,也不意味着 etcd 备份恢复已经得到证明。把故障注入的层级标清楚,读者才能正确复用结论。

部分完成的现场

如果进程在添加 finalizer 后、创建 Deployment 前退出,新进程会看到已有 finalizer 并继续。若在创建 Deployment 后、创建 Service 前退出,则保留同一 Deployment 并补齐 Service。若在删除外部登记后、移除 finalizer 前退出,下一轮将 NotFound 视为清理完成,再移除 finalizer。

这些路径之所以能恢复,是因为每一步都有稳定身份和可重复判断。它们不是依赖某个全局“事务成功”标志。Kubernetes API 的单对象条件更新很强,但多对象流程仍需要通过可重入步骤连接。

本项目没有为每条指令都注入进程崩溃;真实进程测试覆盖停机期间更新和启动重发现,代码与删除测试共同说明其他重入路径。更严格的生产验证可以加入每个持久化边界的进程门闩,逐一保留原始证据,不能把当前覆盖范围描述为全指令故障证明。

优雅退出与强制终止

CLI 监听 SIGINT 和 SIGTERM,通过 context 结束 informer 和队列。理想情况下 worker 停止取得新键,正在进行的 API 请求接受取消。退出过程不能把完成了一半的资源当作需要手工回滚的异常,下一进程应能继续。

现实运维还需要设置终止宽限时间,避免无限等待不可取消的依赖。优雅退出改善重复工作和日志完整性,但不能作为正确性的唯一基础;容器也可能被直接杀死。控制器应该在不优雅退出后仍保持持久责任可恢复。

重启期间用户看到什么

控制器停机时,已运行 Envoy 通常继续处理请求,但新 spec 不会被推进,删除也可能等待 finalizer。用户需要分别观察控制平面和数据面。将控制器进程健康误当成代理可用,或把代理仍可服务误当成控制器正常,都会掩盖故障。

练习是停止控制器后删除 Service,再创建一个新的 GatewayInstance,确认旧实例的修复和新实例的创建都会在重新启动后发生。记录恢复时延来自 informer 初始 list、队列排队还是子资源启动,而不是只给出一个总秒数。

评审案例:重启后不应丢掉删除责任

进程停机期间用户删除一个带finalizer的实例。重新启动后,初始list必须包含仍在删除的对象,并使它进入清理分支。如果观察代码过滤掉所有有deletionTimestamp的对象,正常创建测试仍会成功,删除却永远无法恢复。

因此“只处理活跃对象”的优化必须谨慎。生命周期中的待删除也是活跃责任,只是目标从创建资源变为消除外部影响。生产恢复演练可以在清理阻塞时终止进程,恢复依赖后启动新进程,检查它完成相同责任,进一步扩展当前测试。

同样,升级程序后不能轻易更名finalizer。旧对象仍携带旧名称,新程序若只认识新名称,就会遗留无法处理的删除入口。变更清理协议时应保留兼容处理,并在迁移完成前同时识别历史标记。