第 7 / 23 篇

第6章 快照与增量衔接:把水位放回命名空间

一个位置需要带上身份

“当前位点是一千”几乎没有诊断价值。至少应当知道它属于哪个源实例、哪个日志文件,或者哪个 topic 的哪个分区。消费者的 next_offset 又表示下一条待读位置,和最后应用的记录 offset 差一。对这些概念没有统一约定,最常见的结果就是恢复时跳过一条或反复重放一条。

本书的投影行保存 source_partition 与 source_offset,但这里的 source 指 Kafka 交付来源,并不是 MySQL binlog 原始坐标。字段命名只是局部实现,阅读代码时必须看清它存的实际值。MySQL 位置保留在 Debezium 消息的 source 中,不能与这两个整数混用。生产中更好的命名可以明确使用 delivery_partition 和 delivery_offset,减少误解。

从快照集合到变化日志

快照给出一个受一致性边界约束的数据集合,日志描述其前后的变更。正确衔接不是简单地“先查全表,再从当前日志末尾开始”,因为两步之间可能已经发生业务提交。连接器负责建立快照与日志位置的关系,我们负责保留其状态、消费结果并验证最终集合。

如果自行编写初始化程序,先读表再查询日志位置,读取期间的更新可能丢失;先记录位置再读表,又需要处理重复和快照读到新值后旧日志回放的问题。两种顺序都需要严密协议,不能仅靠一个时间戳补洞。使用成熟连接器并不免除理解这些边界,只是避免在教学实验里重新实现复制算法。

为什么分区顺序只解决一部分问题

Kafka 只保证分区内顺序。我们为数据 topic 使用一个分区,并保持连接器产生的主键作为记录键。消费者依次处理,因此同一商品的快照、修改和删除具有可用的交付顺序。投影再加上 offset 条件,防止显式重放旧记录造成倒退。

增加分区后,同一主键仍需稳定路由,但分区数变化可能使之后的键落到不同分区。本书条件更新只接受同一分区中的更高 offset,因此跨分区迁移不是自动支持的在线操作。直接把分区数调大可能使对象停止更新或产生顺序歧义。生产迁移可以建立新 topic 和新世代重建,再以明确切换协议替换旧流。

多个源表还带来关联问题。商品表与分类表分别更新时,分区顺序不能自动给出跨表一致视图。需要选择反范式事件、事务边界聚合、查询时关联或者接受暂时不一致。本书只验证单表投影,不把单表结果延伸成跨表事务保证。

什么时候可以说追平

在持续写入的系统里,读取到当前 Kafka 末尾只能说明赶上某次观察到的末尾,新消息仍可能正在源库或连接器缓冲中。业务验收最好使用带身份的探针:在源库提交一个可识别变更,等待投影观察到同一业务修订,再判断相关读取路径。全局追平需要更完整的水位协议,不能只看 lag 等于零。

实验使用更强但范围更小的条件:一批源写入结束,连接器和消费者处理后,对整个有限集合逐行比较。对账时没有其他业务写入,因此源表与投影有共同稳定参照。这个条件适合验证正确性,不能替代生产持续变化条件下的延迟服务目标。

位点持久化与恢复动作要成对

连接器位点存储在 Kafka Connect 内部 topic 中,消费者位点保存在消费组状态中,数据库检查点保存在 PostgreSQL。恢复手册必须明确哪一个是恢复依据,哪一个只是诊断证据。本书消费者从 Kafka 组位点恢复,数据库历史表确保重投安全;不会因为数据库检查点更大就直接跨过未确认消息。

如果消费者位点被手工推进而投影没有对应内容,程序可能永远看不到被跳过的记录。若位点回退,则会产生更多重复,但当前幂等设计能够处理已保留的历史范围。两个动作风险不对称,恢复时应优先保留原始位点和证据,再选择回放或重建,避免用一次不可追踪的 reset 掩盖问题。

不要用墙上时钟替代日志顺序

两个消息的事件时间可能相同,机器时钟也可能调整。即使时间戳精确到微秒,也不自动具有分区内交付位置的唯一性。业务修订号可以表达某个对象的变化先后,但前提是源应用真正维护它。当前实现选择真实Kafka坐标处理交付身份,选择完整源行定义业务内容,各自承担能够证明的范围。把所有顺序都压成updated_at,通常只是把复杂性藏到下一次边界故障里。

本章练习

给定两个分区分别位于 offset 十和二十,说明为什么不能声称全局水位为二十。再列出把分区数从一改为三之前必须验证的键路由、旧数据、投影条件和切换步骤。