MySQL 主从延迟根因诊断法

一、为什么主从延迟这么难定位

主从延迟是一个典型的"果",真正的"因"可能藏在任意一层:网络带宽、磁盘 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-ostpt-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 的 stateExecuting eventWaiting 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 层(大事务、无主键表)和参数层(单线程复制)就能找到答案,真正需要动网络和内核参数的场景反而是少数。

相关推荐
东北甜妹2 小时前
MySQL数据库高级特性
mysql
Trouvaille ~2 小时前
【MySQL篇】从零开始:安装与基础概念
linux·数据库·mysql·ubuntu·c·教程·基础入门
Yana.nice3 小时前
MySQL 事务的四大特性(ACID)
数据库·mysql·oracle
Yana.nice4 小时前
MySQL 三大日志(redo log、undo log、binlog)的区别和作用
数据库·mysql
XDHCOM4 小时前
MySQL CASE WHEN语句应用实例:如何实现条件查询与数据转换?
数据库·mysql
木下~learning4 小时前
MySQL 从入门到精通:安装、终端操作、远程连接与 C 语言 API 全教程
c语言·数据库·mysql
阿维的博客日记4 小时前
MySQL中type字段解析
数据库·mysql
Trouvaille ~4 小时前
【MySQL篇】表的操作:数据的容器
linux·数据库·mysql·oracle·xshell·ddl·表的操作
黑牛儿4 小时前
从0开始实现Mysql主从配置实战
服务器·数据库·后端·mysql