简介:面向具备一定 Flink 基础、希望打通实时同步链路的研发人员,这份文档围绕 Flink CDC 3.0 讲解从概念到落地的完整流程。内容先厘清 CDC 的变更捕获思想,对比基于查询与基于 Binlog 两种方式的执行模式、覆盖范围、延迟和数据库压力差异,再引出 flink-cdc-connectors 对 MySQL、PostgreSQL 等数据源的全量与增量读取能力。案例从 MySQL 建库建表和数据准备入手,分别演示 DataStream 与 Flink SQL 写法,并给出 MySQL 到 Doris 的 Streaming ETL 实操,涵盖环境搭建、配置编写、任务启动与测试验证,同时强调开启 Binlog、设置检查点等关键配置。资源包为 1 个 docx,约 145KB;已有 1789 人学习,适合按章节跟做,理解各配置项作用,进而独立完成数据源连接、流处理与目标存储对接。
1. MySQL 到 Doris 的秒级同步:为什么第一次启动要先扫全表
在 MySQL 里 UPDATE 一行,Doris 里几秒后就能查到新值,用 Flink CDC 搭这条链路,核心代码不到三十行。但真正反直觉的地方不在“监听”,而在第一次启动:任务并没有从 binlog 开头读起,它先把整张表扫了一遍,扫完才无缝接进 binlog 流。全量和增量在同一条数据流里接上了,中间没有锁表,也没有丢一条变更。
这套“快照 + 增量 + 位点对齐”的组合拳,才是 MySQL 到 Doris 实时同步能做到秒级又不重不漏的基础。搞懂它,你才知道为什么有的同步任务重启一次就多出几万条重复数据,有的却能精确续跑;也才知道binlog_format=row、server-id这类看起来无关紧要的配置,为什么能决定整条链路能不能工作。
下面按“CDC 两条路线 → binlog 物理结构 → 快照位点对齐 → Source 内部实现 → 3.0 Pipeline → 踩坑排查”的顺序拆。
2. CDC 的两条路线:为什么 binlog 能同时拿到全量和增量
CDC 这个词听起来新,做数据同步的人其实早就在干这事。最朴素的办法是查:定时跑一条select * from t where update_time > ?,把增量捞出来写进目标端。这条路线的代表是各种批同步工具,几百万行的表几分钟能过一遍,不算慢。问题是它有几个绕不过去的坎,每一个都能在真实场景里出问题。
2.1 “基于查询”的路线为什么会出问题
第一个坎是删除。物理删除之后,那行数据在源表里彻底消失了,靠select永远查不出来。要捕获 delete,要么把业务改造成软删除,在表里加is_deleted字段,要么定期做全量比对,成本立刻上去一个量级。很多团队一开始图省事选择忽略删除,跑了半年才发现目标端的“已删除用户”还在源源不断进报表。
第二个坎是时间列的可信度。不少表压根没有update_time;有的表有,但值是应用侧传进来的,应用服务器和数据库的时钟差几秒,边界上的数据就漏了。这种漏数据极其隐蔽——任务日志一片正常,几个月后月底对账才发现在某个时间点少了那么几十条。我一般会这么理解:基于查询的 CDC 把一致性责任推给了业务表的字段设计,而这个设计往往不受数据团队控制。
第三个坎是轮询本身的代价。10 张表按 1 秒轮询一次,就是 10 个 QPS 的索引扫描;表大了索引扫不动,直接退化成全表扫。业务库的 IO 本来就紧张,再挂一个同步任务上去,血泪经验基本都来自这里。第四个坎更根本:批模式天然做不到秒级,延迟下限就是你设定的轮询间隔。
| 维度 | 基于查询的 CDC | 基于 binlog 的 CDC |
|---|---|---|
| 执行模式 | 批(Batch) | 流(Streaming) |
| 能否捕获删除 | 不能,物理删除后行已消失 | 能,DeleteRows 事件 |
| 是否依赖业务字段 | 依赖 update_time 之类的列 | 不依赖,直接看日志 |
| 变化延迟 | 取决于轮询间隔,通常分钟级 | 毫秒到秒级 |
| 对源库压力 | 每次轮询都是一次查询 | 一条长连接持续读日志 |
| 实现复杂度 | 低 | 高,要解析日志并维护 schema |
基于 binlog 的路线把视角从“表里现在有什么”换成了“数据库刚刚发生了什么”。视角换了以后,上面几个坎同时消失了:删除变成一条日志事件,不需要时间列,也不需要轮询。代价是复杂度转移到解析端——你得能把二进制日志还原成结构化的行变更,还得在源表结构变化时跟得上。
2.2 binlog 的物理结构:一次 UPDATE 到底写了什么
binlog 是 MySQL server 层维护的逻辑日志,记录的是“数据变成什么样”,而不是“磁盘哪一页改了”——后者是 InnoDB 的 redo log 负责的事。这个区别很关键:redo log 是物理的、循环写的、格式和存储引擎绑定;binlog 是逻辑的、追加写的、可以被任何外部程序解析。CDC 能成立,前提就是 binlog 是逻辑的且可读的。
binlog 有三种格式,CDC 场景下只能选一种:
STATEMENT:记录 SQL 原文。问题是NOW()、UUID()、不带ORDER BY的LIMIT这类语句重放结果不确定;而且 CDC 端拿到 SQL 原文也还原不出 before image,下游根本不知道“改之前是什么”。ROW:记录每一行变更前后的值。CDC 必须要这个格式。MIXED:由 MySQL 自己决定用哪个,对 CDC 来说等同于赌博。
ROW格式下,一次事务在 binlog 里大致长这样:
GTID_EVENT或QUERY_EVENT(BEGIN):事务开始标记;TABLE_MAP_EVENT:把“表 ID”和“这张表的列定义”建立映射;WRITE_ROWS_EVENT/UPDATE_ROWS_EVENT/DELETE_ROWS_EVENT:真正的行变更,带列序号和值;XID_EVENT:事务提交。
这里有个容易被忽略但后果严重的推论:行事件里不含列名,只有列序号。解析端必须自己能还原出“第 3 列叫什么、是什么类型”,而这个还原依赖TABLE_MAP_EVENT里那份列定义快照。一旦源表ALTER了,列序号整体位移,解析端如果还用旧映射,读出来的数据就整体错位——这就是 DDL 变更历来是 CDC 链路上最容易出问题的地方。
再补一个参数:binlog_row_image。取FULL(默认)时 before 和 after 都记全;取MINIMAL时只记主键和被改的列,日志体积能小一大截,但 CDC 端拿不到完整的 before image,遇到“只更新某一列”的语义就丢了。生产环境如果为了省空间调成MINIMAL,同步链路的行为会悄悄变化。
# 在源库机器上直接解析 binlog 文件 # -v 把行事件翻译成可读的伪 SQL,--base64-output=DECODE-ROWS 让 ROW 事件以明文输出 mysqlbinlog -v --base64-output=DECODE-ROWS \ --start-position=4 /var/lib/mysql/mysql-bin.000003 | head -n 60-v出现一次是翻译行事件,出现两次(-vv)会额外打出列类型;--start-position从指定位点开始,用来复现某一段;不加--base64-output会看到一堆 Base64 的行事件,读不了。排查“CDC 到底有没有读到这条变更”时,第一步就是在源库上确认这条变更确实进了 binlog——这一步能直接排除掉一半的“下游没数据”问题。
2.3 快照与增量的衔接:位点是怎么对齐的
全量和增量要接上,传统做法有三种,演进过程本身就是一部踩坑史。
第一种最老:FLUSH TABLES WITH READ LOCK拿全局读锁,记录SHOW MASTER STATUS的位点,全量 dump,再释放锁,然后从那个位点读 binlog。问题是锁表期间业务完全写不进去,几百 G 的表 dump 几个小时,就是几个小时的写入阻塞。这个方案在稍大的库上基本不可用。
第二种:在 RR 隔离级别下START TRANSACTION WITH CONSISTENT SNAPSHOT,不开锁,靠 MVCC 拿一个一致性快照,事务期间读到的都是快照时刻的数据,同时记下位点。这个方案能跑,但有两个隐患:长事务会阻止 undo purge,大表 dump 几个小时能把 undo 表空间撑爆;而且它是单线程的,一张大表只能一个会话慢慢读,全量时间下不来。
第三种是 Flink CDC 用的增量快照,核心思路分三步:
- 切分。按主键把表切成若干 chunk,比如
id BETWEEN 1 AND 10000、10001 AND 20000,chunk 大小可配。 - 双读并行。一边用
SELECT * FROM t WHERE id BETWEEN ? AND ?读 chunk,一边保持 binlog 流不断,两条路同时走。 - 水位对齐。每个 chunk 开读前记下当前 binlog 位点(low watermark),读完再记一个(high watermark)。chunk 读完后,把落在
[low, high]区间内的 binlog 事件重放一遍:如果某行主键已经在 chunk 里读过,就以 binlog 事件为准覆盖;如果主键不在本 chunk 范围内,说明是别的 chunk 的事,跳过。
最终效果是:一条记录在快照和增量里最多被处理两次,但经过水位对齐后,结果等价于“在某个时间点做了一次全量,然后从那个点开始严格按顺序应用增量”,不重不漏。
这套机制换来的好处很实在:不锁表,业务无感;chunk 可以并行,10 个 reader 同时读 10 个 chunk,大表全量时间线性下降;失败恢复粒度细到 chunk,checkpoint 里记的是“哪些 chunk 已完成”,重启只重跑没做完的。
代价也要说清楚:必须有主键或唯一非空键,没有主键的表切不了 chunk,只能退化成单并行度全表扫;binlog 事件要在状态里缓存一段时间,chunk 读得越慢缓存越大;水位对齐本身有开销,chunk 切得太碎反而拖慢。我一般会这么理解这套设计——它把“一致性快照”这件事从数据库侧(锁、长事务)挪到了计算侧(水位、重放),数据库只负责不停地吐日志,一致性的保证交给流处理引擎的状态来兜。
3. MySqlSource 的内部结构:StartupOptions 与状态存储
理解了上面的机制,再去看 Flink CDC 暴露出来的那几个配置项,就不像是在背诵参数了——每一个都对应着上面某个环节的取舍。
3.1 StartupOptions 的五个模式,语义差在哪
MySqlSource<String> mySqlSource = MySqlSource.<String>builder() .hostname("node03") .port(3306) .databaseList("test") // 捕获的库,支持正则 .tableList("test.t1") // 捕获的表,格式必须是 db.table .username("root") .password("000000") .deserializer(new JsonDebeziumDeserializationSchema()) .startupOptions(StartupOptions.initial()) // 启动位点策略 .build();databaseList和tableList都支持正则,但tableList的每一项必须是库名.表名的完整形式,只写表名匹配不上。deserializer决定SourceRecord怎么变成下游要的类型,后面单独讲。
startupOptions是最容易选错的一项,五种模式的语义差别很大:
| 模式 | 首次启动行为 | 会重跑全量吗 | 典型场景 | 风险 |
|---|---|---|---|---|
initial | 先做一致性快照,再从快照位点读 binlog | 有 checkpoint 时不重跑 | 首次初始化,目标端为空 | 大表全量耗时长,占带宽 |
earliest | 不做快照,从当前最早的 binlog 位点开始 | 否 | 目标端已有历史数据,只补日志 | binlog 可能已被 purge;位点前的表结构可能已变 |
latest | 不做快照,从 binlog 末尾开始 | 否 | 只要新变更,历史数据已离线导入 | 启动之前的变更全部丢失 |
specificOffset | 不做快照,从指定的 file + pos 开始 | 否 | 精确复现某一段,或从备份位点续 | 位点算错就丢数据或重复 |
timestamp | 不做快照,从指定时间戳开始,遍历 binlog 跳过更早事件 | 否 | 按时间点回补 | 需要从头遍历日志,日志大时启动很慢 |
earliest是最常被误解的一个。它不做快照——语义是“我假设目标端已经有历史数据了,只把日志从头补一遍”。如果目标端是空的却用earliest启动,最后读到的只有 binlog 里还留着的那部分变更,更早的历史数据永远补不回来。而 binlog 是有保留期的,过期就被 purge 了,earliest实际能拿到的起点往往比你想象的晚得多。
initial之所以是默认推荐,是因为它把“全量”和“增量”这两件事在语义上合并成了一次完整的同步。但要注意:它只在没有可用 checkpoint 的时候才走全量。一旦有了状态,重启会直接从状态里的位点续,不会重来一遍。
3.2 binlog 位点是怎么被 checkpoint 存下来的
Flink CDC 的 Source 是按 FLIP-27 的 Source API 实现的,分两个角色:SplitEnumerator跑在 JobManager 上,负责切分和分配;SourceReader跑在 TaskManager 上,负责实际读数据。这个拆分是 checkpoint 能工作的前提——枚举器知道全局还有哪些 split 没做完,reader 知道自己手上这个 split 读到哪了,两边各自往状态里写。
split 分两类:快照 split(一个 chunk 一个,状态是“主键上界 + 是否完成”)和增量 split(只有一个,状态是 binlog 位点,包含文件名、position、GTID set)。checkpoint 触发时,枚举器存“未分配的和已分配未完成的 split 列表”,reader 存“当前 split 的进度”。恢复时枚举器从状态重建,把没完成的 split 重新下发,reader 从记录的位点继续——已完成的 chunk 一个都不会重读。
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); // 关键:不开 checkpoint,binlog 位点就没地方存,重启只能从头再来 env.enableCheckpointing(3000L, CheckpointingMode.EXACTLY_ONCE); env.getCheckpointConfig().setCheckpointTimeout(60 * 1000L); // 两次 checkpoint 之间至少隔 3 秒,避免上一次没做完下一次又压上来 env.getCheckpointConfig().setMinPauseBetweenCheckpoints(3000L); // 任务被 cancel 时保留最后一次成功的 checkpoint,否则状态跟着作业一起没了 env.getCheckpointConfig().enableExternalizedCheckpoints( CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION); // 1 天内失败不超过 3 次就自动重启,两次重启之间至少间隔 1 分钟 env.setRestartStrategy(RestartStrategies.failureRateRestart( 3, Time.days(1L), Time.minutes(1L))); env.setStateBackend(new HashMapStateBackend()); env.getCheckpointConfig().setCheckpointStorage("hdfs://node02:8020/flinkCDC");EXACTLY_ONCE在这里的作用不只是笼统的“数据不重复”——它保证的是 barrier 对齐,也就是“状态里记的 binlog 位点”和“已经向下游发出的数据”处于同一时刻。改成AT_LEAST_ONCE后 barrier 不对齐,恢复时位点可能比实际已发出的数据靠前,重复就来了。
RETAIN_ON_CANCELLATION是最容易漏的一项。默认行为是作业 cancel 时把 checkpoint 一起删掉。如果你是手动 cancel、改个配置再启,状态没了,任务只能按startupOptions重新来一遍——initial就是重跑全量。所以规范流程是先打 savepoint 再 cancel:
# 先触发 savepoint,把当前 binlog 位点固化到指定路径 bin/flink savepoint <JobId> hdfs://node02:8020/flinkCDC/save # 然后从 savepoint 恢复,而不是从零开始 bin/flink run -s hdfs://node02:8020/flinkCDC/save/savepoint-xxxx \ -c com.example.cdc.FlinkCDCDataStreamTest ./flink-cdc-test.jarfailureRateRestart(3, Time.days(1L), Time.minutes(1L))的语义是:一天之内失败次数不超过 3 次就继续重启,每次重启前等 1 分钟。这个策略适合 CDC 场景——源库短暂抖动、网络闪断这类问题重启就能好,但如果是配置错误,3 次之后作业会彻底失败,不会无限重启把集群拖垮。
3.3 反序列化:SourceRecord 到 JSON 的那一层
Debezium 把 binlog 事件解析成SourceRecord,这是 Kafka Connect 的数据结构,包含 key、value、topic、partition、sourceOffset。默认的JsonDebeziumDeserializationSchema把它转成 JSON,结构大致是这样:
{ "before": null, "after": { "id": "1001", "name": "zhangsan" }, "source": { "db": "test", "table": "t1", "file": "mysql-bin.000003", "pos": 154, <p> <a href="https://download.csdn.net/download/zzl8888888/91240170" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>