4、MySQL Binlog原理:Binlog格式、写入读取机制与GTID模式
聊到CDC增量同步,Binlog是绕不开的核心。说白了,MySQL的Binlog就是一份「操作日记」,记录了所有数据变更。我刚开始接触CDC时,以为只要开了Binlog就能随便同步,结果被格式和模式坑了好几次。今天咱们把这块彻底捋清楚。
4.1 Binlog的三种格式
MySQL提供了三种Binlog格式:Statement、Row和Mixed。每种格式都有自己的脾气,选错了轻则数据不一致,重则同步直接崩掉。
| 格式 | 记录方式 | 优点 | 缺点 |
|---|---|---|---|
| Statement | 记录执行的SQL语句 | 日志量小,易于阅读 | 非确定性函数可能导致主从不一致 |
| Row | 记录每一行数据的变化 | 精确可靠,任何情况都一致 | 日志量大,尤其大事务 |
| Mixed | 混合模式,自动选择 | 兼顾两者优点 | 逻辑复杂,偶有意外 |
4.1.1 Statement格式
Statement格式记录的是原始SQL。比如你执行UPDATE user SET age=age+1 WHERE id=1,Binlog里就原样记下这条SQL。从库重放时再执行一遍。
听起来挺合理对吧?但问题来了——非确定性函数。比如NOW()、UUID()、RAND(),主库执行时是一个值,从库重放时又是另一个值。我曾经遇到一个案例,主库用SYSDATE()做分区键,结果从库数据全跑偏了。排查了一整天才发现是Statement格式惹的祸。
NOW()、UUID()、RAND()等函数的操作,建议改用Row格式。
4.1.2 Row格式
Row格式记录的是每一行数据变更前后的完整镜像。同样是UPDATE user SET age=age+1 WHERE id=1,Row格式会记录:
# 变更前:id=1, name='张三', age=25
# 变更后:id=1, name='张三', age=26
这有什么好处?精确。不管你怎么折腾,从库重放时拿到的就是确定的数据。CDC工具(比如Canal、Debezium)也最喜欢Row格式,因为可以直接解析出变更前后的数据,不需要再回查数据库。
但代价也很明显——日志量暴增。一个UPDATE影响100万行,Statement格式可能就一行SQL,Row格式要记录100万行的前后镜像。我见过一个极端案例,某电商大促时一个批量更新产生了50GB的Binlog,直接把磁盘撑爆了。
4.1.3 Mixed格式
Mixed格式是MySQL的「智能选择」模式。默认情况下使用Statement格式,遇到非确定性语句时自动切换为Row格式。听起来很完美?实际用起来有点尴尬。
我个人不太推荐Mixed格式做CDC。为什么?因为CDC工具解析时,需要同时处理两种格式的解析逻辑,增加了复杂度。而且「自动切换」的时机并不总是符合预期。我曾经踩过一个坑:某个存储过程里混合了确定性和非确定性操作,Mixed格式把整个事务都切成了Row,日志量暴涨,但CDC工具却因为解析逻辑bug丢了一部分数据。
4.2 Binlog的写入与读取机制
理解了格式,咱们再看看Binlog到底是怎么写进去、怎么读出来的。这部分对理解CDC的延迟和性能至关重要。
4.2.1 写入机制
Binlog的写入流程大致如下:
- 事务执行:MySQL Server层执行SQL,生成事件(Event)。
- 写入Binlog Cache:每个事务的Binlog事件先写入线程私有的Binlog Cache。
- 刷盘:事务提交时,将Cache中的内容写入Binlog文件,并根据
sync_binlog参数决定是否刷盘。 - 通知从库/CDC:写入完成后,通知Dump线程有新数据可读。
这里有个关键参数——sync_binlog:
| 值 | 行为 | 性能 | 安全性 |
|---|---|---|---|
| 0 | 由操作系统决定刷盘时机 | 最高 | 最低(宕机可能丢数据) |
| 1 | 每次事务提交都刷盘 | 最低 | 最高(不丢数据) |
| N | 每N个事务提交刷一次盘 | 中等 | 中等(最多丢N个事务) |
做CDC时,我建议sync_binlog设为1。虽然写入性能会下降,但能保证CDC工具读到完整的数据。你想想看,如果CDC刚读到一半,主库宕机丢了最后几个事务,那同步的数据就不完整了。
4.2.2 读取机制
Binlog的读取依赖Dump线程。CDC工具(或从库)连接到主库后,主库会启动一个Dump线程,负责把Binlog事件推送给客户端。
读取流程:
- 注册Slave:客户端发送
COM_REGISTER_SLAVE命令注册自己。 - 请求Binlog:客户端发送
COM_BINLOG_DUMP或COM_BINLOG_DUMP_GTID,指定起始位置。 - 持续推送:Dump线程从指定位置开始,持续推送Binlog事件。
- ACK确认:客户端处理完事件后,发送ACK确认位置。
关键点:Dump线程是单线程推送的。如果CDC工具处理速度跟不上,Dump线程会积压事件,导致主库的Binlog文件无法及时清理。我曾经遇到一个案例,CDC工具挂了半天,Binlog文件堆积到200GB,恢复后Dump线程追了好久才追上。
4.3 GTID模式
GTID(Global Transaction Identifier)是MySQL 5.6引入的杀手级特性。它给每个事务分配一个全局唯一的ID,格式为UUID:序列号。比如d3b1c2a4-5e6f-7890-abcd-ef1234567890:12345。
4.3.1 为什么需要GTID?
没有GTID时,主从同步依赖文件名+偏移量。比如mysql-bin.000001:12345。这有什么问题?
- 主库切换后位置失效:新主库的Binlog文件名和偏移量完全不同,从库需要重新找位置。
- 难以判断事务是否已执行:从库不知道某个事务是否已经重放过,容易重复执行。
- 搭建复制链路复杂:需要手动指定
MASTER_LOG_FILE和MASTER_LOG_POS。
GTID完美解决了这些问题。每个事务有唯一ID,从库可以精确知道哪些事务已经执行过,哪些还没执行。主库切换时,从库自动从GTID集合中找出缺失的事务继续同步。
4.3.2 GTID的生命周期
一个事务从产生到被CDC工具消费,GTID经历了以下阶段:
- 分配:事务开始时,MySQL分配一个新的GTID。
- 写入:事务提交时,GTID随Binlog事件一起写入。
- 同步:从库或CDC工具读取Binlog,获取GTID。
- 记录:从库将已执行的GTID记录到
gtid_executed表中。 - 清理:MySQL根据
gtid_purged清理已过期的GTID。
4.3.3 GTID模式的配置
开启GTID模式需要修改MySQL配置文件:
[mysqld]
gtid_mode = ON
enforce_gtid_consistency = ON
log_bin = mysql-bin
binlog_format = ROW
server_id = 1
注意enforce_gtid_consistency必须设为ON,否则MySQL会禁止可能导致GTID不一致的语句(比如CREATE TABLE ... SELECT)。
4.3.4 GTID与CDC的配合
CDC工具(如Canal、Debezium)都原生支持GTID模式。连接时只需要指定GTID集合的起始位置,工具会自动从该位置开始消费。
举个例子,Canal连接GTID模式的MySQL:
canal.instance.master.address = 192.168.1.100:3306
canal.instance.dbUsername = canal
canal.instance.dbPassword = canal
canal.instance.gtidon = true
# 指定起始GTID集合,为空则从最新位置开始
canal.instance.gtid.start = d3b1c2a4-5e6f-7890-abcd-ef1234567890:1-1000
我个人习惯在生产环境强制使用GTID模式。虽然配置多了一步,但换来的是主库切换时的零人工干预。有一次线上主库硬件故障,我们直接把CDC工具指向新主库,GTID自动对齐,全程没丢一条数据。
4.4 本章核心知识图谱
下面这张图展示了Binlog格式、写入读取机制和GTID模式之间的关系:
4.5 避坑总结
最后,我把这些年踩过的坑和总结的经验列出来,希望能帮你少走弯路:
- 格式选择:做CDC增量同步,无脑选Row格式。Statement格式省的那点磁盘空间,不够你排查一次数据不一致的。
- sync_binlog:设为1,别犹豫。CDC工具读到一半数据丢了,比性能下降更可怕。
- GTID持久化:一定要把消费过的GTID集合持久化到外部存储(ZooKeeper、数据库、文件都行)。CDC工具重启时,从持久化位置继续消费,避免重复或遗漏。
- 监控Binlog堆积:定期检查Binlog文件数量和磁盘使用率。CDC工具挂了要及时恢复,否则Binlog堆积会撑爆磁盘。
- 大事务处理:Row格式下,大事务会产生巨量Binlog。建议拆分大事务,或者对CDC工具做限流处理。
UPDATE全表,Row格式下每次产生2GB Binlog。CDC工具处理不过来,Dump线程积压,最终Binlog文件堆积到300GB,磁盘报警。从那以后,我强制要求所有批量操作必须分页执行,并且监控Binlog生成速率。