MySQL 主从同步原理与延迟排查
问题:为什么主从复制会延迟,怎么查、怎么修?
主从复制是 MySQL 高可用架构的基石——读写分离、灾备、在线变更都靠它。但几乎所有维护过 MySQL 的人都被「主从延迟」坑过:读请求打到从库拿到的是脏数据,监控报警响个不停,DBA 紧急上线处理。
本文从复制原理出发,先讲清楚三个线程如何协作、binlog 格式的影响,再聚焦延迟的根因排查和实际解法,最后给出一个可直接落地的指标体系。
一、主从复制三线程模型
MySQL 主从复制本质上是异步的 binlog 分发 + 重放。参与方只有三个线程:
主库 从库
┌──────────────┐ ┌──────────────────┐
│ 提交事务 │ ──→ │ IO Thread │
│ ↓ │ binlog │ ↓ │
│ Binlog Dump │───────→│ Relay Log │
│ Thread │ │ ↓ │
└──────────────┘ │ SQL Thread │
│ ↓ │
│ 应用数据 │
└──────────────────┘- Binlog Dump Thread(主库):事务提交后写入 binlog,Dump 线程负责顺着 binlog 位置把事件推送给从库。主库为每个从库启动一个独立的 Dump 线程——如果有 3 个从库,主库上就有 3 个 Dump 线程。
- IO Thread(从库):接收事件,写入 relay log。这一步只涉及网络 I/O 和顺序写,速度通常很快。
- SQL Thread(从库):读取 relay log 并重放 SQL。这是整个复制链路中最慢的一环,也是延迟的起点。
核心矛盾:主库可以多线程并发写,但从库的 SQL Thread 在 5.6 之前是单线程,5.6 之后才逐步引入并行回放。写入量一大,SQL Thread 必然跟不上。
1.1 binlog 格式对复制的关键影响
不少开发者只关心 binlog 开没开,却不关心格式。三种格式对复制行为影响巨大:
| 格式 | 记录内容 | 体积 | 从库重放行为 | 延迟风险 |
|---|---|---|---|---|
| STATEMENT | 原始 SQL | 小 | 从库重新执行 SQL,依赖上下文 | 高——NOW()、LIMIT 等非确定性函数可能产生不一致 |
| ROW(5.7+ 默认) | 每行变更前/后快照 | 大(单条 UPDATE 影响 1000 行则记录 1000 行前后镜像) | 直接应用行变更,无需上下文 | 低——但 binlog 体积大,网络传输压力大 |
| MIXED | 自动切换 | 中 | 根据语句是否确定性自动选择 | 中——复杂场景下会自动降级为 ROW |
生产坑:某团队把 binlog_format=STATEMENT 跑在读写分离架构上,从库执行 DELETE FROM t LIMIT 1 时因为主从数据排序不同,删了不同的行,导致主从数据永久不一致。线上必须用 ROW 格式,这是硬性红线。
ROW 格式的代价是 binlog 体积膨胀。实测:同样的 UPDATE t SET c=1 WHERE id IN (SELECT id FROM t2 WHERE ...) 影响 5000 行,STATEMENT 格式 binlog 约 2KB,ROW 格式约 1.2MB——差了 600 倍。所以网络带宽也成了延迟瓶颈。
二、延迟的成因分析
2.1 大事务——最直接的元凶
一个 DELETE FROM orders WHERE create_time < '2024-01-01' 删了 200 万行,主库可能 2 秒完成,但 ROW 格式 binlog 里记录了 200 万条行变更(前后镜像约 200MB+)。从库 IO Thread 接收 200MB 数据需要时间,SQL Thread 一条一条重放更慢,耗时可能是几十秒甚至几分钟。
排查手段:看 SHOW BINLOG EVENTS 定位大事务时间戳,或者监控 SHOW SLAVE STATUS 的 Seconds_Behind_Master 突然跳增。
2.2 单线程重放瓶颈
MySQL 5.6 之前,从库只有一个 SQL Thread。主库 8 个线程并发写,从库 1 个线程串行放,不延迟才怪。
5.6 引入的 slave_parallel_workers 参数支持多个 SQL 线程,但 5.6 的并行粒度是按库分(DATABASE),如果你的业务都在一个库里,并行等于没开。
5.7 引入 LOGICAL_CLOCK 模式,基于主库的组提交信息判断哪些事务可以并行。8.0 进一步引入 writeset 并行复制,只要事务修改的行集(writeset)没有交集,就允许并行回放——即使这些事务不在同一个组提交批次里。这意味着 UPDATE t1 和 UPDATE t2 可以并行,即使它们提交时间相隔几秒。
2.3 从库硬件不足
从库的磁盘如果是 HDD,随机写性能比 SSD 差一个数量级;CPU 核数不够,解析和重放 binlog 事件也慢。很多公司把从库当「低配备用机」,结果延迟成了常态。
真实案例:某电商公司从库用 4C8G 云服务器 + 普通云盘,主库是 32C64G 物理机 + NVMe SSD。主库 TPS 8000,从库延迟稳定在 15-30 秒。升级到 16C32G + ESSD 后延迟降到 0.5s 以内。
2.4 锁竞争
从库上如果还有别的查询(比如报表查询、备份读取),这些查询会和 SQL Thread 争抢表锁或行锁,导致重放被阻塞。
典型场景:晚上 10 点跑日报聚合查询,大表全表扫描。SQL Thread 要更新同一张表,被 MDL(元数据锁)或行锁堵住,从库延迟瞬间飙升到几百秒。
2.5 网络延迟
IO Thread 接收 binlog 慢,relay log 积压,SQL Thread 无数据可放。这种情况在跨机房部署时尤其明显。北京机房到上海机房 RTT 约 30ms,每次 binlog 事件传输都多 30ms,累积起来就很可观。
三、延迟排查三板斧
3.1 SHOW SLAVE STATUS\G
这是第一站,重点关注三个字段:
SHOW SLAVE STATUS\G
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 192.168.1.100
Master_Log_File: mysql-bin.000023
Read_Master_Log_Pos: 85763400
Relay_Log_File: relay-bin.000089
Relay_Log_Pos: 12345
Relay_Master_Log_File: mysql-bin.000023
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 45 -- ← 延迟秒数,近似值
Exec_Master_Log_Pos: 82341000
Relay_Log_Space: 3422400 -- ← relay log 积压大小(字节)Seconds_Behind_Master 是近似值,它的计算方式:当前时间 − SQL Thread 执行到的 binlog 事件的时间戳。大事务场景下这个值可能不准——比如主库的大事务刚提交,从库还没开始放,延迟可能显示为 0。
关键判断:Relay_Log_Space 持续增长说明 IO Thread 接收快但 SQL Thread 消费慢,问题在重放端;Slave_IO_State 显示 Waiting for master to send event 但 Seconds_Behind_Master 大,说明网络传输慢或者主库 Dump 线程来不及生成事件。
3.2 看 SHOW PROCESSLIST
SQL Thread 是否在等待什么?
SHOW PROCESSLIST;重点关注:
Waiting for table flush:有 DDL 或FLUSH TABLES在排队,SQL Thread 被卡住。从库跑FLUSH TABLES;或ALTER TABLE时最常见。Waiting for lock:SQL Thread 和从库上的其他查询在争锁。System lock:可能是元数据锁。
3.3 用 pt-heartbeat 精确测量
Seconds_Behind_Master 是秒级精度且受时钟偏差影响。Percona Toolkit 的 pt-heartbeat 通过在主库定期写入时间戳、从库计算差值,能达到毫秒级精度。
# 在主库创建心跳表
pt-heartbeat --create-table -D percona -u root -p
# 主库写入心跳(1 秒一次)
pt-heartbeat --update -D percona --daemonize
# 从库查询延迟
pt-heartbeat --check -D percona -S /tmp/mysql.sock输出示例:0.003 表示 3ms 延迟,45.200 表示 45.2 秒。这个值比 Seconds_Behind_Master 可靠得多。
四、修复方案
4.1 并行复制(Multi-Threaded Slave)
MySQL 8.0 的并行复制已经成熟,优先开启:
# my.cnf 从库配置
slave_parallel_workers = 8
slave_parallel_type = LOGICAL_CLOCK关键点是 LOGICAL_CLOCK 模式——它基于主库的组提交(Group Commit)信息,同一组提交的事务在从库上可以安全地并行回放。主库的 binlog_group_commit_sync_delay 可以控制组提交的等待时间,适当调大可以让更多事务被分到同一组,提高并行度。
# 主库配置,提升并行复制的效果
binlog_group_commit_sync_delay = 1000 -- 微秒,等待 1ms 聚合更多事务
binlog_group_commit_sync_no_delay_count = 100注意:binlog_group_commit_sync_delay 调太大(>5000)会显著增加主库响应延迟,因为事务提交被强制等待。通常 1000 微秒(1ms)是安全值。
4.2 拆分大事务
大事务不仅产生延迟,还会导致 binlog 文件膨胀、主从切换变慢。业务上应该限制单次操作的数据量:
-- 分批删除,每批 1000 行,带 SLEEP 给从库喘息时间
DELIMITER $$
CREATE PROCEDURE batch_delete_old_orders()
BEGIN
DECLARE done INT DEFAULT 0;
REPEAT
DELETE FROM orders
WHERE create_time < '2024-01-01'
LIMIT 1000;
SET done = ROW_COUNT();
SELECT SLEEP(0.1); -- 给从库 SQL Thread 喘口气
UNTIL done = 0 END REPEAT;
END$$
DELIMITER ;业务代码层面,也建议对批量操作做分页:Java 里用 LIMIT 1000 OFFSET ? 循环,每次提交独立事务。
4.3 半同步复制(Semi-Sync Replication)
异步复制下,主库提交事务后不管从库是否收到,直接返回客户端。如果主库在从库收到 binlog 前宕机,切换后数据丢失——这就是异步复制的 RPO > 0。
半同步复制至少有一个从库确认收到 binlog 后,主库才返回客户端提交成功。思路:
client → 主库提交事务 → 写 binlog → 等待至少一个从库 ACK → 返回客户端# 主库安装插件 & 开启
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_master_timeout = 10000; -- 10 秒超时,超时后降级为异步
# 从库
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_slave_enabled = 1;
STOP SLAVE IO_THREAD; START SLAVE IO_THREAD;代价:每次事务提交多一次网络往返(主库 → 从库 → 主库 ACK),延迟增加约 1-5ms。rpl_semi_sync_master_timeout 是保命机制——如果从库挂了,等待超时后自动降级为异步,避免主库不可写。
4.4 架构层面解耦
如果延迟无法根除,应用层必须感知:
- ProxySQL:支持延迟感知路由,
mysql_replication_hostgroups可以配置当从库延迟超过阈值时自动将流量切回主库。
-- ProxySQL 配置示例
UPDATE mysql_replication_hostgroups
SET writer_hostgroup=0, reader_hostgroup=1
WHERE comment='production';
-- 延迟超过 10 秒的从库不分配读流量
INSERT INTO mysql_servers (hostgroup_id, hostname, port, weight, max_latency_ms)
VALUES (1, 'slave1', 3306, 1, 10000);- 缓存兜底:读请求先查 Redis,Redis miss 再从从库读,缓存只有几秒 TTL,能扛住延迟窗口。
4.5 relay log 损坏的兜底
relay log 损坏会导致从库复制中断。常见原因:磁盘满、MySQL 异常 crash、文件系统错误。
-- 暂停并从当前位置重建 relay log
STOP SLAVE;
RESET SLAVE;
START SLAVE;注意 RESET SLAVE 会清除 relay log 和 master.info 信息,但不会丢失数据——IO Thread 会从之前记录的 Master_Log_File + Read_Master_Log_Pos 重新拉取 binlog。前提是主库的 binlog 还没被 purge 掉。
五、总结
| 环节 | 常见问题 | 解决手段 |
|---|---|---|
| 大事务 | 单条 SQL 影响百万行 | 分批操作、设置 max_execution_time |
| 单线程重放 | 跟不上主库写入 | 开启并行复制 LOGICAL_CLOCK(8.0 建议 writeset) |
| 从库硬件 | 磁盘 IOPS 不足 | 升 SSD、增加从库数量做负载分担 |
| 锁竞争 | SQL Thread 被阻塞 | 从库上报表查询错峰、用 SELECT ... FROM SECONDARY 或只读实例 |
| 网络延迟 | 跨机房复制 | 半同步复制(rpl_semi_sync_master)、压缩传输 |
| binlog 格式 | ROW 格式体积爆炸 | 保证带宽充足、监控 Binlog_cache_disk_use |
| 异步复制 RPO>0 | 主库宕机丢数据 | 半同步复制 + 配合 sync_binlog=1 和 innodb_flush_log_at_trx_commit=1 |
主从延迟不可怕,可怕的是没有监控手段和应急方案。建议每套 MySQL 集群都配置 pt-heartbeat + 延迟告警(阈值 10 秒),同时业务层做好降级方案,别让从库延迟直接拖垮线上服务。
面试追问:给你一个 Seconds_Behind_Master=0 但从库数据明显不一致的场景,你怎么排查?——答案可以从 binlog 格式(STATEMENT 非确定性函数)、sync_binlog 不为 1 导致主库 crash 后 binlog 丢失、log_slave_updates 未开启导致级联复制断链这几个方向答。