2. Flink CDC 核心原理:架构、日志捕获与Changelog Stream

好,咱们进入正题。Flink CDC 到底是怎么工作的?很多人一开始觉得它就是个“数据库的 binlog 消费者”,其实没那么简单。我最早接触 CDC 时也这么想,结果在线上踩了个大坑——数据对不上。后来才明白,它的核心是 Snapshot + Streaming 的无缝衔接,以及一套统一的 Changelog Stream 抽象。

这一节,我把这几个关键点拆开讲清楚。你理解透了,后面写代码才不会懵。

2.1 Flink CDC 的整体架构

先看一张图,这是 Flink CDC 的顶层设计。我习惯把它分成三层:

核心分层:

  • Source Connector 层:对接具体数据库(MySQL、PostgreSQL、Oracle、MongoDB 等),负责拉取原始变更数据。
  • Debezium 引擎层:内置 Debezium 作为日志解析引擎,把 binlog/redo log 转成统一的事件格式。
  • Flink Runtime 层:将 Debezium 事件包装为 Flink 的 DataStream,并处理状态、Checkpoint、Exactly-Once 语义。
Flink CDC 三层架构图 Source Connector 层 MySQL CDC Connector | PostgreSQL CDC Connector | Oracle CDC Connector | MongoDB CDC Connector Debezium 引擎层 解析 binlog/redo log → 统一 Change Event(JSON/Avro) Flink Runtime 层 DataStream API | Changelog Stream | Checkpoint | Exactly-Once

每一层各司其职。Source Connector 负责“怎么连”,Debezium 负责“怎么解析”,Flink 负责“怎么算”。我个人觉得,这种分层最大的好处是——你换数据库只需要换最上面那层,下面的逻辑完全不用动。

2.2 基于日志的增量捕获原理

CDC 的核心是“读日志”,不是“查表”。为什么?

你想想看,如果每隔几秒去查一次表,用 SELECT * FROM table WHERE update_time > last_time,会有什么问题?

  • 数据延迟高:轮询间隔决定了实时性上限。
  • 无法捕获删除:行被删了,你就查不到了。
  • 对数据库有压力:频繁查询,尤其是大表,容易把库拖垮。

而基于日志的方式,是直接读取数据库的 事务日志(MySQL 的 binlog、PostgreSQL 的 WAL、Oracle 的 Redo Log)。

我的经验: 曾经有个项目,业务方要求秒级同步,但数据库是 MySQL 5.7 的从库,轮询方案延迟一直在 10 秒以上。换成 Flink CDC 读 binlog 后,延迟直接降到 200 毫秒以内。而且对数据库几乎零影响——因为 binlog 本来就是 MySQL 主从同步在用的东西,你只是多了一个消费者而已。

Debezium 在底层做了这么几件事:

  1. 模拟 MySQL Slave:向 Master 注册为一个 Slave,请求 binlog 流。
  2. 解析 binlog 事件:把二进制格式的 binlog event 解析成结构化的 Change Event。
  3. 生成统一 Schema:每个表对应一个 Schema,包含所有字段的元数据。

举个例子,一条 MySQL 的 UPDATE 语句,在 binlog 里会被记录为:

{
  "before": {"id": 1, "name": "Alice", "age": 30},
  "after":  {"id": 1, "name": "Alice", "age": 31},
  "op": "u",   // u = update
  "ts_ms": 1700000000000
}

你看,before 和 after 都给你了。这在做数据对比、回滚、审计时特别有用。

2.3 Changelog Stream 概念

Flink CDC 把 Debezium 的 Change Event 进一步抽象成了 Changelog Stream。说白了,就是一张表的“变更日志流”。

这个流里每条记录都带一个 RowKind 标记,表示这条数据是:

RowKind 含义 对应数据库操作
INSERT 插入一条新数据 INSERT
UPDATE_BEFORE 更新前的旧数据(回撤流用) UPDATE
UPDATE_AFTER 更新后的新数据 UPDATE
DELETE 删除一条数据 DELETE

