第2章 日志边界:理解 CDC 能证明什么
为什么从日志读取
定时查询 updated_at 看起来很简单:记住上次执行时间,下一轮读取更晚的行。这个方案在时间精度、并发提交和删除上都有边界。一个事务较早生成更新时间,却较晚提交,可能落到已经推进的时间窗口之前。物理删除的行不再存在,更无法通过普通增量查询发现。业务程序若漏写更新时间,同步程序也无法补救。
MySQL 二进制日志描述已提交变更。Debezium 的 MySQL 连接器读取日志并形成变化事件,使我们不必在每个业务写入点维护另一条发送逻辑。代价是同步系统开始依赖复制配置、日志保留、连接器位点和表结构历史。CDC 不是绕过数据库一致性的捷径,而是选择一种更接近事务提交边界的变更来源。
本实验明确要求 ROW 格式与 FULL 行镜像。行格式记录行变化,完整镜像有助于把事件中的 before 和 after 与业务对象对应。此处不是所有 MySQL 模式的比较评测;如果生产使用不同配置,需要检查连接器兼容性、数据库负载与字段可用性。不要因为实验容器能启动,就推断任意已有实例都满足接入要求。
三种位置不能混为一谈
第一种是源日志位置,例如 binlog 文件与文件内位置,也可能涉及 GTID 集合。第二种是 Kafka 的 topic、partition 和 offset。第三种是消费者组已经提交的下一个消费位置。它们关联同一条数据的旅程,却属于不同系统的命名空间。Kafka offset 为一千,既不代表 MySQL 有一千个事务,也不表示这一千条消息都已经进入投影。
连接器还维护自己的源位点与 schema 历史。源位点帮助它知道从哪里继续读取,schema 历史帮助它解释历史日志中的字段布局。只保留数据 topic,却随意删除连接器内部状态,再声称可以从原点无损续读是不成立的。重建可以使用新连接器和新 topic 重新快照,但那是另一条明确的恢复路径。
本书报告保存消息 source 中的真实文件名与位置,用来证明事件来自连接器读取的 MySQL 日志。报告只取少量样例防止体积失控,完整正确性依据是实际消费结果和全量对账。样例中的位置不能被当成整条链路的全局事务序列号,也不能跨不同源实例直接比较大小。
事务与事件的关系
一次源事务可以修改多行,连接器通常产生多条行级事件。消费者逐条更新查询投影时,读者可能短暂看到该事务的一部分变化。单个投影写入事务只保证当前处理单元原子,不会自动把所有源事务重新聚合。若业务要求多个对象一起可见,需要使用事务元数据、缓冲和边界提交,或者把必要不变量放在源库查询中。
同样,Kafka 的事务能力有适用边界。它可以约束支持事务的 Kafka 写入和消费位点组合,不能自动把任意 PostgreSQL 事务加入同一个原子提交。本文采用明确的顺序:先提交投影,再提交 broker 位点。故障窗口产生重复投递,通过幂等处理收敛。这是至少一次交付下的设计,不叫端到端恰好一次。
日志留存是一项恢复预算
消费者停止并不等于连接器停止。前者可能只在 Kafka 形成积压,后者可能导致 MySQL binlog 留存成为瓶颈。如果连接器所需日志已被清理,重新启动的任务不能凭空补出缺口。必须选择重新快照、从备份恢复或其他经过验证的补数方式。配置一天日志留存不是承诺能够容忍任何一天故障,还要计算磁盘容量、写入峰值与实际清理策略。
本实验把 binlog 过期设置为一天以便清楚展示配置边界,但没有等待一天制造日志丢失,因此不会宣称验证了日志缺口恢复。我们验证的是日志仍可读取条件下的快照与持续增量。生产演练应增加日志清理后的失败识别,并验证报警和重建决策,而不是让运维脚本盲目重试到超时。
回放需要保留解释能力
一份原始日志只有在知道当时表结构和转换规则时才容易解释。数据库新增列、连接器升级或字段编码调整,都可能改变同样字节对应的含义。恢复资料应当包含输入版本、schema历史与转换代码,而不仅是一个offset数字。日志备份和应用版本管理因此需要协作:保存消息却丢失可运行的旧消费者,会使历史回放变成临时考古;保存旧代码却没有所需日志,同样无法恢复过去状态。
本章练习
画出一条消息经过的五个提交边界,分别标记由哪个系统保存位置。假设 Kafka 已有事件而投影没有更新,列出至少三种可能原因,并说明每种原因需要读取什么证据来区分。