资讯动态

FlinkCDC 实时同步:MySQL Binlog、增量快照与一致性实践

发布时间:2026/10/27 20:03:32 来源:尧图企业网站定制
数据库里的数据每分每秒都在变可下游的数仓宽表还停在昨天夜里跑批的那个快照上。业务方早上九点看报表发现十点钟下的单查不到跑来问是不是系统坏了——这种场景我见过太多次。问题的根子不在报表而在于数据从业务库到分析库的那段路原本是每天一次的重卡运输现在需要改成一条不停歇的传送带。FlinkCDC 就是这条传送带最常被选中的那套零件它蹲在 MySQL、PostgreSQL、Oracle 这些库的日志边上把每一次 insert、update、delete 原封不动地捞出来变成 Flink 里的一条流再往下游任何地方送。先把概念说清楚。CDC 是 Change Data Capture 的缩写直译叫变更数据捕获本质上就是盯着数据库的账本抄流水。FlinkCDC 不是一个独立软件它是 Apache Flink 生态里的一组 Source/Sink 连接器把 Debezium 这类日志解析引擎的能力包装成了 Flink 原生的数据源让变更数据能直接进入 Flink 的算子链路享受到分布式并行、状态管理、Checkpoint 容错这些能力。能做什么实时同步整库、给缓存做失效、给搜索索引做增量更新、把多个业务库汇到一个 ODS 层、做异构数据库之间的不停机迁移。适合谁看正在被 T1 批处理折磨的数据开发、要做实时链路的后端工程师以及刚接触 Flink 想找个真实场景落地的同学。下面这些内容是我在几个项目里反复趟过之后的整理原理部分尽量讲透实操部分给到能直接抄的程度。1. 先想清楚为什么要用日志而不是定时去查表在动手写第一行配置之前有个问题必须先回答为什么不用定时任务去扫表这个问题的答案决定了你后面会不会在半夜被叫起来修数据。1.1 批处理抽取的四个硬伤最常见的做法是起一个定时任务每隔十分钟或一小时用WHERE update_time 上次时间把增量数据捞出来写下游。这个方案上手快、门槛低但它在四个地方会咬人。第一是删除捕获不到。物理删除之后那行数据在源表里消失了你用任何时间戳条件都查不出它曾经存在过、现在没了。下游只能永远保留一条幽灵记录日积月累宽表和明细对不上账。第二是中间态丢失。一条订单在十分钟内从待支付改成已支付再改成已发货定时任务只看到最后一次的状态。如果下游需要计算支付到发货的平均时长这个信息就彻底没了。日志方案能拿到每一次变更的前后镜像中间态一个不落。第三是扫表压力。为了捞增量你需要在update_time上建索引但业务表往往已经有五六个索引了再加一个会拖慢写入。更麻烦的是那些没有update_time字段、或者字段由业务代码手动维护经常忘更新的表根本没法用这个方案。第四是延迟和批量耦合。调度间隔决定了延迟下限想做到秒级就得把间隔压到几秒那扫表频率就上去了压不下去实时性就没了。这个矛盾在批处理框架里基本无解。1.2 日志订阅好在哪儿数据库自己写日志是为了崩溃恢复和主从复制这份日志天然记录了每一行数据的完整变更前后值前提是 ROW 格式而且写入顺序就是事务提交顺序。订阅这份日志有几个先天优势无侵入不需要业务表加字段不需要改任何一行业务代码数据库层面只多一个复制账号。完整insert、update、delete 全都能拿到update 还能拿到变更前和变更后两份镜像。低延迟日志是实时产生的解析出来就是毫秒到秒级的延迟。顺序性同一个主键的变更严格按提交顺序到达不会乱序。代价是什么一是运维复杂度上来了你要保证日志不被过早清理、复制账号权限配好、网络链路稳定二是历史数据得单独处理日志里只有从现在开始的变更存量数据还得靠一次全量快照。FlinkCDC 的价值恰恰在于它把全量快照和增量日志这两件事缝在了一起做成了一个对用户透明的 Source。1.3 哪些场景值得上这套东西不是所有同步都值得引入 FlinkCDC。我的判断标准是三条延迟要求是否在分钟级以内、是否需要捕获删除和中间态、源表是否频繁变更且量大。三条里占两条以上就值得考虑。典型场景包括实时数仓的 ODS 层同步这是最大的一类、缓存与数据库的一致性维护、搜索引擎索引的准实时更新、多业务库汇聚做全局用户画像、以及数据库迁移时的双写过渡期数据校验。反过来如果只是每天凌晨同步一次全量维度表或者数据量小到几万行老老实实用批任务更省心别为了技术而技术。2. 一条变更数据在 FlinkCDC 内部走过的完整链路这一节讲原理。很多人用 FlinkCDC 用了很久只知道配几个参数就能跑一旦出问题就抓瞎根子在于不清楚数据在内部到底经过了哪几道手。把这层搞清楚后面排查问题会轻松一大截。2.1 第一道关数据库必须开启行级日志MySQL 的 binlog 有三种格式STATEMENT、ROW、MIXED。STATEMENT 记录的是 SQL 语句本身比如UPDATE t SET status2 WHERE id100ROW 记录的是每一行数据变更前后的具体值MIXED 是两者的混合由数据库自己判断。CDC 必须用 ROW。原因很直接STATEMENT 格式下你只能看到一条 SQL拿不到改之前是什么样也拿不到这条 SQL 实际影响了哪些行下游无法做幂等的 upsert。而且 STATEMENT 里的函数调用比如NOW()在备库重放时结果可能不同本身就不适合做数据同步。配套的还有一个参数binlog_row_image取值 FULL、MINIMAL、NOBLOB。必须设成 FULL意思是变更前后两行数据的全部列都要记录。如果设成 MINIMALupdate 的 before 镜像里只有主键FlinkCDC 拿不到完整的旧值宽表更新时会出现字段被空值覆盖的问题。这个坑我踩过一次表现是下游某一列莫名其妙变成 NULL查了半天才发现是源库的这个参数被人改过。2.2 第二道关伪装成一个从库Debezium 拿到变更的方式是把自己注册成 MySQL 的一个从库slave/replica。它向主库发送COM_BINLOG_DUMP指令主库就把它当成一个普通的复制客户端持续推送 binlog 事件。这里的关键参数是server-id。MySQL 的主从复制拓扑里每个实例都必须有唯一 ID如果两个客户端用了同一个 ID后连上的会把先连上的踢掉表现为同步任务随机中断、报错提示 server id 冲突。所以 FlinkCDC 的server-id参数支持配置一个区间比如5400-5404表示从 5400 到 5404 这五个 ID 可用。区间的容量必须大于等于任务并行度否则启动时直接报错。在一个 Flink 集群上跑多个 CDC 任务时每个任务的区间要错开这个必须提前规划不然任务一多就互相打架。2.3 第三道关增量快照算法如何做到不锁表这是 FlinkCDC 2.x 之后最核心的改进值得单独讲。老版本的 Debezium 做全量快照时会在表上加全局读锁FLUSH TABLES WITH READ LOCK锁住期间业务写入全部阻塞。一张几千万行的表全量跑二十分钟业务就卡二十分钟这在生产环境是不可接受的。FlinkCDC 2.x 引入了**增量快照Incremental Snapshot**算法思路是把大表切成小块逐块读取读的时候不加锁。具体分几步走第一步记录起点。连接器先读取当前 binlog 的最新位点记为整个任务的起始 offset同时短暂加表级元数据锁读取表结构这个锁的持有时间在毫秒级可以忽略。第二步切分数据块。根据表的主键没有主键则用唯一键把全表数据按主键区间切成若干个 chunk。切分的粒度由scan.incremental.snapshot.chunk.size控制默认 8096 行一个块。比如主键范围是 1 到 100000chunk size 设为 8000就会切出十三个块。第三步并行读取每个块。每个块记录两个水位low watermark块内主键最小值和 high watermark块内主键最大值。读取的时候执行SELECT * FROM t WHERE pk low AND pk high这个查询走主键索引速度很快。关键点在于每个块读取前会记录当下的 binlog 位点这个位点跟块绑定在一起。第四步全量转增量。所有块读完之后连接器从最早的块位点开始消费 binlog。对每一条 binlog 记录它做一次判断这条记录的主键落在哪个块里那个块读完了没有如果块已经读完且 binlog 位点在该块的 high watermark 之后直接向下游发送如果位点落在块读取的区间内说明这条变更可能已经包含在快照里了需要按主键和位点做去重。这个设计的精妙之处在于全量阶段和增量阶段是无缝衔接的中间没有断点也不需要锁表。而且全量阶段可以多并行度跑不同 chunk 分给不同 subtask大幅缩短大表的初始化时间。代价是状态会变大——每个 chunk 的水位信息都要存到 Flink 的 state 里checkpoint 体积会上升这个后面讲调优时再展开。2.4 第四道关并行度的跷跷板这里有个很多人第一次配置时会困惑的点为什么全量阶段能并行增量阶段却不能因为 binlog 是一条全局有序的单流。MySQL 的 binlog 里不同表的变更、同一张表不同行的变更全都混在一条序列里。FlinkCDC 的增量阶段只有一个 subtask 负责读 binlog或者说读到的 binlog 需要按主键分发如果无脑提高并行度会出现同一主键的变更被分到不同 subtask 上、顺序错乱的问题。所以 FlinkCDC 2.x 的实际行为是全量阶段按 chunk 分配到多个 subtask 并行读增量阶段会在 source 内部做一次分发按主键 hash 把变更路由到下游对应的 subtask。你在 Flink UI 上看到的 source 并行度是 N但其中只有一部分真正在消费 binlog。这个特性决定了增量阶段的吞吐上限受单点 binlog 解析能力限制如果单库的变更量特别大比如每秒几万条靠加并行度是解决不了的得考虑拆分任务或者升级到新版本的 Pipeline 模式。2.5 第五道关Checkpoint 才是真正的进度条FlinkCDC 把 binlog 位点存在 Flink 的 state 里随 Checkpoint 一起持久化。这意味着Checkpoint 必须开启而且间隔不能太长。如果没开 Checkpoint位点只存在内存里任务一重启连接器找不到上次读到哪儿只能从头再来一遍全量。我见过一个任务因为 Checkpoint 配置写错忘了配状态后端导致每次重启都全量重跑八千万行运维同学差点崩溃。开了 Checkpoint 之后每次成功做一次快照位点就被持久化。任务失败重启时从最近一次成功的 Checkpoint 恢复接着往下消费不会重复也不会丢下游需要配合幂等写入这点后面细说。所以 Checkpoint 间隔的设置是一个权衡太短会增加集群压力太长则故障时的重放数据量变大。一般生产环境设 30 秒到 3 分钟之间具体看集群承受能力和可接受的重放量。3. 从零把一个同步任务跑起来原理讲完进入动手环节。这一节给的是能直接复制粘贴的配置以及每一步为什么这么配。3.1 版本对齐最容易翻车的第一步FlinkCDC 的版本和 Flink 版本有严格的对应关系装错了启动就报NoSuchMethodError。下面是我实际验证过的组合Flink 版本FlinkCDC 版本连接器包名1.13.x2.2.xflink-sql-connector-mysql-cdc-2.2.x.jar1.14.x2.3.x / 2.4.xflink-sql-connector-mysql-cdc-2.4.x.jar1.15.x / 1.16.x2.4.x同上1.17.x / 1.18.x3.0.xflink-cdc-3.0.x-bin.tar.gz表里最后一行的 3.0 是个大变化它不再走 Flink SQL 的 connector 体系而是提供了一套独立的 Pipeline 框架用 YAML 描述 source 到 sink 的整条链路运行时自己起一个轻量集群。这个模式在整库同步场景下体验好很多后面单独说。选包的时候还有个细节优先用带sql前缀的 shade 包flink-sql-connector-mysql-cdc它把 Debezium、MySQL 驱动这些依赖都打进去了不用手动解决依赖冲突。不带前缀的那个包需要你自己保证 classpath 里有debezium-connector-mysql版本对不上就是一堆ClassNotFoundException。3.2 源库侧的四项准备动手之前源库必须满足四个条件缺一个都跑不起来。日志格式。执行SHOW VARIABLES LIKE binlog_format必须是 ROWSHOW VARIABLES LIKE binlog_row_image必须是 FULL。改这两个参数需要重启 MySQL8.0 里部分可以动态改但保险起见按重启规划。日志保留时间。执行SHOW VARIABLES LIKE expire_logs_days8.0 用binlog_expire_logs_seconds。这个值决定了 binlog 保留多久。它必须大于全量快照耗时 增量消费延迟的总和。假设一张大表全量要跑两小时增量追平还要半小时那日志至少要保留三小时以上建议直接设 7 天。这个参数配小了会导致一个特别隐蔽的故障全量阶段跑完了回头去消费 binlog发现最早的位点已经被清理直接报Could not find first log file name in binary log index file一切重来。复制账号权限。CDC 账号需要这几项SELECT读数据、RELOAD刷新元数据、SHOW DATABASES列库、REPLICATION SLAVE和REPLICATION CLIENT订阅日志。少一项就会在启动时报权限错误。CREATE USER cdc_user% IDENTIFIED BY your_password; GRANT SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO cdc_user%; FLUSH PRIVILEGES;GTID。如果源库开了 GTIDgtid_modeONFlinkCDC 支持用 GTID 位点容灾切换时更好处理。不开也能用走 binlog file position 的方式。3.3 一段能跑通的最小 SQL把 jar 包丢进 Flink 的lib目录重启集群然后进 SQL Client 执行下面的语句-- 源表定义 CREATE TABLE orders_source ( id BIGINT, order_no STRING, user_id BIGINT, amount DECIMAL(12,2), status INT, create_time TIMESTAMP(3), update_time TIMESTAMP(3), PRIMARY KEY (id) NOT ENFORCED ) WITH ( connector mysql-cdc, hostname 10.0.0.11, port 3306, username cdc_user, password your_password, database-name app_db, table-name orders, server-id 5400-5404, server-time-zone Asia/Shanghai, scan.incremental.snapshot.chunk.size 8096, scan.startup.mode initial ); -- 下游结果表这里用 print 演示实际换成 JDBC/ClickHouse/Doris CREATE TABLE orders_sink ( id BIGINT, order_no STRING, user_id BIGINT, amount DECIMAL(12,2), status INT, create_time TIMESTAMP(3), update_time TIMESTAMP(3), PRIMARY KEY (id) NOT ENFORCED ) WITH ( connector print ); INSERT INTO orders_sink SELECT * FROM orders_source;提交之后你会看到先有一批历史数据打出来全量阶段然后新插入或修改一条数据几秒内就会输出对应的变更增量阶段。这个先全量后增量的连贯体验就是 FlinkCDC 最直观的价值。几个参数的取值理由server-id给了五个 ID 的区间够并行度 4 使用并行度不能超过区间容量server-time-zone设了东八区不设的话时间字段会按 UTC 解析下游看到的时间差八小时这个坑非常常见scan.startup.mode设为initial表示先全量再增量如果只想从当前位点开始改成latest-offset。3.4 启动报错时先看这几条跑不起来的情况八成是下面四种之一报错关键字根因处理办法The MySQL server has a timezone offset没配server-time-zone加上该参数值与源库一致server id conflict多个任务 server-id 区间重叠规划互不重叠的 ID 段Access denied; you need the REPLICATION SLAVE privilege账号权限不全按上文补授权Table xxx has no primary key表无主键加主键或临时用全表扫描模式最后一条要单独说FlinkCDC 2.4 之前对无主键表的支持很有限如果表实在没法加主键比如一些日志表可以尝试用唯一索引替代或者用scan.incremental.snapshot.enabled false退回老的全量模式会锁表慎用。生产环境里我的态度很明确上 CDC 的表必须有主键没主键的表先治理别硬上。4. 全量切增量那几分钟才是真正的事故高发区任务跑起来了不代表跑稳了。真正容易出问题的不是长期运行阶段而是全量快照切到增量消费的那段时间以及运行中遇到的各种边界情况。4.1 大表全量阶段的耗时估算先给个经验值单块 8096 行走主键范围的顺序扫描在普通的 SSD 机器上大约是每秒读几万行。一张一千万行的表串行跑大概五分钟四并行能压到一分半。但这个数字波动很大取决于行宽、索引情况、源库负载。估算的意义在于反推 binlog 保留时间。假设你有二十张大表总计两亿行全量阶段跑下来要半小时那 binlog 保留时间至少得设一小时以上留足冗余。上线前一定要做一次耗时压测用生产数据的副本跑一遍全量把真实耗时摸清楚再回去调整expire_logs_days。这一步省不得。另外有个容易被忽略的点全量阶段会占用源库的读资源。如果源库本身负载就高全量扫描可能把 IO 打满影响业务。缓解办法是控制并行度和 chunk size把压力摊薄也可以在低峰期做初始化。4.2 更新主键会带来一条幽灵记录这是 CDC 同步里最经典的数据陷阱必须提前跟下游对齐。假设源表有一条id100的记录业务执行了UPDATE orders SET id200 WHERE id100。FlinkCDC 收到的是两条事件一条 delete旧值 id100一条 insert新值 id200。如果下游是 upsert 写入会先删掉 id100 再插入 id200一切正常。但如果下游写入方式不支持删除比如某些只追加的存储、或者你用INSERT INTO而没配主键那条 id100 的旧记录就会永远留在下游成为幽灵。更麻烦的是有些业务代码用更新主键来实现某种分表逻辑这种操作在 CDC 链路上往往是重灾区。我的处理方式是建表规范里明确禁止更新主键这条规则在数据同步场景下是硬性的。如果业务确实有这种需求就在下游做额外处理比如定期跑一次对账任务清理孤儿记录。4.3 数据类型映射的六个坑Flink SQL 和 MySQL 的类型不是一一对应的映射时会有信息损失或语义偏移。下面几个是我实际遇到过的TINYINT(1)会被映射成 BOOLEAN。MySQL 里TINYINT(1)常被用来存 0/1 布尔值但也有很多系统用它存状态码0、1、2、3。FlinkCDC 默认把TINYINT(1)当布尔处理状态码 2 和 3 会被截断成 true数据直接错了。解决办法是在 Flink 表定义里显式声明为TINYINT或者源库改用TINYINT(2)及以上。无符号整型溢出。BIGINT UNSIGNED的最大值超过了 Javalong的范围超过 9223372036854775807 的值会被当成负数。如果表里有雪花 ID 且用无符号存务必检查。通常建议 Flink 侧用DECIMAL(20,0)承接。DATETIME 精度丢失。MySQL 的DATETIME(3)带毫秒如果 Flink 侧声明成TIMESTAMP(0)毫秒会被截断。声明时要写TIMESTAMP(3)或TIMESTAMP(6)。时区。TIMESTAMP类型在 MySQL 里是按会话时区转换的DATETIME不带时区。混用会导致下游时间偏移。统一做法是源库全用DATETIMEFlink 侧配好server-time-zone下游按本地时区处理。DECIMAL 精度。Flink 的 DECIMAL 默认是DECIMAL(10,0)金额字段必须显式声明精度否则小数部分被抹掉。这个错误很隐蔽因为不报错只是数值不对。BLOB / TEXT。大字段同步会显著拖慢速度如果下游用不上建议在下游表定义里排除或者用 Flink SQL 的字段投影裁掉。全量阶段每个 chunk 都要把这些大字段读出来白白消耗带宽。4.4 源表 DDL 变更怎么办这是 FlinkCDC 2.x 的一个明显短板SQL 模式下 Schema 变更不会自动同步到 Flink 的表定义。源表加了一列Flink 侧的表结构还是老的新列的数据会被静默丢弃不报错。等业务发现下游缺字段时往往已经过了好几天。处理方式分两种。如果是新增列且下游需要只能改 Flink SQL 的表定义然后重启任务重启时如果从 Checkpoint 恢复位点还在不会重跑全量。如果只是加了一个下游不要的列可以暂时忽略。3.0 版本的 Pipeline 模式引入了 Schema Evolution支持部分 DDL 的自动同步比如加列但也不是全部 DDL 都支持改列类型这类操作依然要人工介入。所以建立一套 DDL 变更的通知机制是必要的——源库的 DDL 上线流程里加一步通知数据同步方比任何自动化解法都靠谱。4.5 大事务会把延迟拉爆一个事务里更新了五十万行这在批量订正、数据修复的场景下很常见。Debezium 解析这种大事务时需要把整个事务的所有变更事件在内存里缓冲等事务提交后才一并下发。缓冲期间下游看到的延迟会突然飙到几分钟甚至更久。缓解办法有两个一是源库侧控制事务大小大批量更新拆成小批次提交这个建议本身对数据库也更友好二是调大 Debezium 的缓冲参数debezium.max.queue.size和debezium.max.batch.size代价是内存占用上升。还有一些场景可以开启debezium.event.processing.failure.handling.mode来做容错但这属于兜底手段不解决根本问题。5. 并发、状态与反压让任务长期跑稳的参数逻辑配置能跑通只是及格线能在生产环境连续跑几个月不出事靠的是对几个关键参数的理解决策。5.1 并行度到底设多少前面说过全量阶段可并行、增量阶段受单流限制。所以并行度的设置要分情况看。如果这张表的存量数据很大千万级以上增量变更量不大那把并行度设高一点4 到 8能显著缩短初始化时间。如果存量不大但增量特别猛比如订单表每秒几千条变更并行度设再高也帮不上忙瓶颈在 binlog 解析这时候要考虑的是拆分任务或者换用更高吞吐的方案。有个细节并行度不能超过 server-id 区间的容量。配了server-id 5400-5404并行度最多 5。这个约束在改并行度时特别容易忘改完重启就报错。另外一个经验多个 CDC 任务跑在同一个 Flink 集群上时尽量把它们的执行资源隔开用单独的 Slot Group 或者干脆分集群。一个任务做全量时会把 IO 和网络打满殃及其他任务。5.2 Chunk size 的取舍scan.incremental.snapshot.chunk.size默认 8096这个值决定了全量阶段的切分粒度。调小比如 2000会让块更多、并行更均匀但每块的查询开销连接建立、结果集初始化占比上升调大比如 50000能减少查询次数但单块耗时长并行度不够时整体反而更慢而且块太大意味着状态里要记的水位信息更粗去重判断的窗口也更大。我的经验值表在一千万行以内用默认 8096超过一亿行用 20000 到 50000配合 8 以上并行度。同时在初始化完成后可以观察 Flink UI 里各个 subtask 的处理速率是否均衡如果明显有几块拖后腿说明切分不够均匀可以调小 chunk size 重来。5.3 Checkpoint 配置的三个关键值execution.checkpointing.interval: 60s execution.checkpointing.timeout: 10min execution.checkpointing.unaligned: true间隔 60 秒是个折中。CDC 任务的状态主要是位点和 chunk 水位体积不大60 秒一次的额外开销可以接受故障时最多重放 60 秒的数据。超时 10 分钟。CDC 任务在写下游慢的时候checkpoint 可能被反压拖住。超时设太短会导致 checkpoint 频繁失败任务陷入一直失败一直重试的循环。设长一点给反压留出恢复空间。非对齐 Checkpointunaligned。开了之后checkpoint 不再等待所有在途数据被处理完而是把在途数据也快照下来。在反压场景下这个开关能大幅提升 checkpoint 成功率。代价是状态体积会变大一些。在有明显反压的 CDC 任务上我一般都会打开它。还有一点状态后端建议用 RocksDB把状态放到本地磁盘而不是堆内存。CDC 任务的 chunk 水位信息在表特别多的时候可能达到几百 MB全放堆里容易 OOM。5.4 反压的定位路径任务跑着跑着延迟越来越高Flink UI 上看到 source 的currentFetchEventTimeLag持续上涨这就是典型的反压。排查顺序应该是从下游往上游倒推。先看 Sink。写 JDBC 是不是单条 insert 逐条提交改成批量写入batch size 500 到 1000吞吐能提升一个数量级。写 ClickHouse/Doris 是不是没走批量导入接口这些库对单条写入的支持很差必须走攒批。这部分优化往往能解决 80% 的反压问题。再看中间算子。有没有做窗口聚合、双流 join 这种重操作有的话检查状态是否在无限增长考虑加 TTL。最后看 Source。如果下游都优化完了还是跟不上那就是源库的变更量确实超过单任务处理能力了考虑拆分或者升级架构。有个工具值得推荐Flink 的currentFetchEventTimeLag和currentEmitEventTimeLag两个指标前者表示数据产生到被 source 读取的延迟后者表示读取到下发到下游的延迟。前者高说明源头堆积消费不过来后者高说明下游卡住。用这两个指标能快速定位瓶颈在哪一段。6. 上线之后怎么确认下游的数是对的同步任务跑起来、指标看着正常不代表数据就是对的。数据一致性这块必须有一套独立的验证手段别指望没报错就是对的。6.1 幂等写入是底线先明确一个事实FlinkCDC 配合 Checkpoint 能保证**至少一次at-least-once**语义故障恢复时会有少量数据重放。要做到精确一次需要下游配合。下游是数据库的话用 upsert 语义写入JDBC connector 配INSERT INTO ... ON DUPLICATE KEY UPDATE或者 Flink 表定义里声明主键让 connector 自动处理。这样重放的数据会覆盖而不是追加天然幂等。下游是消息队列的话消费端要做去重通常用业务主键加版本号或者消费位点做判断。下游是 ClickHouse 这类不支持更新的库用 ReplacingMergeTree 引擎靠主键去重注意它的去重是最终一致的查询时要用FINAL或者接受短暂重复。如果下游既不支持 upsert 又不去重那这个链路的一致性就是没保障的早晚出事。6.2 三层对账机制我一般会建三层校验从粗到细第一层行数对比。每天凌晨跑一次SELECT COUNT(*)对比源表和目标表。这个最粗糙但能发现大规模的数据丢失。缺点是行数相同不代表内容相同。第二层关键字段聚合对比。选几个有代表性的数值字段做 SUM/AVG加上按时间分组的趋势对比。金额类的字段尤其要做这个能发现精度丢失、符号错误这类问题。第三层抽样明细对比。随机抽几千条主键逐字段比对源和目标的完整记录。这个能发现字段错位、类型截断这类细节问题。三层都跑一遍的成本不低实际落地时我是这么安排的行数和聚合对比每天跑抽样对比每周跑一次全量对比只在重大变更后做一次。另外全量初始化刚结束的那一次三层都必须跑因为全量阶段是数据问题最集中的地方。6.3 必须盯住的四个监控指标除了 Flink 自带的任务指标CDC 链路还有几个专用指标需要单独配告警指标含义告警阈值建议currentFetchEventTimeLag数据产生到被读取的延迟持续 5 分钟 60 秒numRecordsInPerSecondsource 每秒处理记录数骤降 50% 以上可能是链路断了lastCheckpointDuration最近一次 checkpoint 耗时接近超时阈值numRestarts任务重启次数10 分钟内 3 次其中currentFetchEventTimeLag是最重要的一个。它不只看业务延迟还能反映 binlog 消费是否停滞。如果这个值持续上涨且不回落通常是下游卡住或者源库那边出了问题比如有人做了大事务、主库切换导致位点失效。还有一类监控容易被忽略源库的 binlog 生成速率。如果突然飙升到平时的十倍说明有异常的大批量操作这时候要提前预警别等下游撑不住了才发现。6.4 主备切换后的位点处理MySQL 主从切换是运维常态但对 CDC 任务是生死考验。切换后新主库的 binlog file 名可能跟老库不一样尤其是不开 GTID 的环境连接器按老的位点去找会失败。开 GTID 是首选方案。GTID 是全局唯一的事务 ID不依赖文件名主备切换后连接器能自动按 GTID 定位恢复过程对用户透明。这也是我建议生产库一律开 GTID 的原因之一另一个原因是主从切换本身更方便。没开 GTID 的环境切换后大概率要人工介入从最近的 checkpoint 恢复并手动指定位点。这种情况下最好提前把切换流程和 CDC 任务的恢复步骤写进运维手册别等到半夜切换时再临时摸索。7. 版本 3.0 的 Pipeline 模式值不值得迁最后聊一下 3.0因为这两年问的人很多。3.0 最大的变化是引入了 Pipeline 框架整库同步变成一个 YAML 文件的事不再需要为每张表写一段 Flink SQL。7.1 一个 YAML 搞定整库同步source: type: mysql hostname: 10.0.0.11 port: 3306 username: cdc_user password: your_password tables: app_db\.orders,app_db\.users server-id: 5400-5404 server-time-zone: Asia/Shanghai sink: type: jdbc url: jdbc:mysql://10.0.0.12:3306/dw username: dw_user password: your_password pipeline: name: mysql-to-dw-sync parallelism: 4提交命令是bin/flink-cdc.sh job.yaml它会自动起一个 Flink 作业。相比 SQL 模式好处很明显表结构自动映射、新增表可以自动捕获不用改代码、Schema Evolution 支持加列同步。但我不会无脑推荐所有人升级。3.0 的生态成熟度、社区踩坑资料都比 2.4 少一截遇到问题能查到的答案不多。如果你的场景是整库同步到数仓且团队对 Flink SQL 不熟3.0 值得上如果是少量关键表的精细化同步2.4 的稳定性和可控性更好。升级前把关键任务的 2.4 配置和 3.0 的 YAML 都跑一遍用同样的数据量对比延迟和资源占用再决定。7.2 迁移时容易忽略的兼容性问题从 2.4 迁到 3.0有几个地方会咬人。连接器命名变了。2.4 里connector mysql-cdc3.0 的 YAML 里写type: mysql看着是简化了但如果你同时维护两套任务很容易混淆。部分参数名改了。比如scan.incremental.snapshot.chunk.size在 3.0 里依然存在但有些 Debezium 的透传参数前缀从debezium.变成了别的形式具体要查对应版本的文档。升级前把现有配置逐项对照一遍别指望完全兼容。状态不兼容。3.0 的 state 格式跟 2.4 不同不能从 2.4 的 savepoint 恢复。这意味着迁移时必须重新做一次全量或者手工把位点搬过去。如果表特别大这个切换窗口要提前规划最好在业务低峰期做。Sink 的写法变了。3.0 内置了一批 SinkJDBC、Kafka、Doris 等如果下游是比较少见的存储可能还得自己写或者退回 2.4。我个人在几个项目里的实际体会是新项目、整库同步、下游是主流数仓的直接上 3.0省下的重复配置工作量很可观存量大、任务多、下游五花八门的老系统先别动等 3.x 再迭代一两个小版本看看稳定性再说。技术选型最怕的就是为了尝鲜把稳定跑了半年的链路推倒重来那个代价远比省下的几段配置代码高。7.3 一个容易被低估的细节下游表的建表语句不管用 2.4 还是 3.0下游表的建表语句都值得单独说。很多人直接把源表结构复制过去改个名字结果踩一堆坑。首先是主键必须带。下游表没有主键就无法做 upsert重放数据会变成重复行。哪怕源表没主键暂时没法加下游也要想办法造一个比如用几个字段组合否则一致性没法保证。其次是字符集和排序规则。源表是utf8mb4下游建成utf8遇到四字节字符emoji、生僻字就会截断或报错。这个错误在测试环境往往发现不了因为测试数据都是纯英文数字。然后是字段长度。源表VARCHAR(255)下游建成VARCHAR(100)看起来只是省点空间实际上超过 100 的数据写入时会报错或者被截断而且这类错误在批量写入时可能被吞掉。下游的字段长度一律不小于源表这是我定的死规矩。最后是时间字段的类型。源库DATETIME下游建成TIMESTAMP后者在 MySQL 里有 2038 年上限而且受时区影响。统一用DATETIME能省掉一堆麻烦。这些看起来都是细枝末节但真正在生产环境出问题时往往就是这些看着不重要的地方在捣鬼。我在一个项目里见过下游表把DECIMAL(12,2)写成了DECIMAL(12,0)跑了三个月才发现所有金额的小数部分都没了补数据补了整整两天。从那以后我的习惯是每次建完下游表都写一条脚本自动比对源表和目标表的information_schema.columns字段类型、长度、精度、可空性逐项对比把差异打印出来人工过一遍这个投入产出比极高。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