暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

CDC组件的检查点机制设计:如何保证数据不丢不重

聊CDC,绕不开一个问题:任务挂了怎么办?

数据同步不像普通应用,挂了重启就行。CDC这种场景,系统崩溃后最怕两件事:一是丢数据(该同步的没同步),二是重复数据(同一条数据同步了两次) 。前者影响数据完整性,后者可能导致下游业务逻辑混乱。

解决这个问题靠的就是检查点(Checkpoint)机制——定期记录当前同步进度,系统重启后从上次记录的位置继续,不多不少。

这篇文章不聊虚的,直接从实现层面聊聊检查点机制怎么设计,以及TLA在这个问题上是怎么做的。

一、检查点要解决什么问题?

CDC的本质是流式读取日志。系统在不停地读,下游在不停地收。如果进程突然挂了,你总得知道“读到哪了”。

但Oracle的redo log跟MySQL的binlog不太一样。MySQL binlog有明确的位点(文件名+偏移量),Oracle的redo log定位用的是SCN(System Change Number) ——全局递增的版本号。每个Change Vector都带一个SCN,代表这个变更发生的顺序。

所以检查点的核心就是:记录一个SCN值,代表“这个SCN之前的所有变更都已经处理完毕了”。

重启的时候,从这个SCN开始往后读,继续处理。

二、好的检查点机制应该满足什么条件?

从实现角度看,有几个硬性要求:

第一,原子性。 检查点要么成功,要么失败,不能处于“写了一半”的状态。如果在写入检查点的过程中系统崩溃了,重启后可能会读到一份损坏的检查点数据。Flink CDC社区就有人遇到过“检查点文件损坏”导致无法恢复的问题。所以写入操作必须是原子的,或者有校验机制能识别损坏的检查点。

第二,低开销。 检查点不能太频繁。每次做检查点都有I/O开销,频率太高会影响主链路的吞吐。但也不能太稀——间隔越大,崩溃时可能丢失的数据就越多。这个平衡需要根据业务对RPO的要求来调。

第三,恢复路径清晰。 从检查点恢复的逻辑要简单可靠。不能恢复过程中还要做复杂的判断,否则恢复本身就可能出错。

第四,与事务边界对齐。 这个最容易被忽略。检查点不能卡在事务中间——如果一个事务还没提交,你记录了检查点,那恢复的时候这个事务怎么办?TLA的做法是只在事务提交边界打检查点。每次遇到COMMIT(OP 5.4),确认这个事务的所有Change Vector都已经处理完毕,这时候才记录检查点。这样恢复的时候,从检查点SCN开始读,不会遇到“半个事务”的问题。

三、Debezium和FlinkCDC的检查点机制有什么问题?

基于LogMiner的方案在检查点这件事上有个天然的麻烦——LogMiner的检查点逻辑不受上层控制

Debezium官方文档提到,Oracle连接器通过在偏移量中跟踪SCN来实现重启续传。听起来合理对吧?但问题在于:

如果数据库变更频率很低(几小时甚至几天才有一条变更),偏移量可能会过时。 重启的时候,记录的SCN可能在日志里已经找不到了,连接器就无法成功重启。Debezium的解决方案是启用心跳机制,定期往数据库里插一条假数据来强制刷新偏移量——用一条假数据来保活,这在生产环境里总让人觉得不太踏实。

CDB/PDB多租户模式下更麻烦。Debezium在CDB模式下跟踪PDB内部的变更,需要额外配置心跳动作查询,定期在PDB里插入或更新一行数据来触发变更事件,才能保证偏移量同步。

全量阶段不支持检查点也是个大问题。 Flink CDC读取分为全量读取和增量读取两个阶段,全量阶段是不支持checkpoint的。如果全量阶段任务挂了,就要从头开始。Oracle CDC源在扫描快照期间无法执行检查点,会导致检查点等待超时,进而触发任务故障转移。

这些问题的根因都一样:LogMiner是黑盒,上层控制不了它的行为。 你只能在它外面做一些补救措施,但改不了它内部的检查点逻辑。

四、TLA的检查点机制是怎么设计的?

TLA不依赖LogMiner,直接解析redo log的二进制格式。这意味着检查点的所有逻辑都是自己控制的

核心思路:事务边界对齐 + 增量持久化。

TLA的检查点记录的信息很简单:当前已处理到的SCN + RBA(Redo Byte Address) 。RBA定位具体的日志文件和块位置,SCN保证全局顺序。

整个流程是这样的:

  1. 解析线程持续读取redo log,逐个解析Change Vector,按XID归集事务
  2. 每遇到一个COMMIT(OP 5.4),确认该事务的所有Change Vector已处理完毕
  3. 将当前SCN和RBA写入检查点存储(本地文件或外部存储)
  4. 循环继续

关键设计决策:只在COMMIT边界打检查点。

这意味着:

  • 检查点永远不会落在事务中间
  • 恢复的时候从检查点SCN开始读,不会遇到“半个事务”
  • 不需要额外的回滚或补偿逻辑

异常恢复流程:

  1. 系统启动,读取最近一次检查点记录的SCN和RBA
  2. 定位到对应的日志文件和块位置
  3. 从这个位置开始继续解析
  4. 已提交的事务正常输出,未提交的事务等待提交或回滚

检查点存储:

TLA支持将检查点写入本地文件或外部存储(如ZooKeeper、etcd)。多实例部署场景下,外部存储可以保证检查点在实例切换时仍然可用。写入采用原子操作——先写临时文件,再rename覆盖——避免写入过程中崩溃导致检查点损坏。

性能考量:

检查点的频率可以配置。默认是每个COMMIT都记录,适合对数据丢失零容忍的场景。如果追求极致性能,可以配置为每N个事务或每M秒记录一次——牺牲少量数据丢失的可能性,换取更高的吞吐。

五、跟套壳方案比,区别在哪?

对比维度 基于LogMiner的方案 TLA
检查点控制权 LogMiner黑盒,上层只能被动读取 完全自主控制
检查点粒度 依赖LogMiner内部机制,可能过时 事务边界对齐,精准可控
心跳保活 需要额外心跳机制防止偏移量过时 不需要,解析器持续读取日志
全量阶段检查点 不支持,挂了从头来 同样不支持,但全量阶段通过分片设计可部分恢复
CDB/PDB支持 配置复杂,需要额外心跳动作 直接支持,无额外配置
恢复可靠性 受LogMiner限制,可能因SCN过期无法恢复 自己控制SCN和RBA,恢复路径清晰

TLA的检查点机制本质上是把控制权从LogMiner手里拿回来。不依赖任何外部组件的内部状态,所有元数据自己维护,所有逻辑自己控制。重启的时候从检查点继续读就行,不需要心跳保活,不需要额外配置,不受LogMiner版本变化影响。

六、说句实在话

检查点机制看起来是个小功能,不就是记个位置嘛。

但真做起来,涉及的问题不少:原子性怎么保证?事务边界怎么对齐?全量阶段怎么处理?多实例部署怎么共享检查点?

套壳方案在这些问题上处处受制于LogMiner——它能给你什么,你才能用什么。它不支持全量检查点,你就只能忍着;它的偏移量会过时,你就得额外配心跳。

TLA做的事情就是把这些控制权全部拿回来。自己解析日志、自己管理SCN、自己控制检查点粒度。代码在自己手上,想怎么调就怎么调。

欢迎交流。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论