3. Debezium深度解析
Debezium 这东西,说白了就是 CDC 领域的事实标准。我最早接触它是在 2019 年,那时候公司要从 Oracle 迁到 MySQL,业务数据不能停,还得实时同步到数仓。试了一圈工具,最后还是选了 Debezium。为什么?因为它基于 Kafka Connect,生态好,社区活跃,而且对数据库的侵入性极低。
这一章,我会把 Debezium 的核心拆开来讲。架构长什么样?Connector 怎么配?Snapshot 和 Streaming 两种模式到底怎么切换?嗯,咱们一个一个来。
3.1 Debezium 整体架构
先看一张图,把 Debezium 的骨架搭起来。
从图里能看出来,Debezium 不是单打独斗的。它寄生在 Kafka Connect 框架里,通过 Connector 去读数据库的变更日志。我个人习惯把这条链路分成四层:
- 源数据库层:MySQL 的 binlog、PostgreSQL 的 WAL、MongoDB 的 oplog,这些都是 CDC 的原始数据来源。
- Connector 层:每个数据库类型对应一个专用 Connector。它负责解析日志格式,把行变更转成统一的事件结构。
- Kafka Connect 层:负责 Connector 的生命周期管理、任务分配、偏移量持久化。说白了,就是让 Connector 能跑在集群里,挂了能自动恢复。
- Kafka 消息层:变更事件被序列化成 Avro/JSON/Protobuf,写入 Topic。下游想怎么消费都行。
💡 我的经验:刚开始用 Debezium 时,我总把 Connector 和 Kafka Connect 混为一谈。其实 Connector 是插件,Kafka Connect 是运行时。你配的是 Connector,但真正干活的是 Kafka Connect 进程。
3.2 Connector 配置详解
配置 Connector 是上手 Debezium 的第一步。我见过不少人在这一步栽跟头——配错了参数,要么连不上库,要么丢数据。咱们拿 MySQL Connector 举例,把关键参数过一遍。
3.2.1 核心配置项
| 参数名 | 说明 | 必填 | 默认值 |
|---|---|---|---|
connector.class |
Connector 全限定类名 | 是 | — |
database.hostname |
数据库主机地址 | 是 | — |
database.port |
数据库端口 | 是 | 3306 |
database.user |
数据库用户名 | 是 | — |
database.password |
数据库密码 | 是 | — |
database.server.name |
逻辑名称,用于 Topic 前缀 | 是 | — |
database.include.list |
要监听的数据库列表 | 否 | 全部 |
table.include.list |
要监听的表列表 | 否 | 全部 |
snapshot.mode |
快照模式 | 否 | initial |
offset.storage |
偏移量存储后端 | 否 | Kafka |
下面是一个完整的 MySQL Connector 配置示例:
{
"name": "mysql-connector-orders",
"config": {
"connector.class": "io.debezium.connector.mysql.MySqlConnector",
"database.hostname": "192.168.1.100",
"database.port": "3306",
"database.user": "debezium_user",
"database.password": "debezium_pass",
"database.server.name": "prod_orders",
"database.include.list": "orders_db",
"table.include.list": "orders_db.orders, orders_db.order_items",
"snapshot.mode": "initial",
"snapshot.locking.mode": "minimal",
"tombstones.on.delete": "false",
"decimal.handling.mode": "double",
"include.schema.changes": "true"
}
}
⚠️ 避坑指南:我曾经在生产环境配漏了 database.server.name,结果 Topic 名全乱了。这个值会作为 Topic 前缀,一旦上线再改,历史数据全对不上。建议一开始就定好命名规范,比如 环境_业务线 的格式。
3.2.2 权限配置
Debezium 需要数据库用户有特定权限。拿 MySQL 来说:
SELECT:读取表结构,做快照RELOAD:执行FLUSH TABLES WITH READ LOCK,保证快照一致性REPLICATION CLIENT:查看主从状态REPLICATION SLAVE:读取 binlog
我习惯单独建一个用户,只给最小权限。别偷懒用 root,安全审计过不去。
3.3 Snapshot 模式:初次启动的基线数据
Connector 第一次启动时,数据库里已经有一堆数据了。Debezium 不会直接跳进 Streaming 模式——它会先做一次 Snapshot,把当前全量数据拍下来,然后再开始监听增量变更。
Snapshot 模式有几种,我列个表对比一下:
| 模式 | 行为 | 适用场景 |
|---|---|---|
initial |
先做全量快照,再切到 Streaming | 默认模式,首次启动 |
when_needed |
只在需要时做快照(如偏移量丢失) | 容错恢复 |
never |
不做快照,直接开始 Streaming | 已有基线数据,只接增量 |
schema_only |
只拍表结构,不拍数据 | 只关心 DDL 变更 |
schema_only_recovery |
恢复时只重建 schema | 偏移量丢失但 schema 还在 |
Snapshot 的执行流程大致是这样的:
- Connector 获取全局读锁(如果配置了
minimal模式,只锁相关表) - 读取当前 binlog 位置
- 逐表扫描,把每一行数据作为
read事件写入 Kafka - 释放锁
- 记录偏移量,切换到 Streaming 模式
🔑 关键点:Snapshot 阶段产生的每条记录,op 字段是 r(read),而不是 c(create)。下游消费时可以通过这个字段区分是快照数据还是实时变更。
3.4 Streaming 模式:持续捕获增量变更
Snapshot 完成后,Connector 自动进入 Streaming 模式。这时候它不再扫描表,而是蹲在 binlog 上,等着新事件冒出来。
Streaming 模式的核心机制:
- 轮询 binlog:每隔
poll.interval.ms(默认 500ms)拉一次新事件 - 事件转换:把 binlog 事件转成 Debezium 的 Change Event 格式
- 偏移量提交:每处理一批事件,就把当前 binlog 位置写到 offset topic
- 容错:如果进程挂了,重启后从上次提交的偏移量继续读
一个典型的 Change Event 长这样:
{
"schema": { ... },
"payload": {
"before": { "id": 1001, "status": "pending" },
"after": { "id": 1001, "status": "shipped" },
"source": {
"version": "1.9.7",
"connector": "mysql",
"name": "prod_orders",
"ts_ms": 1696000000000,
"snapshot": false,
"db": "orders_db",
"table": "orders",
"server_id": 123,
"gtid": null,
"file": "mysql-bin.000023",
"pos": 45678,
"row": 0
},
"op": "u",
"ts_ms": 1696000001000,
"transaction": null
}
}
注意 op 字段:c 是创建,u 是更新,d 是删除。你想想看,下游只要解析这个字段,就能知道数据发生了什么变化。
💡 我的习惯:Streaming 模式下,我会把 tombstones.on.delete 设为 false。墓碑消息是 Kafka 的压缩机制用的,对 CDC 消费端没啥用,还占磁盘。关了它,Topic 能小不少。
3.5 Snapshot 与 Streaming 的衔接
这里有个容易踩的坑:Snapshot 和 Streaming 之间怎么保证数据不丢不重?
Debezium 的做法是:
- 在 Snapshot 开始前,先记录当前 binlog 位置(称为 binlog 锚点)
- Snapshot 过程中,所有新产生的变更都在锚点之后
- Snapshot 完成后,从锚点开始消费 Streaming 事件
这样一来,Snapshot 和 Streaming 的数据在时间线上是连续的。不会因为快照时间长而漏掉中间产生的变更。
⚠️ 我曾经踩过的坑:有一次 Snapshot 跑了 40 分钟,期间业务库有大量更新。我以为 Debezium 会自动合并,结果发现下游出现了重复数据。后来查文档才知道,Snapshot 和 Streaming 是独立的两条管道,如果下游没有做幂等处理,重复是必然的。解决方案是在消费端按 source.file + source.pos 做去重。
3.6 实战建议
最后,给几个我在生产环境总结出来的建议:
- 先做 Schema Only 快照:如果表很大,先跑一次
schema_only模式,确认 Connector 能正常启动,再切到initial做全量。 - 监控偏移量延迟:用 Kafka Lag 指标监控 Connector 的消费进度。如果延迟持续增长,说明 Connector 处理能力跟不上写入速度。
- 不要频繁重启 Connector:每次重启都会触发一次新的 Snapshot(如果配置了
initial模式)。我见过有人每天重启一次,结果 Snapshot 永远跑不完。 - 测试环境用
never模式:开发测试时,如果已经有基线数据了,直接用snapshot.mode=never,省去快照时间。