当使用 Flink CDC 任务进行 Savepoint 备份并重新启动时,它是如何准确定位到正确的 Binlog 偏移量(Offset)并实现断点续传的?
当 Flink CDC 任务通过 Savepoint 挂起并重新启动时,它能够实现“断点续传”且不丢失、不重复数据的核心在于 Flink 的状态管理机制(State Backend)、FLIP-27 统一数据源框架(FLIP-27 Source Framework),以及 Chandy-Lamport 分布式快照算法的协同工作。
以下是 Flink CDC 实现准确定位并恢复 Binlog 偏移量(Offset)的具体工作机制:
一、 核心状态数据的持久化
基于 Flink 的 FLIP-27 架构,Flink CDC(以常用的 MySQL CDC 为例)将整个读取任务拆分为两部分,并分别管理它们的状态:
- SourceCoordinator / Enumerator(协调器):
- 负责监控物理表的变化,并将要读取的表划分为多个分片(Splits),分发给多个 Reader 算子。
- 它在状态中维护了
PendingSplitsState,即那些尚未分配或尚未读取完的分片信息。
- SourceReader(读取器):
- 负责接收分片并执行实际的数据读取工作。
- 运行期间,每个 Reader 维护了一个
MySqlSplitState,其中包含了当前正在处理的分片及其读取进度(如果是 Binlog 阶段,则为一个全局的BinlogSplit)。
保存的内容是什么?
当触发 Savepoint 时,Flink 会将上述两个组件的状态写到持久化存储(如 HDFS、S3)中:
- 如果任务处于“全量快照(Snapshot)阶段”:状态中会保存所有还未开始读取的主键区间 Chunk、正在读取的 Chunk 的范围(Low Watermark 与 High Watermark)以及当前读取到的主键位置。
- 如果任务处于“增量 Binlog 阶段”:此时通常只有单并发的 Reader 在读取全局 Binlog。该 Reader 会在本地状态中持续更新当前的
BinlogOffset。- 该
BinlogOffset包含了诸如:当前的 Binlog 文件名(file)、日志位置(pos)、已消费的 GTID 集合(gtids)以及事件时间戳等。
- 该
二、 Savepoint 恢复与断点续传的执行流程
当指定 Savepoint 重启 Flink 任务时,底层的恢复和对齐逻辑如下:
1. 状态恢复与覆盖(Override)
当 Flink 任务带着 -s <savepointPath> 参数启动时,Flink 的 JobManager 会读取 Savepoint 元数据,并将存储的算子状态重新灌入对应的 SourceEnumerator 和 SourceReader 中。
- 此时,用户在 SQL 或 DataStream 中配置的
scan.startup.mode(例如默认的initial)将会被忽略。 - 框架会检测到状态中已经存在历史恢复的 Split 状态信息,并自动切换为状态恢复模式。
2. 解析与提取位点(Offset Extraction)
- 如果恢复的状态显示任务此前已进入 Binlog 阶段,
SourceEnumerator将会重新将保存着历史位点的MySqlBinlogSplit分配给对应的 Reader 节点。 - Reader 节点从接收到的
BinlogSplit状态对象中反序列化出上一次成功 Checkpoint/Savepoint 时保存的BinlogOffset(比如mysql-bin.000121, pos: 540212或对应的 GTID)。
3. 重新向数据库请求数据流(Re-establish Streaming)
- 负责读取 Binlog 的 Reader 拿到上述位点信息后,通过底层连接器(基于改进的 Debezium 引擎)与 MySQL 数据库重新建立连接。
- 连接器向数据库发送类似
COM_BINLOG_DUMP或COM_BINLOG_DUMP_GTID的复制请求,并明确指定要从 Savepoint 中记录的file、pos或GTID开始读取。 - 数据库接收到请求后,便会从该指定位置开始,继续向 Flink CDC 推送增量 Binlog 变更流。
三、 如何保证“精确一次”(Exactly-Once)一致性?
仅仅恢复到正确的位点还不够,在 Savepoint 触发的瞬间,可能仍有部分数据在网络传输中或处于 Flink 算子链的中间状态。Flink CDC 配合 Flink 引擎通过以下方式保障端到端的一致性:
全局一致性屏障(Barrier):
在触发 Savepoint 时,Flink 的JobManager会向数据源注入一个特殊的 Checkpoint Barrier。
这个 Barrier 随着数据流向下游流动。只有当下游所有的算子(包括 Sink 算子)都处理完该 Barrier 之前的所有数据并完成了状态持久化后,整个 Savepoint 才宣告成功。
这意味着,Savepoint 中记录的 Binlog 位点,与下游 Sink 算子已经写出(或提交)的数据状态在逻辑时间上是完全一致的。下游 Sink 端配合(幂等或二阶段提交):
- 幂等写入(Idempotent Sink):如果下游是 Redis、HBase 或支持 Upsert 的数据库,在恢复后由于少量重放产生的数据,会通过主键(Primary Key)进行覆盖更新,从而保证数据最终一致。
- 两阶段提交(2PC Sink):如果下游是 Kafka、JDBC 等支持事务的系统,在恢复时,Flink 会回滚 Savepoint 之后那些未提交(Uncommitted)的事务。从 Savepoint 记录的 Binlog 重新读取并写出的数据,会重新放入新的事务中进行提交,从而实现物理上的端到端 Exactly-Once。