第9章 位点前崩溃:用真实进程验证恢复
把故障放在明确的提交窗口
测试消费者恢复时,随便启动进程再立刻 kill,可能只杀在初始化阶段。这样的绿色测试不能证明最危险的窗口已经发生。我们为实验增加一个受控屏障:消费者成功提交一条 PostgreSQL 事务后,尚未调用 Kafka commit,写出包含 topic、partition、offset 的标记文件,然后阻塞等待。
控制程序不是根据固定睡眠猜测进程走到哪里,而是等待标记出现。它进一步查询 PostgreSQL,要求 crash 消费组的交付历史恰好一条;再通过 Kafka 客户端查询该组实际提交位点,要求未提交,或下一个位置尚未越过当前记录。两个存储的观测共同确认故障窗口真实存在。
此处标记文件只负责实验同步,不是生产检查点,也不参与消费者恢复。生产逻辑仍以 broker 位点和数据库历史为依据。把测试控制面与运行时状态分开,才能避免因为测试注入了一份额外真相,程序才偶然恢复成功。
SIGKILL 为什么有意义
控制程序向真实子进程发送 SIGKILL,并检查退出码对应该信号。进程无法执行 finally 或优雅关闭逻辑,因此不会在退出时补交 Kafka 位点。这个实验比在函数中抛异常更接近突然进程消失,但仍然不等价于数据库断电、磁盘损坏或主机内核故障。数据库服务和 broker 在整个实验中继续运行。
当前实现选择新的 crash 消费组和 crash-gen 世代,从数据流起点读取。这样首条事件一定能触发故障窗口,也不会污染在线 live 投影。控制程序确认数据库已经持久化一条事件后杀进程,再以同样组和世代重启普通消费者。重启必须至少报告一次重复,然后完成剩余数据。
预期结果是重复,不是丢失
Kafka 组位点尚未确认,所以首条记录会再次投递。delivery_history 已有同组同坐标,消费者识别重复,不再应用业务变化;数据库检查点仍可推进,随后位点提交成功。最终 crash-gen 投影与源表逐行相等,这同时检查重复记录和后续记录没有被错误跳过。
如果恢复后没有重复,可能是测试屏障位置错了,也可能是位点被自动提交。我们显式关闭 enable.auto.commit,并关闭自动 offset store,提交由业务事务成功后的代码控制。测试不仅检查最终状态,还检查 broker 位点与重复计数,就是为了避免一种错误实现恰好得到相同结果却没有经过预期路径。
同步提交与吞吐取舍
每条记录都执行数据库事务和同步 Kafka commit,吞吐会受到往返延迟限制。这里优先让故障顺序易于观察。生产可以把同分区连续记录批量放进一个数据库事务,再提交批次末尾位点,但必须确保批次内全部记录都已持久化,失败时不能越过未完成对象。
并发消费者还需要处理分区撤销。若一个线程仍在写旧分区,另一个线程已经提交该分区的新位置,就可能违反预期顺序。当前教学程序单线程轮询与处理,不声称验证了多线程 rebalance 协议。扩展并发前应补充真实分区转移和处理中撤销的故障实验。
恢复证据应该能回答什么
crash 标记说明被杀时处理了哪条记录;数据库查询说明事务已经提交;broker 查询说明位点尚未确认;进程退出码说明没有优雅收尾;重启报告说明发生了重复;完整对账说明最终内容收敛。这些证据各自覆盖不同断言,不能用一张“测试通过”截图代替。
还有一个常见误区:把下一个位点与最后处理位点直接相等比较。Kafka commit(message=msg) 提交的是这条消息之后的位置,因此记录 offset 为零时,成功确认通常意味着 next offset 为一。故障断言使用“没有越过当前记录”的关系,避免在边界处把已提交误判为未提交。
本章练习
把屏障移动到 Kafka commit 之后,预测 broker 位点、重启重复数和最终对账会怎样变化。再说明为什么两种实验都可能最终相等,但只有当前窗口能验证数据库已提交而 broker 尚未确认的重复恢复路径。