一、为什么主从延迟这么难定位
主从延迟是一个典型的"果",真正的"因"可能藏在任意一层:网络带宽、磁盘 IO、业务大事务、参数配置,甚至是从库上跑了一条慢查询把锁卡住了。更麻烦的是,这几层因素经常同时存在、互相掩盖,单独看任何一个指标都容易误判。
本文的目标不是给你一张"改这几个参数就能解决"的速查表,而是帮你建立一套从现象到根因的完整排查思路------先搞清楚延迟发生在哪个环节,再针对性地处理。
二、先把复制流程看清楚
诊断之前,必须对 MySQL 复制的完整链路有清晰的认知,否则很容易在错误的地方找原因。
以 MySQL 5.7+ 基于行的复制(Row-Based Replication)为例,一个事务从主库提交到从库可见,要经过四个阶段:
主库事务提交
│
▼
Binlog 落盘(主库 Dump Thread 监听)
│
▼
网络传输 → 从库 IO Thread 接收 → 写入 Relay Log
│
▼
SQL Thread(或 MTS Worker)读取 Relay Log → 本地回放
│
▼
从库数据可见
这是一个经典的生产者-消费者模型。延迟本质上只有两种情况:
- 管道堵了:网络或 IO 跟不上,Relay Log 积压
- 消费慢了:SQL Thread 回放速度跟不上主库写入速度
所有的根因最终都能归到这两类里。
三、第一步:别被 Seconds_Behind_Master 骗了
很多人排查延迟的第一反应是跑一条 SHOW REPLICA STATUS\G,盯着 Seconds_Behind_Master(SBM)看。这个指标有用,但有两个严重的盲区,不了解的话很容易误判。
盲区一:网络堵塞时 SBM 会显示为 0
SBM 的计算逻辑是:当前系统时间 - 当前正在回放的事务在主库的提交时间戳。如果网络拥塞导致 IO Thread 根本没拉到新的 Binlog,SQL Thread 把手头已有的 Relay Log 都回放完了,SBM 就会归零------但主库实际上已经领先从库很多了。
盲区二:大事务期间 SBM 长时间不动
一个执行了 40 分钟的 DDL,在从库回放期间 SBM 会一直停在某个值,等 DDL 执行完的瞬间才会大幅跳降。这段时间你看到的 SBM 根本反映不了真实的积压程度。
更可靠的做法:对比 Binlog 坐标
sql
SHOW REPLICA STATUS\G
重点看这几个字段的关系:
|-------------------------------------------------|----------------------|
| 字段 | 含义 |
| Master_Log_File / Read_Master_Log_Pos | IO Thread 已拉取到的主库位置 |
| Relay_Master_Log_File / Exec_Master_Log_Pos | SQL Thread 已回放到的主库位置 |
- 如果
Read_Master_Log_Pos明显落后于主库当前位置 → 网络或 IO Thread 有问题 - 如果
Read和主库接近,但Exec落后Read很多 → SQL Thread 回放跟不上
这两种情况的处理方向完全不同,先区分清楚再往下查。
对于核心业务,建议引入 pt-heartbeat(Percona Toolkit),它在主库定期写入微秒级时间戳心跳,在从库端精确计算差值,是目前最准确的延迟监控方案。
四、网络层:跨机房场景的第一道坎
如果上一步判断是 IO Thread 跟不上,优先排查网络。
典型现象 :Read_Master_Log_Pos 长期停滞,或者主从 Binlog 文件序号相差很大,说明从库根本没在正常接收数据。
排查步骤:
bash
# 检查网络往返延迟,跨可用区建议 < 2ms
ping <主库IP>
# 测试实际 TCP 吞吐量,排除云厂商带宽限流
iperf3 -c <主库IP> -t 30
# 检查 TCP 重传率,有重传就说明有丢包
netstat -s | grep retransmitted
针对性处理:
如果是弱网或跨国环境,可以在从库开启传输压缩,用 CPU 换带宽:
sql
-- MySQL 8.0.18+
CHANGE REPLICATION SOURCE TO SOURCE_COMPRESSION_ALGORITHMS = 'zstd';
-- 旧版本
SET GLOBAL slave_compressed_protocol = 1;
如果是 TCP 窗口问题,在内核层面调整接收缓冲区:
bash
# /etc/sysctl.conf
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_window_scaling = 1
五、IO 层:从库的磁盘压力比主库更大
如果网络正常,Relay Log 已经充足,但从库 Load 高、iowait 居高不下,问题在 IO 层。
从库的 IO 负担实际上比主库更重:它既要写 Relay Log,又要写数据页、Redo Log,如果开启了级联复制还要写 Binlog。
bash
# 观察磁盘使用率和 IO 响应时间
iostat -x 1
# 重点关注:
# %util → 接近 100% 说明磁盘已饱和
# await → 单次 IO 响应时间,HDD > 10ms 就要注意,NVMe 正常应在 0.1ms 级别
在纯读从库上可以适当放宽 IO 安全策略(注意:这会增加宕机时丢失复制进度的风险,需要结合业务容忍度评估):
sql
-- 关闭 Binlog 实时刷盘,交由 OS Page Cache 管理
SET GLOBAL sync_binlog = 0;
-- Redo Log 延迟刷盘,每秒刷一次而非每次提交都刷
SET GLOBAL innodb_flush_log_at_trx_commit = 2;
-- 如果使用 NVMe SSD,调高 InnoDB 的 IO 容量上限
-- 默认值是按 HDD 设计的,NVMe 上严重低估了实际能力
SET GLOBAL innodb_io_capacity = 5000;
SET GLOBAL innodb_io_capacity_max = 10000;
六、SQL 层:最常见、也最容易被忽视的根因
网络和 IO 都正常,但延迟还是居高不下?问题大概率在 SQL 回放层。这也是实际生产中最高频的根因所在。
6.1 大事务与 DDL
一条 UPDATE orders SET status = 1 WHERE created_at < '2023-01-01' 如果涉及 500 万行,在 RBR 模式下会产生 500 万条 Row Event,传到从库后 SQL Thread 需要逐条回放。主库可能 10 秒执行完,从库要跟几十分钟。
DDL 更危险。ALTER TABLE 在从库回放时会持有元数据锁(MDL),不仅自身执行慢,还会把后续所有针对该表的复制事件全部堵在队列里。
处理方式:
- 大批量 DML 拆成小批次,每次
LIMIT 1000~2000,批次之间加短暂 sleep - 所有表结构变更使用
gh-ost或pt-online-schema-change,将阻塞时间压缩到毫秒级
6.2 无主键表:复制性能的隐形杀手
在 RBR 模式下,如果表没有主键或唯一索引,从库回放一条修改 N 行的语句时,无法精准定位记录,会退化为 N 次全表扫描。主库一次索引更新,从库变成全表扫描,CPU 直接打满,复制几乎停滞。
用这个脚本定期巡检,找出没有主键的表:
sql
SELECT
t.table_schema,
t.table_name,
t.table_rows
FROM information_schema.tables t
LEFT JOIN (
SELECT table_schema, table_name
FROM information_schema.statistics
GROUP BY table_schema, table_name, index_name
HAVING SUM(CASE WHEN non_unique = 0 THEN 1 ELSE 0 END) = COUNT(*)
) pk ON t.table_schema = pk.table_schema AND t.table_name = pk.table_name
WHERE pk.table_name IS NULL
AND t.table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys')
AND t.table_type = 'BASE TABLE';
6.3 从库读请求挤占回放资源
读写分离架构下,从库承载了大量复杂的 JOIN 查询或聚合报表,把 CPU 和内存带宽消耗殆尽,SQL Thread 没有资源做回放。
这种情况的特征是:从库 CPU 高,但 iowait 不高,SHOW PROCESSLIST 里能看到大量长时间运行的 SELECT。
短期处理是 kill 掉异常的长查询;长期方案是把重度分析类查询剥离到 ClickHouse 等 OLAP 引擎,从库只承担轻量读请求。
七、并行复制:从根本上提升回放吞吐量
前面几层都排查完、也都处理了,但高并发下延迟还是反复出现?这时候需要从架构层面解决问题------开启多线程并行复制(MTS)。
MySQL 5.6 之前,SQL Thread 是单线程的,主库再高的并发写入,到从库都变成串行回放,天花板很低。5.7 引入了基于逻辑时钟(LOGICAL_CLOCK)的并行策略:在主库能同时提交的事务,说明它们之间没有行锁冲突,在从库也可以并行回放。
html
# my.cnf
# 使用逻辑时钟策略,替代 5.6 时代鸡肋的 DATABASE 级别并行
slave_parallel_type = LOGICAL_CLOCK
# Worker 线程数,建议设为 CPU 核心数的 1~2 倍
slave_parallel_workers = 16
# 保证从库回放顺序与主库提交顺序一致
# 不开这个可能导致从库短暂出现"未来数据",引发业务逻辑错乱
slave_preserve_commit_order = 1
一个容易被忽略的配套优化:如果主库并发不够高,每个时间窗口只有一两个事务提交,MTS 的并行度就会很低,Worker 大部分时间在空等。可以在主库端配置组提交参数,人为让多个事务凑成一批再提交,显著提升传导到从库的并行度:
sql
-- 等待最多 5ms,或凑齐 10 个事务,再一起刷盘生成 Binlog 组
-- 代价是主库写入延迟略微增加,收益是从库并行度大幅提升
SET GLOBAL binlog_group_commit_sync_delay = 5000;
SET GLOBAL binlog_group_commit_sync_no_delay_count = 10;
这两个参数需要根据实际业务的写入 TPS 调整,不是越大越好。
八、用 Performance Schema 透视 Worker 状态
开启 MTS 之后,如何判断并行效果?某个 Worker 是不是被卡死了?SHOW REPLICA STATUS 给不了这个粒度,需要查 performance_schema:
sql
SELECT
t.THREAD_ID,
t.PROCESSLIST_STATE AS state,
t.PROCESSLIST_TIME AS time_sec,
t.PROCESSLIST_INFO AS current_sql,
w.WORKER_ID,
w.LAST_APPLIED_TRANSACTION,
w.LAST_ERROR_MESSAGE
FROM performance_schema.threads t
LEFT JOIN performance_schema.replication_applier_status_by_worker w
ON t.THREAD_ID = w.THREAD_ID
WHERE t.NAME LIKE '%worker%'
OR t.NAME LIKE '%coordinator%'
ORDER BY t.THREAD_ID;
结果判读:
- 正常 :各 Worker 的
state在Executing event和Waiting for an event from Coordinator之间快速切换,time_sec很小 - 大事务阻塞 :大量 Worker 在等待,唯独某一个 Worker 的
time_sec持续飙升,current_sql显示一条长时间运行的 UPDATE------并行复制已退化为串行,需要整改业务大事务 - MDL 锁等待 :
state显示Waiting for table metadata lock,说明从库本地有长查询持有了表锁,把复制堵住了,直接 kill 掉对应的查询线程即可疏通
九、总结:一张诊断决策路径图
sql
发现主从延迟
│
├─ Read_Master_Log_Pos 落后主库?
│ ├─ 是 → 检查网络(ping / iperf3 / 重传率)→ 优化带宽或开启压缩
│ └─ 否 ↓
│
├─ Exec 落后 Read 很多?
│ ├─ 是 → SQL Thread 回放慢,进入下一层
│ └─ 否 → 确认 SBM 是否为误报,引入 pt-heartbeat 验证
│
├─ 从库 iowait 高?
│ ├─ 是 → 检查磁盘(iostat),考虑放宽 IO 参数
│ └─ 否 ↓
│
├─ 存在大事务 / 无主键表 / DDL?
│ ├─ 是 → 拆分事务、补主键、用 gh-ost 做 DDL
│ └─ 否 ↓
│
├─ 从库有大量长查询占用 CPU?
│ ├─ 是 → kill 异常查询,长期考虑剥离 OLAP 负载
│ └─ 否 ↓
│
└─ MTS 未开启或并行度不足?
└─ 是 → 配置 LOGICAL_CLOCK + 调整 Worker 数 + 主库开启组提交
主从延迟的根因诊断没有捷径,但有方法论。按照网络 → IO → SQL → 参数这条链路逐层排查,每一层都有明确的观测指标和处理手段。大多数情况下,问题在 SQL 层(大事务、无主键表)和参数层(单线程复制)就能找到答案,真正需要动网络和内核参数的场景反而是少数。