聊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保证全局顺序。
整个流程是这样的:
- 解析线程持续读取redo log,逐个解析Change Vector,按XID归集事务
- 每遇到一个COMMIT(OP 5.4),确认该事务的所有Change Vector已处理完毕
- 将当前SCN和RBA写入检查点存储(本地文件或外部存储)
- 循环继续
关键设计决策:只在COMMIT边界打检查点。
这意味着:
- 检查点永远不会落在事务中间
- 恢复的时候从检查点SCN开始读,不会遇到“半个事务”
- 不需要额外的回滚或补偿逻辑
异常恢复流程:
- 系统启动,读取最近一次检查点记录的SCN和RBA
- 定位到对应的日志文件和块位置
- 从这个位置开始继续解析
- 已提交的事务正常输出,未提交的事务等待提交或回滚
检查点存储:
TLA支持将检查点写入本地文件或外部存储(如ZooKeeper、etcd)。多实例部署场景下,外部存储可以保证检查点在实例切换时仍然可用。写入采用原子操作——先写临时文件,再rename覆盖——避免写入过程中崩溃导致检查点损坏。
性能考量:
检查点的频率可以配置。默认是每个COMMIT都记录,适合对数据丢失零容忍的场景。如果追求极致性能,可以配置为每N个事务或每M秒记录一次——牺牲少量数据丢失的可能性,换取更高的吞吐。
五、跟套壳方案比,区别在哪?
| 对比维度 | 基于LogMiner的方案 | TLA |
|---|---|---|
| 检查点控制权 | LogMiner黑盒,上层只能被动读取 | 完全自主控制 |
| 检查点粒度 | 依赖LogMiner内部机制,可能过时 | 事务边界对齐,精准可控 |
| 心跳保活 | 需要额外心跳机制防止偏移量过时 | 不需要,解析器持续读取日志 |
| 全量阶段检查点 | 不支持,挂了从头来 | 同样不支持,但全量阶段通过分片设计可部分恢复 |
| CDB/PDB支持 | 配置复杂,需要额外心跳动作 | 直接支持,无额外配置 |
| 恢复可靠性 | 受LogMiner限制,可能因SCN过期无法恢复 | 自己控制SCN和RBA,恢复路径清晰 |
TLA的检查点机制本质上是把控制权从LogMiner手里拿回来。不依赖任何外部组件的内部状态,所有元数据自己维护,所有逻辑自己控制。重启的时候从检查点继续读就行,不需要心跳保活,不需要额外配置,不受LogMiner版本变化影响。
六、说句实在话
检查点机制看起来是个小功能,不就是记个位置嘛。
但真做起来,涉及的问题不少:原子性怎么保证?事务边界怎么对齐?全量阶段怎么处理?多实例部署怎么共享检查点?
套壳方案在这些问题上处处受制于LogMiner——它能给你什么,你才能用什么。它不支持全量检查点,你就只能忍着;它的偏移量会过时,你就得额外配心跳。
TLA做的事情就是把这些控制权全部拿回来。自己解析日志、自己管理SCN、自己控制检查点粒度。代码在自己手上,想怎么调就怎么调。
欢迎交流。




