讲讲Flink-CDC的数据同步流程
Flink CDC(Change Data Capture)的数据读取原理经历了从 1.x 版本(基于锁、单并发) 到 2.x/3.x 版本(无锁、并发读取、支持断点续传) 的重大演进。其当前最核心的底层数据读取原理,是基于 Flink 社区的 FLIP-27 架构 以及借鉴 Netflix DBLog 算法设计的 无锁增量快照算法(Lock-free Incremental Snapshot Algorithm)。
下面将从架构设计、读取阶段、核心无锁算法以及状态恢复机制四个维度,详细解析 Flink CDC 的数据读取原理。
一、 整体架构:FLIP-27 读写分离模型
从 2.0 版本开始,Flink CDC 抛弃了传统的单线程 SourceFunction 架构,全面基于 Flink 的 FLIP-27 规范进行重构。该架构将“工作发现(元数据切分)”与“实际数据读取”进行解耦,核心由两个组件协同工作:
- SplitEnumerator(分片协调器):
- 运行在 JobManager 上,属于单线程组件。
- 负责监控物理表、提取表结构信息,并将整张大表按照主键范围切分成多个小数据块,称为 Split(分片) 或 Chunk(数据块)。
- 负责将这些 Chunk 动态分配给空闲的
SourceReader执行。
- SourceReader(数据读取器):
- 运行在 TaskManager 上,多实例并行运行(支持水平扩展并发度)。
- 从
SplitEnumerator接收分配的 Chunk,与源数据库建立连接,读取该分片内的历史存量数据,并在全量读取结束后无缝切入增量读取。
二、 数据读取的两个核心阶段
Flink CDC 对一张表的完整同步过程分为两个阶段:全量快照(Snapshot)阶段 与 增量日志(Binlog)阶段。
[全量快照阶段 (多并发并发读取)]
Table -> Chunk 1, Chunk 2, ..., Chunk N ──> 并行 SELECT 扫描数据 (结合低/高水位线修正)
│
▼
[增量日志阶段 (单并发持续读取)]
所有全量 Chunk 消费完毕 ──> 启动单并发 Reader ──> 从所有 Chunk 的最小高水位线(HW)开始,持续消费 Binlog
1. 全量快照阶段(Snapshot Phase)
在此阶段,Flink CDC 会将历史存量数据并行导出。为了避免对数据库加锁,Flink CDC 采用并发分块读取机制。
- 分片划分(Chunk Splitting):
SplitEnumerator获取表的主键范围(Min PK 至 Max PK),并结合用户配置的步长(chunk.size)将主键区间动态切分为多个左闭右开的 Chunk(例如:[1, 1000)、[1000, 2000)等)。 - 并行读取:多个
SourceReader并行认领这些 Chunk,并通过执行普通的 SQL 查询(如SELECT * FROM table WHERE pk >= 1000 AND pk < 2000)来导出历史存量数据。
2. 增量日志阶段(Binlog Phase)
在所有的全量 Chunk 被并行读取完毕后,SplitEnumerator 会将这些分片的元数据收回,并重新组装成一个全局的 BinlogSplit,分配给某一个特定的 SourceReader。
- 此时,任务会自动切换为单并发模式,该 Reader 与数据库建立 Binlog 复制连接,开始持续、实时地拉取数据库的增量变更日志。
三、 核心:无锁增量快照算法(实现“不锁表”与“恰好一次”)
由于在全量阶段多并发读取历史数据的过程中,业务系统仍在对数据库执行写入、修改和删除操作,这就无法直接保证读取出来的快照与后续的 Binlog 保持一致。
为了不使用全局读锁(FLUSH TABLES WITH READ LOCK)就能保证全量与增量阶段数据的“恰好一次(Exactly-Once)”一致性,Flink CDC 实现了无锁打点与合并算法:
对于每一个被分配的 Chunk,单个 SourceReader 执行以下读取步骤:
时间线 ───────────────────────────────────────────────────────────────>
│ │ │
[1. 记录低水位线 LW] [2. SELECT 扫描存量] [3. 记录高水位线 HW]
│ │ │
└─────── 捕获该期间产生的 Binlog [LW, HW] ──────────────┘
│
▼
[4. 数据合并与修正 (Normalize)]
│
▼
[5. 输出最终快照数据]
- 记录低水位线(Low Watermark, LW):
在开始读取该 Chunk 历史数据之前,Reader 首先向数据库查询当前最新的 Binlog 位点,并将其记录为低水位线 。 - 扫描存量数据:
Reader 执行SELECT查询读取当前分片主键区间内的所有历史数据,并在内存中缓存这些数据。此时读取到的数据可能是脏数据(因为在SELECT执行期间,数据可能随时被其他业务事务修改)。 - 记录高水位线(High Watermark, HW):
在SELECT查询执行完毕后,Reader 再次向数据库查询当前的最新 Binlog 位点,并将其记录为高水位线 。 - 捕获区间变更 Binlog:
Reader 读取在低水位线与高水位线区间 之间产生的所有涉及当前 Chunk 主键范围内的 Binlog 变更日志。 - 数据合并与修正(Normalize):
这是保证Exactly-Once的核心。Reader 会将步骤 2 缓存的存量快照数据与步骤 4 捕获到的 Binlog 变更流进行对齐和修正:- 若存量数据中的某条记录在 Binlog 中存在
UPDATE事件,则将该记录更新为最新的修改值。 - 若某条记录在 Binlog 中存在
DELETE事件,则将该记录从快照中移除。 - 若 Binlog 中产生了新的
INSERT,则将其加入。
经过修正后的快照,其数据状态完美等价于数据库在 (高水位线)时刻该主键区间内的真实物理快照。
- 若存量数据中的某条记录在 Binlog 中存在
- 下发与切换:
当所有 Chunk 均按照上述流程处理完毕并下发后,Flink CDC 会开始从所有已完成 Chunk 中最小的高水位线 开始消费增量 Binlog,在此位点之前已被快照合并过的 Binlog 事件将被自动过滤掉,从而确保了全量与增量切换时的无锁、无缝、且不丢不重。
四、 状态恢复与全量阶段 Checkpoint 支持
得益于 FLIP-27 架构,Flink CDC 在全量读取阶段和增量阶段均支持完整的 Checkpoint:
- 在全量读取阶段,
SplitEnumerator会在状态中持久化保存当前“待分配”、“正在消费”以及“已消费完成”的 Chunk 列表。 SourceReader在每次成功读取完一个 Chunk 并完成打点修正后,也会将该 Chunk 的完成状态反馈给协调器。- 一旦作业中途崩溃,Flink 会从上一个成功的快照点恢复,
SplitEnumerator只需要重新分配那些在快照点时仍处于“未完成”或“正在消费”状态的 Chunk,已完成的 Chunk 无需重新读取。这彻底解决了大表同步时中途失败必须“重头再来”的痛点。