把一套每天凌晨跑批的定时同步链路,换成基于 Flink CDC 的实时流式计算管道,这个决定我们大概纠结了三周。当时团队里争议不小:既有方案虽然延迟有半天,但稳定跑了两年,突然要动它,心里没底。后来真正推动换掉它的原因,是业务方开始要分钟级数据可见性,而且要得越来越急,定时抽取那套玩法明显撑不住了。
项目落地之后,最大的感受是 Flink CDC 并不只是“一个能抓 MySQL 增量数据的工具”,它真正改变的是我们设计数据集成任务的思维方式:把“批”的思维换成“流”的思维,把“抽数据”的思维换成“订阅表变更事件”的思维。这篇文章就是我基于这次实战整理的完整复盘,包括选型理由、核心原理、可直接复现的配置、踩过的坑和后续升级方向,希望给正在评估或者已经在用 Flink CDC 的朋友一些真实参考。
1. 项目背景与方案选型
1.1 从定时抽取到实时流同步
我们原来的架构很朴素:业务库是 MySQL,数仓ODS层靠 DataX 每半小时或者每两小时拉一次全量。数据量小的时候没啥毛病,但随着表规模上来,有几个问题越来越突出。
首先是延迟问题。业务方要求看最近 15 分钟的数据,定时同步根本满足不了,哪怕我们把调度频率调到每五分钟一次,还会牵扯到抽取窗口重叠和数据重复。其次是全量扫描对源库的压力,每次跑批都要把几百万甚至上千万行全部扫一遍,即便分页查,IO 和主从延迟都会显著上升。第三是增量识别困难,虽然我们可以在每张表上加update_time字段做增量标记,但碰到删除、历史数据修正、跨库关联等场景,这个方案就特别别扭。
真正让我下决心替换的,是一次线上事故。某天凌晨 DataX 跑批超时,导致整个 ODS 层数据延迟了四个小时,业务方直接炸了。我当时就想,有没有一种方式能从源头订阅数据库的变更日志,让数据一变就到下游,而不是每次都把整张表捞一遍。这就是 CDC(Change Data Capture,变更数据捕获)的典型思路,而 Flink CDC 是实现这个思路最顺手的一套框架。
1.2 为什么最终选了 Flink CDC
在调研阶段,我对比了几个主流 CDC 方案:Canal、Maxwell、Debezium,以及 Flink CDC 本身。这里放一个我当时的对比视角:
| 维度 | Canal | Maxwell | Debezium | Flink CDC |
|---|---|---|---|---|
| 底层原理 | 解析 binlog,伪装成 slave | 解析 binlog | 解析 binlog,Kafka Connect 生态 | 内置 Debezium,封装为 Flink Connector |
| 下游对接 | 主要对接 MQ/自定义 | Kafka | Kafka | Flink 全生态 |
| 全量增量衔接 | 基本靠手动 | 基本靠手动 | 依赖 Kafka Connect 增量快照 | 自动衔接,内置 chunk 快照 |
| 断点续传 | 较复杂 | 较复杂 | 依赖 offset 管理 | checkpoint + savepoint 原生支持 |
| 计算与清洗 | 需要单独写服务或接 Flink | 需要单独写服务 | 需要接 Kafka Streams/Flink | Flink SQL/DataStream 原生处理 |
这里我说几个关键判断:
第一,Flink CDC 天然和 Flink 生态打通。我们的下游不仅有 Kafka,还有 Hudi 数据湖、MySQL 维表、以及一些实时大屏的聚合指标,如果只用 Canal 或 Maxwell,中间还得包一层消息解析和业务逻辑处理,而 Flink CDC 直接一把梭,从 binlog 捕获到流式 ETL 到写入下游,全都在 Flink 任务里完成,架构链路简洁很多。
第二,全量与增量自动衔接非常重要。Canal 这类工具接入时,通常要自己处理历史存量数据,再手动记录 binlog position 去追增量,中间一旦出现数据重叠或者遗漏,排查成本不低。而 Flink CDC 的 snapshot 设计能在启动时自动加一把短锁、切 chunk 扫描存量数据、同时记录 binlog offset,存量增量一条链路怼完,数据的一致性由 checkpoint 机制兜底,对使用者非常友好。
第三,断点续传和状态管理是 Flink 的看家本领。这个后面我会单独讲,但选型阶段我就明确了一点:同步工具必须能回答“出故障了怎么办”。Flink CDC 基于 Flink 的 checkpoint,把 binlog 消费位点放在状态里,任务重启或者集群 failover,位点不会丢也不会重,方向是对的。
当然,如果你们团队只是想把 MySQL 变更简单推到 Kafka,那 Canal 或者 Maxwell 可能更轻量。但如果已经有 Flink 环境,或者下游需要做流式计算,那 Flink CDC 的综合性价比确实高。
1.3 环境版本:宁可保守也别激进
我踩过一个很实在的坑:Flink CDC 的版本和 Flink 版本、JDBC Connector 版本、Debezium 版本之间的兼容矩阵,比想象中要敏感。
先说结论,我用的是 Flink 1.16.2 + Flink CDC 2.4.1。这组搭配在实际项目中经受住了考验。Flink CDC 2.4.x 对应内置 Debezium 1.9.x,支持 MySQL 5.7 和 8.0,对ROW格式 binlog 和常见 DDL 解析都比较成熟。如果你用的是 Flink 1.17 或者更高,可以考虑 Flink CDC 3.x,但 3.x 目前的架构变化比较大(引入 CDC YAML 和管道的概念),生产环境大规模普及还没到 2.x 那种稳定程度。
版本不合适会出现什么现象?我见过同一个连接器包在 Flink 1.13 上能正常读 binlog,换到 Flink 1.14 直接报序列化异常,因为内部的数据类型结构在框架层有过变更。这种问题排查起来非常折磨人,因为报错并不直观,往往是跑几分钟后某个 event 处理到一半就崩了。所以我的建议是:
生产环境优先选择官方文档标注“兼容”的版本组合,不要追新。先把链路跑通,再考虑升级。Flink CDC 这种底层解析数据库日志的组件,稳定比新功能重要。
如果你用 Maven 管理依赖,核心要引入的包大概是这些(我这里以 DataStream 为例):
<dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-connector-mysql-cdc</artifactId> <version>2.4.1</version> </dependency>如果只写 Flink SQL,需要在 Flink 的lib目录下放对应的 jars:flink-sql-connector-mysql-cdc-2.4.1.jar和flink-shaded-guava相关依赖。SQL 方式省心,但排查依赖冲突也更麻烦一点,后面讲实操会再提。
2. 核心机制拆解:为什么 Flink CDC 能保证一致性
2.1 binlog:CDC 的地基
要彻底搞懂 Flink CDC,离不开 MySQL 的 binlog。简单说,binlog 就是 MySQL 的二进制变更日志,它记录了所有导致数据变化的操作:INSERT、UPDATE、DELETE,甚至 DDL。之前做 DataX 同步时我们完全不看 binlog,因为我们是用 SQL 查询去“问”数据库数据长什么样,而 CDC 的思路变成“听”数据库说它自己改了什么。
这里有一个前提:MySQL 必须开启 binlog,并且格式设为ROW。理论上有STATEMENT、MIXED、ROW三种格式,STATEMENT记录的是 SQL 语句本身,UPDATE之后影响的到底是哪几行,日志里不一定能还原精确的变更内容;MIXED在某些场景下会退化为 STATEMENT。所以 Flink CDC 要求ROW格式,因为只有行级日志才能精确到每条记录的 before image 和 after image。
还需要注意一个点:binlog 里存的是行级变更,但它并没有存完整的表结构信息。Flink CDC 内部通过 Debezium 把 binlog 事件解析成结构化的变更消息,同时还会维护一张表结构的内存映射,这样下游才知道这条数据“应该长什么样”。这也是为什么表结构变更(比如加列、删列)在 CDC 链路里比较敏感,后面踩坑部分我会展开。
2.2 全量和增量是如何“无感”衔接的
很多第一次用 Flink CDC 的人最关心的问题就是:我库里已经有 500 万条历史数据,新建一个同步任务,它是不是先全量再增量?中间数据会不会丢或者重复?
Flink CDC 的 snapshot 机制回答了这个疑虑。任务启动后,它会按主键把表的数据拆成一个个 chunk,并行去扫描这些 chunk 做全量快照。快照过程并非完全无视 binlog,恰恰相反,它在每个 chunk 扫描完成后,会记录当时 binlog 的位点,并保存这部分快照是“基于哪个位点”读的。最终在所有 chunk 都扫完之后,它再从这个任务启动前记录的 binlog offset 开始继续消费增量。
这个过程包含了两个关键设计:
- chunk 拆分粒度:默认是按主键范围拆,比如 500 万行拆成 100 个 chunk,每个 chunk 大约 5 万行。chunk 拆得好不好直接影响全量阶段的并发度和内存占用,也直接影响源库压力。
- 水位线对齐:每个 chunk 扫描完成时记录一个低水位和当前 binlog offset,框架会等待所有 chunk 都完成,再统一推进消费位点,确保“存量数据”不会和“增量数据”出现时间线上的空洞。
所以你交给 Flink CDC 的任务,从外部视角看就是:启动后自动进行存量快照,快照跑完,自然进入增量监听。监控面板上体现为 source 端的 snapshot 状态从started变成finished,然后进入到读 binlog 阶段。
2.3 checkpoint 和 savepoint:容错的底气
Flink CDC 能成为生产可用的方案,距离离不开 Flink 的 checkpoint 机制。这里我打个比方:checkpoint 就像是游戏里的存档点。Flink 定时把算子状态做一次快照,其中包括 MySQL CDC Source 当前读到的 binlog 位点。只要任务正常推进,这个位点就会随 checkpoint 一起被保存。
当任务挂掉重新拉起时,Flink 会从最近一个成功的 checkpoint 恢复所有状态,MySQL CDC Source 也会从对应的 binlog 位点继续读。这样就实现了 at-least-once 甚至 exactly-once(取决于下游 Sink 的事务支持)的语义。如果配置了 externalized checkpoint 或者手动做 savepoint,还可以在升级作业、修改并行度、调整 SQL 逻辑时不丢数据。
我在评估阶段专门做过一次故障演练:用 Flink CDC 同步一张高频更新表,然后人为 kill 掉 TaskManager,等集群自动重启恢复 task,再对比源库和目标库的数据量。结果只差在故障窗口内尚未消费到的最新几条数据上,等恢复后增量追平,数据完全一致。可靠性这一关,算是过了。
3. 完整实操:跑通一条 MySQL 到 MySQL 的实时同步链路
3.1 源库与目标库的准备
我们用一套订单库和一套统计库来做演示,假设源库叫app_order,目标库叫dw_order,源库的业务表是order_info,我们要把它实时同步到统计库里做后续分析。
先在源库上确认 binlog 配置:
-- 查看当前 binlog 配置 SHOW VARIABLES LIKE 'log_bin'; SHOW VARIABLES LIKE 'binlog_format'; SHOW VARIABLES LIKE 'binlog_row_image';mysqld 配置里至少要有:
[mysqld] server-id = 223410 log_bin = mysql-bin binlog_format = ROW binlog_row_image = FULL expire_logs_days = 7binlog_row_image设置为FULL很重要。因为 CDC 需要拿到 before image(变更前数据)和 after image(变更后数据),如果设置成MINIMAL,那么 UPDATE 事件里可能只有被修改过的字段值,下游要做整行 upsert 的时候就缺字段了。
然后创建专门的同步账号,权限最小化:
GRANT SELECT, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'cdc_user'@'%' IDENTIFIED BY '你的密码'; FLUSH PRIVILEGES;这里三个权限分别做什么用:SELECT是快照阶段扫表需要的;REPLICATION SLAVE是让 Flink CDC 假装成一个 MySQL 从库去拿 binlog 流用的;REPLICATION CLIENT用于获取 binlog 位点信息。注意,如果没有REPLICATION CLIENT,任务启动时会报 “Access denied; you need the REPLICATION CLIENT privilege” 的错。
目标库不用特殊配置,只要提前建好目标表结构即可。如果有主键冲突策略,Flink CDC 到 MySQL 的 Sink 一般靠目标表主键做 upsert。
3.2 Flink SQL 方式:从建表到提交流
Flink SQL 的优点是上手快,逻辑直观。我们要在 Flink 的sql-client或者某个 SQL 引擎里执行一段建 source 表的语句:
CREATE TABLE source_order_info ( order_id BIGINT PRIMARY KEY NOT ENFORCED, user_id BIGINT, product_id BIGINT, order_amount DECIMAL(10, 2), order_status INT, create_time TIMESTAMP(3), update_time TIMESTAMP(3) ) WITH ( 'connector' = 'mysql-cdc', 'hostname' = '192.168.10.20', 'port' = '3306', 'username' = 'cdc_user', 'password' = '你的密码', 'database-name' = 'app_order', 'table-name' = 'order_info', 'server-id' = '5400-5405', 'scan.startup.mode' = 'initial', 'scan.incremental.snapshot.chunk.size' = '4096' );说明几个参数:
server-id是伪装成 MySQL slave 的实例 ID,不能和真实从库重复。建议给每个并行子任务分配一个区间,比如并行度 4 就配置5400-5403。scan.startup.mode支持initial(先全量后增量)、latest-offset(只读新增)、timestamp(从某个时间点开始)、specific-offset(从指定位点开始)。日常最常用initial。scan.incremental.snapshot.chunk.size控制每个 chunk 的行数,默认 8096。表特别大时可以调小,避免单个 chunk 扫描时间过长。
目标表的 Sink 定义:
CREATE TABLE sink_order_info ( order_id BIGINT PRIMARY KEY NOT ENFORCED, user_id BIGINT, product_id BIGINT, order_amount DECIMAL(10, 2), order_status INT, create_time TIMESTAMP(3), update_time TIMESTAMP(3), PRIMARY KEY (order_id) NOT ENFORCED ) WITH ( 'connector' = 'jdbc', 'url' = 'jdbc:mysql://192.168.10.21:3306/dw_order', 'username' = '同步账号', 'password' = '你的密码', 'table-name' = 'order_info' );启动任务的核心 SQL 就两句话:
INSERT INTO sink_order_info SELECT * FROM source_order_info;任务提交之后,Flink Web UI 上会看到两个算子:Source 端先处于Snapshotting状态,等全量完成后才变回读 binlog。全量执行期间,源表数据如果发生变化,会被缓冲在 binlog 消费侧,快照完成后自动追平。
这里有个细节我必须提:不要对 source 任务的并行度做无脑调大。MySQL CDC 在快照阶段虽然可以并行扫表,但它读取 binlog 的阶段本质上是串行的,因为一个分片的 binlog 流有先后顺序。我之前把 source 并行度调到 8,结果快照阶段确实快了不少,但进入增量阶段后一部分并行子任务只是在空转,而且 server-id 占用多了,源库的从库连接数压力也上来了。建议 source 并行度默认 2~4,核心瓶颈不在并发而在下游 Sink 的处理能力。
3.3 DataStream 方式:什么时候需要它
Flink SQL 解决大部分场景没问题,但两个场景我建议用 DataStream API:
一是需要在同步过程中做复杂的状态编程,比如跨事件做累计去重、动态字段映射、过滤规则依赖外部配置。二是要精确控制输出的序列化格式,比如直接输出 Debezium 风格的 JSON 事件给下游 Kafka。
一个最简的 DataStream 写法参考:
DataStreamSource<String> stream = env .addSource(new MySqlSource<String>() {{ setHostname("192.168.10.20"); setPort(3306); setDatabaseList("app_order"); setTableList("app_order.order_info"); setUsername("cdc_user"); setPassword("你的密码"); setServerID("5400"); setStartupOptions(StartupOptions.initial()); setDeserializer(new JsonDebeziumDeserializationSchema()); }}); stream.map(new MapFunction<String, String>() { @Override public String map(String value) { return processOrderEvent(value); } }).addSink(...); env.execute("mysql-cdc-order-sync");这种方式的缺点是所有字段结构、表名映射解析都要自己写,尤其是 Debezium 事件里包含before、after、op、ts_ms这些字段,新手容易在解析上花时间。我个人的建议是:能用 SQL 的绝不上手写 DataStream,写代码意味着后续所有改动都要重新编译和提交。
3.4 同步到 Kafka:解耦消息链路
另一个高频场景是 MySQL 变更实时进 Kafka,让下游多个团队各自消费。这个场景下 Sink 端只需要配置 Kafka Connector:
CREATE TABLE order_cdc_kafka ( order_id BIGINT, user_id BIGINT, product_id BIGINT, order_amount DECIMAL(10, 2), order_status INT, create_time TIMESTAMP(3), update_time TIMESTAMP(3) ) WITH ( 'connector' = 'kafka', 'topic' = 'ods_order_info_cdc', 'properties.bootstrap.servers' = '192.168.10.31:9092', 'properties.group.id' = 'cdc-producer-group', 'format' = 'debezium-json', 'scan.startup.mode' = 'earliest-offset' );需要注意,Kafka Sink 里如果用了debezium-json格式,下游消费方拿到的消息结构会包含 Debezium 元数据(before、after、op、source、ts_ms),不是纯数据行。如果下游希望拿到的是“扁平化”后的结果,可以考虑用json格式配合 Flink SQL 的ROW操作来手工把after字段取出来。但这会丢失删除事件的上下文,所以要看业务具体怎么用。
4. 典型问题与排查技巧实录
这部分是真正的“血泪史”,我把实践过程中遇到过的、以及身边同事踩过的坑按频率整理了一下。
4.1 全量阶段 MySQL 锁太久
有次我们同步一张千万级的订单性能表,全量快照阶段源库出现大量Waiting for table flush阻塞。排查后定位到 Flink CDC 在做快照之前会执行一次短暂的FLUSH TABLES WITH READ LOCK以求拿到一致性位点,按设计这个锁应该很快释放,但如果表数据特别多或者有其他长事务卡住,锁窗口会被拉长。
解决办法有几个:
- 如果 MySQL 5.7 和 8.0 都支持,尽量用支持无锁快照的算法。Flink CDC 2.x 默认使用
scan.incremental.snapshot.enabled的 chunk 切分快照,它通过在每个 chunk 扫完后记录 binlog 位点,尽量把全局锁时间降到极小。不过要注意,某些老版本或者特殊配置(比如没有主键的表)会退化成“单次全表快照”,锁表时间会非常久。 - 保证源库没有长时间未提交的事务,定期 kill 掉空闲长事务。
- 把
scan.incremental.snapshot.chunk.size调小一点,比如从 8096 改成 2048,降低单 chunk 的耗时。
4.2 binlog 过期导致任务无法启动
这是最隐蔽的坑之一。如果你的任务曾经因为集群维护或者网络分区停了很久,等重新拉起时,它的上次位点对应的 binlog 文件可能已经被清理了。报错通常类似 “Could not find first log file name in binary log index” 或者 “Requested binlog position is not available”。
我当时把expire_logs_days设置成了 7 天,理论上足够窗口期,但碰上数据量大、写入频繁的生产库,binlog 可能一天就轮替很多个文件,7 天也扛不住超长故障窗口。
我的处理经验是:
- 重要表的关键同步任务,要么把 binlog 保留时间调到足够长(但要评估磁盘成本),要么给任务做一个完善的监控告警,发现故障立即处理。
- 如果 binlog 真的已经丢了,别妄想能找到旧位点。直接停掉旧任务,用
scan.startup.mode = 'latest-offset'重新启一个只读增量任务。对于历史数据,先通过离线方式补一次全量到目标表,再处理从补全量开始到增量启动之间的窗口数据。 - 更稳妥的做法是,平时就定期记录同步位点到一个外部队表,出现丢失时能以“全量 + 从指定位点追增量”的方式恢复,避免整表重扫。
4.3 字段精度和类型映射问题
Flink CDC 的类型映射在某些边界情况下会给人惊喜,也要给人惊吓:
DECIMAL类型要注意精度。MySQL 的DECIMAL(20, 4)在 Flink 里会映射成DECIMAL(20, 4),但如果下游表精度不够,比如 sink 端建成了DECIMAL(18, 2),数据同步到一半会报数字溢出。TIMESTAMP带小数秒的问题。MySQL 支持datetime(3)、datetime(6),Flink 侧需要显式声明TIMESTAMP(3)或TIMESTAMP(6),否则默认精度可能对不上。BIGINT UNSIGNED这个比较经典。Flink CDC 2.x 在读BIGINT UNSIGNED且值超过Long.MAX_VALUE时,会抛出序列化异常。生产表单里一般不会有这么大数字,但从 Oracle/其他系统迁移过来的历史表非常容易中招。我的建议是建表时把这种列转换成DECIMAL(20, 0)或者干脆在业务侧约定不用无符号大数。
4.4 server-id 冲突和 source 端连接数暴涨
如果你有好几个 Flink CDC 任务同时连同一个 MySQL 实例,每个任务都会以“从库”的身份去注册一个 server-id。如果 server-id 设置成写死的单值,很容易冲突,导致 MySQL 侧直接断掉之前的 binlog dump 连接,任务表现为间歇性报错或者读取延迟陡增。
所以我强烈建议:server-id按任务区分,并按并行度留足区间。比如任务 A 用5400-5403,任务 B 用5404-5407。同时,MySQL 侧可以通过:
SHOW PROCESSLIST;看到有多少个来自 CDC 客户端的连接,如果数量异常大,去排查是不是同一个 server-id 在多次复用。
4.5 checkpoint 持续失败和反压
Flink CDC 一旦进入增量阶段,source 端的吞吐会受 binlog 解析效率和下游 sink 处理速度影响。如果你发现 checkpoints 频繁失败,检查这几个方向:
- 检查 JobManager 日志里是否有
Checkpoint expired before completing,这类问题大概率是 sink 端写入变慢,导致整个 pipeline 反压,checkpoint barrier 无法在超时时间内走完全程。 - 查看 Web UI 的 BackPressure 指标,如果 source 或者中间算子处于 High 状态,优先优化下游的写入并行度和批量参数。
- 如果目标是 MySQL,JDBC Sink 可以调大
sink.buffer-flush.max-rows和sink.buffer-flush.interval,减少高频小事务。目标端如果是 Hudi,需要关注小文件合并参数。 - 还有一个容易被忽略的原因:状态太大。Flink CDC Source 内部维护了每个并行子任务的 binlog offset 状态,正常情况下不大。但如果你在算子链上做了大状态的操作(比如
keyed聚合),checkpoint 时间会被状态序列化拉长。这种情况下可以考虑把 CDC 同步和后续的处理逻辑拆成两个 job,中间用 Kafka 解耦。
4.6 常见问题速查表
| 现象 | 可能原因 | 处理思路 |
|---|---|---|
| 任务启动失败:找不到 binlog 文件 | binlog 过期清理 | 使用latest-offset重建或外部保存位点恢复 |
| 全量阶段源库锁表 | 长事务 / 快照锁窗口 | 调小 chunk size,杀掉长事务 |
| 数据出现重复 | sink 非 exactly-once / 下游未幂等 | 下游使用 upsert 语义,或开启 checkpoint 并配置事务性 sink |
| 数据延迟逐步增大 | 反压 / sink 瓶颈 / source 单分区 | 优化 sink,增加 kafka partition,避免大聚合算子链 |
| 类型转换异常 | 精度溢出 / BIGINT UNSIGNED | 调整建表类型,先清洗再同步 |
| Server_id 冲突 | 多任务使用同一 ID | 按任务和并行度分配 ID 区间 |
| UPDATE 事件把整行变更都丢了 | binlog_row_image 设置成 MINIMAL | 源库改为 FULL,重新读取 |
| 表结构变更后同步失败 | DDL 未被算子处理 | 详细见下一节方案 |
5. 进阶玩法与稳定运维经验
5.1 表结构变更:绕不开的“房间里的大象”
Flink CDC 的一个尴尬现状是:它读的是 Debezium 转换后的变更流,Debezium 在事件流中确实能包含一些 DDL 元信息,但 Flink SQL 的 schema 在作业启动时就已经固定了。换句话说,如果源表加了列,旧任务的表结构定义不会自动变,新数据过来后会出现反序列化失败,任务报错:JSON decode error或者Field 'new_col' not found。
我目前在生产环境用的方案是分级处理:
- 全局约定:业务表的字段变更需要提前一周通知数据团队,由我们评估影响面后再执行。
- 小变更(加列):如果是可空列或者有默认值,我会评估能否在任务停机窗口内同步修改 Flink SQL 的表定义并重启任务。重启需要基于 savepoint 恢复,保证位点连续。
- 大变更(删列/改类型):这种情况下已经不能保证新旧数据结构的兼容性,需要停下来,重建同步任务,必要时重跑全量。
如果你们想更自动化,可以在同步链路的中间加一层 Schema Registry,但引入额外组件也会带来运维复杂度。对大多数团队来说,人工流程 + 监控告警反而是性价比最高的选择。
这里我单独强调一个实用技巧:给每张表显式声明 PRIMARY KEY,并且用NOT ENFORCED。Flink CDC 在 snapshot 切 chunk 时严重依赖主键范围,没有主键的表不仅快照性能差,还会在增量阶段产生乱序问题。对于一些没有主键的历史表,宁可先加一个自增列或者用联合字段做主键,也不要裸奔。
5.2 多表汇聚:从“一张表同步”到“数据湖全量入湖”
单一表同步只是入门,生产中最常见的还有多张来源表写入同一张目标表。比如业务上订单表、退款表、订单明细表最终要合流到一张宽表,不同的表结构不同,怎么合?
Flink CDC 的实战思路是先各自建 source,再用 UNION ALL 或者不同字段映射到一个统一的中间层,最后写目标表。举个简化的 SQL:
CREATE VIEW order_union AS SELECT order_id, user_id, order_amount, 'order' AS event_type, create_time FROM source_order_info UNION ALL SELECT refund_id, user_id, refund_amount, 'refund' AS event_type, create_time FROM source_refund_info; INSERT INTO dw_order_union SELECT * FROM order_union;这种方式会让每个表各自独立维护自己的 binlog 消费位点,逻辑清晰,也不互相影响。但如果业务希望把多表的数据基于主键做 join 之后同步,那就涉及 Flink 的状态 join,需要配置 TTL(状态存活时间)来兜底迟到的数据,这块对资源和语义设计要求高,别一上来就搞复杂的。
5.3 延时优化:压到秒级能做什么
环境到位后,Flink CDC 的理论延时很低:从 binlog 产生一条变更,到 Flink 端处理完成,再到写入下游,通常能做到秒级乃至毫秒级。实际影响延时的主要因素有三个:
- 源库自身主从复制落盘时间(一般忽略)。
- Flink 任务中是否有聚合或者 join 操作,有的话状态和 watermark 会引入额外等待。
- Sink 端的写入吞吐。比如 JDBC Sink 默认是批量的,
buffer-flush.interval设得太大会增加最多一个 interval 的延迟。
我们线上订单表从 MySQL 变更到 Kafka 落地的 p99 延迟在 2~5 秒之间,大部分时间在 1 秒以内。这个量级已经能支撑大屏、风控特征、实时对账等场景。
但注意,如果你的下游要消费 CDC 做复杂的事件驱动微服务,请谨慎评估“先到先得”的乱序问题。CDC 流是单表有序的,但多表 join 后事件顺序并不天然保证。必要时可以按主键做 keyBy 后再进入业务逻辑,保证同一主键的事件有序。
5.4 任务监控体系:没监控就别上生产
最后提醒一点,Flink CDC 任务上线后,监控比业务代码本身重要得多。我们给每个同步任务都接了一套基础监控:
- 源端 binlog 位点延迟:通过 Flink metrics 的
currentFetchEventTimeLag和currentEmitEventTimeLag观察。 - checkpoint 成功率和最近 checkpoint 间隔。
- 任务重启次数和 failover reason。
- 目标表主键冲突和写入错误日志。
如果发现某个任务的currentFetchEventTimeLag持续增长,说明源端 binlog 快照和当前消费端之间已经出现积压,需要立即检查是任务处理能力不足还是源库写入暴涨。通过监控系统配合告警,我能在业务感知之前先发现同步故障,这比啥玄学调优都管用。
6. 写在最后:一些我的真实体会
整套 Flink CDC 实践做下来,我最深的体会是:它确实解决了传统定时同步的大部分痛点,但也不是无脑上手就万事大吉。选型时要看团队已有的技术栈,如果你们对 Flink 完全不熟,那引入 CDC 的学习成本可能比 Canal + Kafka 更高。如果你下定决心走 Flink 生态这条路,那么建议先用非核心表跑一个月,把 checkpoint、savepoint、DDL 变更、binlog 清理这些边界场景都练熟了,再逐步替换核心链路。
最后再分享一个小经验:新做的 CDC 任务,先别急着上生产,用scan.startup.mode = 'initial'把全量阶段跑一遍,观察快照结束后的增量追平速度。如果全量阶段把源库拖慢了,就调整 chunk size 和并行度;如果增量追平慢,重点检查下游 Sink 并行度和目标库写入能力。把这两个指标摸透了,后面再放大数据量和业务场景,心里就会很有底。