前言
主从延迟是 MySQL 高可用架构中最高频的问题之一。很多人对延迟的理解停留在"大事务导致延迟"这个层面,遇到问题时无从下手。本文从复制链路的两个阶段出发——IO 线程和 SQL 线程——分别梳理常见延迟原因,并结合真实案例深入分析原理。
一、主从复制链路简述
理解延迟,先要清楚复制的完整链路:
主库写入事务
↓
主库 Dump 线程:读取 binlog 发送给从库
↓ (网络传输)
从库 IO 线程:接收 binlog,写入 relay log
↓
从库 SQL 线程(或并行 Worker):读取 relay log,回放事务
↓
从库数据与主库一致
延迟分两类:
- IO 延迟:从库 IO 线程落后于主库,relay log 还没接收完整
- SQL 延迟:relay log 已收到,但 SQL 线程回放跟不上
用 SHOW SLAVE STATUS\G 区分:
|
|
二、IO 线程延迟的常见原因
原因 1:网络带宽不足
原理:
主库的 Dump 线程将 binlog 通过 TCP 推送给从库 IO 线程。如果主库写入量大,binlog 生成速率超过网络传输速率,从库就会持续落后。
排查方法:
|
|
案例场景:
某公司主从之间走公网专线,带宽 100Mbps。某天主库执行了一批图片数据迁移,binlog 瞬间增长到 150Mbps,从库 IO 线程开始积压,Seconds_Behind_Master 快速攀升到 600 秒。
解决方案:
|
|
压缩对 CPU 有轻微消耗,但可将 binlog 传输量降低 30%~70%,对文本类数据效果显著。
原因 2:主库 Dump 线程压力大
主库的每个从库对应一个 Dump 线程。从库数量多时,主库磁盘读压力增大,每个从库获取的 binlog 速度都会下降。
排查方法:
|
|
解决方案: 使用级联复制(主 → 中继从 → 多个从),减少主库 Dump 线程数量。
三、SQL 线程延迟的常见原因
SQL 线程延迟是最常见的延迟类型,原因也更复杂。
原因 1:大事务 + ROW 格式 + 无索引 → 全表扫描地狱
这是最典型、最容易被忽视的延迟场景,值得深入分析。
案例:主库 1000 万条数据无主键无索引,ROW 模式删除 100 万行
场景还原:
|
|
ROW 模式下从库回放的原理:
ROW 格式 binlog 记录的是每一行的变更。主库删除 100 万行,binlog 里就有 100 万条 delete event,每条记录了该行的完整前镜像(所有字段的值)。
从库 SQL 线程回放时,针对每一条 delete event,需要找到对应的行并删除。InnoDB 通过以下优先顺序来定位行:
- 主键
- 唯一索引
- 如果都没有 → 全表扫描
没有主键、没有索引,从库每删除 1 行,就要对 1000 万行进行一次全表扫描!
延迟估算:
100 万次全表扫描 × 每次扫描耗时
假设 1000 万行扫描一次需要 0.1 秒(内存充足时):
100 万 × 0.1 秒 = 10 万秒 ← 约 27 小时的延迟
即使有缓存加速,实际延迟也会是数小时级别
验证方法:
|
|
临时缓解(已经在延迟中):
|
|
根本解决:
|
|
防范参数:
|
|
原因 2:从库未提交事务 + 主库 DDL → MDL 锁等待
场景还原:
|
|
从库回放 DDL 的原理:
从库 SQL 线程回放 DDL 时,需要获取该表的 MDL 写锁(Metadata Lock)。
而从库上那个未提交的 SELECT ... FOR UPDATE 事务,在整个事务期间持有 MDL 读锁。
锁冲突:
SQL 线程需要:MDL 写锁(互斥锁)
↑ 等待
未提交事务持有:MDL 读锁
SQL 线程被阻塞,relay log 积压,Seconds_Behind_Master 持续增长,直到从库上的未提交事务提交或回滚,MDL 读锁释放,SQL 线程才能继续回放。
排查方法:
|
|
解决方案:
|
|
根本预防:
- 从库应设置
super_read_only = ON,禁止业务连接开启写事务 - 主库执行 DDL 前,先检查是否有长事务:
SELECT * FROM information_schema.INNODB_TRX - 使用
pt-online-schema-change代替直接 DDL
原因 3:单线程回放 vs 主库并发写入
原理:
主库是多线程并发写入,N 个线程同时提交事务。而默认情况下,从库 SQL 线程是单线程串行回放。
主库:线程1、线程2、线程3 并发提交 → binlog 串行记录
从库:单个 SQL 线程串行回放
主库并发优势在从库完全消失,写入压力越大,延迟越明显。
解决方案:并行复制(Parallel Replication)
MySQL 5.7 引入基于 LOGICAL_CLOCK 的并行复制。核心思想:在主库上同一组提交(Group Commit)内的事务,没有锁冲突,可以在从库并行回放。
如何确定合适的 Worker 数量?
通过分析 binlog 中的 last_committed 字段来判断主库组提交的批次大小:
|
|
last_committed 相同的事务属于同一个 Group Commit,可以并行回放。
统计每组平均事务数:
|
|
根据分析结果设置 Worker 数(通常设置为平均组大小的 1~2 倍):
|
|
主库端:优化 Group Commit 批次大小
|
|
验证 Group Commit 效果:
|
|
原因 4:从库慢查询占用资源
从库 SQL 线程和业务查询共享 CPU、内存、IO 资源。如果从库上有大量慢查询(报表、全表扫描等),会导致 SQL 线程资源不足。
排查:
|
|
解决方案:
|
|
四、Seconds_Behind_Master 的局限性
Seconds_Behind_Master 不是真正的延迟时间,它的计算方式是:
当前时间 - relay log 中正在执行事务的 binlog 时间戳
它会失真的场景:
- 网络中断后恢复:IO 线程重连后快速拉取大量 relay log,SQL 线程回放时事务时间戳是历史值,
Seconds_Behind_Master显示很大(如 3600),但 relay log 其实已经追上了 - 主从时钟不同步:服务器时间差会导致该值持续偏大或偏小
- IO 线程停止期间:值为 NULL,重启后可能瞬间显示为 0
更准确的方式(GTID 模式):
|
|
或者使用 pt-heartbeat 工具进行精准心跳检测:
|
|
五、延迟排查决策流程
发现延迟(Seconds_Behind_Master > 阈值)
↓
区分 IO 延迟 vs SQL 延迟
├── Read_Master_Log_Pos 落后主库很多 → IO 延迟
│ ↓
│ 检查网络带宽 → iperf3 测试
│ 开启压缩传输 / 升级带宽 / 级联复制
│
└── Read 接近主库,但 Exec 落后 → SQL 延迟
↓
SHOW PROCESSLIST 查看 SQL 线程状态
├── 等待 MDL 锁 → 找到阻塞事务,KILL 之
├── 正在执行单条 SQL 很慢 → 检查是否无索引全表扫描
│ → 临时开启 HASH_SCAN
│ → 根本上为表添加主键/索引
├── SQL 线程空闲但延迟大 → 检查并行复制配置
│ → 分析 binlog last_committed 分布
│ → 调整 parallel_workers
└── 从库 CPU/IO 跑满 → 隔离业务查询到专用从库
六、关键参数速查
| 参数 | 说明 | 推荐值 |
|---|---|---|
slave_parallel_type |
并行复制模式 | LOGICAL_CLOCK |
slave_parallel_workers |
并行 Worker 数 | 根据 Group Commit 分析,通常 4~16 |
slave_preserve_commit_order |
保证提交顺序 | ON |
slave_compressed_protocol |
启用压缩传输 | ON(跨机房/公网) |
slave_rows_search_algorithms |
行查找算法 | INDEX_SCAN,HASH_SCAN |
binlog_group_commit_sync_delay |
主库 Group Commit 等待(微秒) | 100~1000 |
binlog_group_commit_sync_no_delay_count |
达到 N 个事务立即提交 | 10~50 |
sql_require_primary_key |
强制表必须有主键 | ON(MySQL 8.0.13+) |
lock_wait_timeout |
MDL 锁等待超时(秒) | 30 |
super_read_only |
从库禁止所有写入 | ON |
七、总结
主从延迟不是一个单一问题,而是一个症状。同样是"延迟增大",背后的原因可能完全不同:
| 延迟类型 | 典型场景 | 核心原因 |
|---|---|---|
| IO 延迟 | 大批量写入、跨机房复制 | 带宽不足,binlog 传输慢 |
| SQL 延迟 | 无索引大表 ROW 模式删除 | 从库回放变成逐行全表扫描 |
| SQL 延迟 | 从库有未提交事务时主库执行 DDL | MDL 读写锁互斥,SQL 线程卡死 |
| SQL 延迟 | 主库高并发写入 | 从库单线程回放追不上主库并发 |
| SQL 延迟 | 从库跑大量报表查询 | CPU/IO 资源被业务查询抢占 |
核心原则:先定位是 IO 延迟还是 SQL 延迟,再根据具体原因针对性处理。不要上来就无脑调大 slave_parallel_workers,首先搞清楚瓶颈在哪里。