为什么要分这么细?因为 Flink 的 流计算需要处理回撤。比如你做 GROUP BY 聚合,一条数据从 30 岁变成 31 岁,Flink 需要先“撤回”旧的 30 岁,再“追加”新的 31 岁。没有 UPDATE_BEFORE,聚合结果就会出错。

注意: 如果你用 GROUP BY 或 JOIN 处理 CDC 数据,一定要保留 UPDATE_BEFORE 事件。我曾经见过有人图省事只取 UPDATE_AFTER,结果聚合结果越算越偏,排查了半天才发现是回撤事件丢了。

2.4 Snapshot 与 Streaming 的衔接

这是 Flink CDC 最巧妙的地方,也是很多人容易搞混的点。

一个 CDC 任务启动时,数据库里已经有很多历史数据了。你不能只从 binlog 开始读——那样会漏掉存量数据。所以 Flink CDC 把整个过程分成两个阶段:

阶段一:Snapshot(快照阶段)

  • 任务启动后,先对目标表执行 SELECT *,获取当前全量数据。
  • 同时记录当前的 binlog 位置(比如 MySQL 的 GTID 或文件名+偏移量)。
  • Snapshot 阶段的数据会被标记为 INSERT 事件,发送到下游。

阶段二:Streaming(流式阶段)

  • Snapshot 完成后,从之前记录的 binlog 位置开始消费增量数据。
  • 后续的 INSERT/UPDATE/DELETE 都会实时推送到下游。

关键问题来了:Snapshot 和 Streaming 之间怎么保证数据不重不漏?

嗯,这里要注意。Flink CDC 的做法是:

  1. 在 Snapshot 开始时,记录一个 低水位线(low watermark),即当前的 binlog 位置。
  2. Snapshot 过程中,所有读到的数据都带上这个低水位线。
  3. Streaming 阶段从低水位线开始消费。
  4. 如果 Snapshot 过程中有数据发生了变更(比如一条数据被更新了),Flink CDC 会通过 Chunk 分片 + 动态发现 机制来修正——简单说,它会重新读取那个分片的数据,确保最终一致性。

一句话总结: Snapshot 保证“存量数据不丢”,Streaming 保证“增量数据不丢”,两者通过 binlog 偏移量衔接,配合 Chunk 分片机制保证“不重”。

下面这张图展示了 Snapshot 与 Streaming 的衔接流程:

Snapshot → Streaming 衔接流程 阶段1:Snapshot SELECT * FROM table 记录 binlog 偏移量 Chunk 分片 + 动态发现 分片读取,检测变更 修正不一致的分片 阶段2 Streaming 实时增量 衔接关键:Snapshot 开始时记录 binlog 偏移量(低水位线) Streaming 从该偏移量开始消费,保证不重不漏

我记得第一次在生产环境用 Flink CDC 时,最担心的就是 Snapshot 阶段数据量太大(几亿条),怕把数据库打挂。后来发现 Flink CDC 默认会做 Chunk 分片——把大表按主键拆成多个小分片,每个分片单独读取,而且读取时还会加锁控制并发度。嗯,这个设计很贴心。

避坑指南: 如果你的表没有主键,Flink CDC 的 Snapshot 会退化成单线程读取,速度会慢很多。我曾经遇到一个日志表没设主键,Snapshot 跑了 40 分钟才读完。后来加了自增主键,时间缩短到 5 分钟。所以,CDC 的表尽量要有主键。

2.5 小结

这一节我们拆解了 Flink CDC 的核心原理。说白了就是三件事:

  • 架构分层:Source Connector → Debezium → Flink Runtime,各层职责清晰。
  • 日志捕获:读 binlog 而不是查表,实时性高、对库无压力。
  • Changelog Stream:用 RowKind 标记每条数据的变更类型,支持回撤流。
  • Snapshot + Streaming:通过 binlog 偏移量衔接,Chunk 分片保证一致性。