第 14 / 23 篇

第13章 隔离重建:为查询副本建立新世代

为什么恢复不能只有续读

消费位点仍然存在,未必说明当前投影值得继续使用。早期消费者可能遗漏删除,字段转换可能有错误,运维也可能误改查询数据。继续消费只能应用未来变化,无法自动修复所有历史偏差。可靠系统需要一条从权威源重新生成查询状态的路径,使正确性不依赖对旧故障的完整猜测。

重建不应直接清空在线投影。清空后慢慢补数据会让用户看到大量缺失,失败时也没有可回退版本。我们使用 generation 将新旧投影隔离:live 继续保留,新世代 new-generation 接受另一条完整快照。两个世代共享数据库实例,但主键命名空间不同,因此重建失败不会删除在线查询对象。

新连接器、新流与新消费组

实验创建 book-rebuild 连接器,使用不同的复制 server id、topic 前缀和 schema 历史 topic。它对当前源表执行真实 initial 快照,产生 rebuild.inventory.products。新的 rebuild 消费组写入 new-generation,而不是复用 main 组和旧历史。这样新快照不会被旧事件去重状态挡住。

绑定表把每个世代固定到一个消费组和一个 topic。若读者尝试用 main 为新世代回放,程序启动时明确拒绝。这不是限制重建能力,而是要求重建带着新的输入身份发生。Kafka offset 是流内位置,不是能够跨 topic 复用的业务版本。

重建的验收顺序

首先要求新连接器确实产生快照读事件,数量等于当前源表行数。然后读取源库全部字段,与新世代未删除文档逐行比较。最后比较 live 与 new-generation 的查询集合,要求旧世代仍保持完整且两者相等。连接器与任务状态也必须都为 RUNNING,避免刚好读完部分数据后任务已经失败却未被发现。

这里的源写入在重建阶段保持停止,因此完整比较拥有稳定参照。实验验证了独立全量重建、schema 默认值和旧世代保留,没有实现持续写入期间的无缝原子切换。标题中的可重建索引强调数据派生与验证能力,具体查询存储为 PostgreSQL,不涉及外部搜索引擎 alias 的原子性。

持续写入时还需要什么

生产重建通常要在新快照后继续追增量,直到新世代达到明确切换水位。切换动作本身也需要一致性:可以让查询配置指向一个世代,变更时原子替换指针,或者使用查询引擎提供的别名能力。每种实现都要验证正在进行的请求、缓存与回滚路径,而不是只更换一条环境变量。

若旧世代与新世代使用不同转换逻辑,不能要求它们字节完全相同,而应根据新契约进行对账。例如新增字段、删除敏感内容或改变索引表示时,应当把差异写成明确的转换规则。没有规则的差异是故障,有规则的差异仍需真实验收,不能笼统称为“版本升级所以不同”。

回滚与回收是两个时刻

新世代通过对账后,也不宜立即删除旧世代。保留一段观察期可以在查询性能或业务语义出现问题时快速回退,但要限制旧世代的使用资格,避免之后误切回过时数据。回滚若要保持新写入可见,需要旧世代继续追流,或者接受明确的数据延迟边界。

最终回收旧世代之前,应检查查询指针、后台任务、缓存和导出程序都不再引用它,再清理对应消费组与恢复元数据。清理某一张文档表并不自动释放 Kafka 组状态和历史表空间。当前实验只在隔离容器结束时整体清理,没有提供生产世代回收调度器。

重建也要考虑源库压力

重建连接器会再次扫描源表,并与在线连接器共同消耗资源。大型业务不能因为查询副本“可丢弃”就无限重复重建。需要估算扫描时间、额外日志生成、快照事务存活、网络带宽与投影写入成本。失败后从头重建是否可接受,也应当在服务恢复目标中有明确答案。

本书通过两千行和完整比较建立方法,不提供大表重建耗时承诺。生产可分租户、分范围建立世代,或者选择增量快照,但每次拆分都要保留集合覆盖和删除处理的证明。只把全表拆成多个任务,没有共同水位,可能形成一个从未真实存在过的混合视图。

重建完成后还要验证读取路径

数据在新世代中正确,并不代表用户已经读到它。查询配置可能仍然指向旧世代,缓存可能持有旧结果,导出任务也可能绕过统一入口。生产切换验收应使用实际查询接口读取已知新增、更新和删除对象,再核对世代标识。当前实验只比较数据库文档,没有一个生产商品查询服务,因此不会把数据库内的新世代相等称为端到端线上切换已经完成。

本章练习

为持续写入的商品系统设计世代切换协议,写出快照完成、增量追平、验收、指针切换、观察与回收六个边界。再指出本书哪些边界已经有真实实验,哪些仍需要你新增验证。