📌 解决思路:从网络、IO、SQL 到参数,系统化定位高并发下的同步瓶颈
📌 适用版本 :MySQL 5.7 / 8.0
📌 适用场景:高并发写入、主从延迟告警、从库追不上主库
目录
- 一、先量化延迟:别被假数据骗了
- 二、三层定位法:快速锁定瓶颈层次
- 三、网络层排查
- 四、IO 线程层排查
- 五、SQL 线程层排查(最常见根因)
- 六、深层陷阱:大事务 / 锁竞争 / DDL / 磁盘 IO
- 七、关键参数速查表
- 八、监控告警体系搭建
- 九、建立持续治理 SOP
一、先量化延迟:别被假数据骗了
排查延迟的第一步,是拿到真实可信的延迟数值。
1.1 Seconds_Behind_Master 的局限
sql
SHOW SLAVE STATUS\G
-- 关注字段:Seconds_Behind_Master
这个值有一个致命缺陷:当 SQL 线程卡住时,它会停止更新,导致显示值失真。在大事务或 DDL 阻塞场景下,它可能长时间静止不动,但实际延迟仍在累积。
1.2 推荐:pt-heartbeat(精准测量)
sql
# 主库:持续写入心跳
pt-heartbeat --user=root --password=xxx --host=master \
--database=test --create-table --daemonize --update
# 从库:实时读取延迟
pt-heartbeat --user=root --password=xxx --host=slave \
--database=test --monitor --master-server-id=1
pt-heartbeat 的优势:
- 跨时区安全,不依赖系统时钟
- 精确到毫秒级
- SQL 线程卡住时依然能反映真实延迟
二、三层定位法:快速锁定瓶颈层次
拿到延迟数值后,执行以下语句,对照两个关键位点判断延迟来自哪一层:
sql
SHOW SLAVE STATUS\G
| 对比位点 | 差距大说明 | 延迟层次 |
|---|---|---|
Master_Log_File vs Relay_Master_Log_File |
binlog 没传过来 | 网络层 / IO 线程 |
Relay_Log_File vs Exec_Master_Log_File |
relay log 没回放完 | SQL 线程 |
主库写入 → [网络传输] → IO线程接收 → relay log → [SQL线程回放] → 从库执行
↑ 第一段延迟 ↑ 第二段延迟
三、网络层排查
3.1 诊断命令
sql
# 测试带宽(双向)
iperf3 -c master_ip -t 30
# 检查 RTT 和丢包
ping master_ip -c 100
# 查看网卡实时流量
sar -n DEV 1 10
3.2 常见问题与对策
带宽不足:binlog 产生速度 > 网络传输速度
sql
# my.cnf 从库配置:开启压缩传输(CPU 换带宽)
[mysqld]
slave_compressed_protocol = ON
网络抖动导致重连慢:
sql
# 缩短重连超时(默认 60s 太长)
slave_net_timeout = 30
跨机房场景:优先申请专线或使用 VPN 隔离,避免公网延迟抖动。
四、IO 线程层排查
IO 线程慢的本质是:主库 binlog 产生速度 > IO 线程接收写入速度。
4.1 检查 binlog 产生速率
sql
# 主库:观察 binlog 增长速度
mysqlbinlog --start-datetime="2024-01-01 10:00:00" \
--stop-datetime="2024-01-01 10:01:00" \
/var/lib/mysql/mysql-bin.000001 | wc -c
4.2 binlog_format 的影响
| format | event 大小 | 对延迟影响 |
|---|---|---|
| STATEMENT | 小(只记 SQL) | 低带宽,但有安全风险 |
| ROW | 大(记录行变更) | 带宽消耗高,适合高一致性要求 |
| MIXED | 折中 | 推荐默认 |
sql
binlog_format = ROW # 高一致性场景
binlog_row_image = MINIMAL # 减少 ROW 模式下的 event 大小(MySQL 5.6+)
4.3 主库刷盘参数
sql
# 主库:高性能写入(权衡持久性)
sync_binlog = 0 # 0=OS 决定刷盘时机,性能最高
innodb_flush_log_at_trx_commit = 2 # 每秒刷盘,非每次事务
# 主库:高安全(金融场景)
sync_binlog = 1
innodb_flush_log_at_trx_commit = 1
五、SQL 线程层排查(最常见根因)
这是高并发场景最普遍的瓶颈。主库多线程并发写入,从库默认单线程串行回放,必然追不上。
5.1 确认 SQL 线程是瓶颈
sql
-- 确认 SQL 线程正在运行但回放慢
SHOW SLAVE STATUS\G
-- Slave_SQL_Running: Yes
-- Exec_Master_Log_Pos 长期落后 Relay_Log_Pos
5.2 开启并行复制(核心解法)
MySQL 5.7+ 基于逻辑时钟的并行复制(推荐):
sql
[mysqld]
# 从库配置
slave_parallel_type = LOGICAL_CLOCK # 基于 binlog group commit 信息
slave_parallel_workers = 8 # 从 CPU 核数 50% 开始调,逐步压测
slave_preserve_commit_order = ON # 保证从库事务提交顺序与主库一致
# 主库需配合(提高 group commit 批量)
binlog_group_commit_sync_delay = 100 # 微秒,等待更多事务进组
binlog_group_commit_sync_no_delay_count = 10
⚠️ 注意 :
slave_preserve_commit_order = ON必须开启,否则从库事务顺序与主库不一致,可能导致读到脏数据。
5.3 验证并行复制效果
sql
-- 查看并行复制工作线程状态
SELECT * FROM performance_schema.replication_applier_status_by_worker\G
-- 查看 worker 线程分配情况
SHOW STATUS LIKE 'Slave_worker%';
5.4 MySQL 8.0 的改进
MySQL 8.0 引入 Writeset 并行复制,不依赖 group commit,并行度更高:
binlog_transaction_dependency_tracking = WRITESET
slave_parallel_type = LOGICAL_CLOCK
slave_parallel_workers = 16
六、深层陷阱:大事务 / 锁竞争 / DDL / 磁盘 IO
6.1 大事务(最常被忽略的杀手)
大事务在主库被多线程并发所掩盖,到从库单线程串行回放时会产生秒级甚至分钟级卡顿。
定位大事务:
sql
# 找到 binlog 中的超大 event
mysqlbinlog --verbose /var/lib/mysql/mysql-bin.000001 \
| awk '/^# at/{pos=$3} /^### /{count++} /^COMMIT/{if(count>10000) print pos, count; count=0}'
# 或用 mysqlbinlog 直接统计
mysqlbinlog --base64-output=DECODE-ROWS -v mysql-bin.000001 \
| grep -E "^(# at|^### )" | awk '...'
业务层改造:
sql
-- ❌ 危险:一次删除 500 万行
DELETE FROM orders WHERE created_at < '2023-01-01';
-- ✅ 安全:分批删除,每批 1000 行
DELETE FROM orders WHERE created_at < '2023-01-01' LIMIT 1000;
-- 循环执行直到影响行数为 0
6.2 锁竞争(从库上的读写冲突)
从库并非只读------备份、统计查询会产生锁,与 SQL 线程的写操作产生冲突。
sql
-- 查看从库当前锁等待
SELECT
r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
r.trx_query waiting_query,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread
FROM information_schema.INNODB_TRX b
JOIN information_schema.INNODB_TRX r ON r.trx_wait_started IS NOT NULL;
对策:
- 从库大查询使用
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED - 备份使用
--single-transaction避免持锁 - 将分析查询迁移到专用的只读从库
6.3 DDL 阻塞
原生 DDL 在从库执行时会独占 SQL 线程,期间所有回放暂停。
sql
# 推荐:使用 gh-ost 进行在线表变更(不阻塞从库)
gh-ost \
--host=master \
--user=root --password=xxx \
--database=mydb \
--table=orders \
--alter="ADD INDEX idx_user_id(user_id)" \
--execute
或使用 Percona 的 pt-online-schema-change:
sql
pt-online-schema-change \
--alter="ADD INDEX idx_user_id(user_id)" \
D=mydb,t=orders \
--execute
6.4 磁盘 IO 瓶颈
sql
# 实时观察磁盘 IO
iostat -xm 1 10
# 找到 IO 最多的进程
iotop -o
# 查看 MySQL 数据目录所在磁盘
df -h /var/lib/mysql
关键参数:
sql
# 从库可以适当降低持久性换性能
innodb_flush_log_at_trx_commit = 2 # 从库安全降级
innodb_flush_method = O_DIRECT
innodb_io_capacity = 4000 # SSD 场景可调高至 8000-20000
innodb_io_capacity_max = 8000
七、关键参数速查表
| 参数 | 推荐值 | 作用 | 适用位置 |
|---|---|---|---|
slave_parallel_workers |
4 ~ 16 | 并行回放线程数 | 从库 |
slave_parallel_type |
LOGICAL_CLOCK |
并行复制策略 | 从库 |
slave_preserve_commit_order |
ON |
保证事务顺序 | 从库 |
sync_binlog |
1(安全)/ 0(性能) | 主库 binlog 刷盘 | 主库 |
innodb_flush_log_at_trx_commit |
1(主库)/ 2(从库) | redo log 刷盘 | 主 / 从 |
slave_net_timeout |
30 |
网络超时重连 | 从库 |
relay_log_recovery |
ON |
从库重启自动修复 | 从库 |
slave_compressed_protocol |
ON(跨机房) |
压缩传输节省带宽 | 从库 |
binlog_row_image |
MINIMAL |
减小 ROW 格式 event | 主库 |
innodb_io_capacity |
4000 ~ 20000(SSD) | IO 调度上限 | 从库 |
八、监控告警体系搭建
8.1 Prometheus + mysqld_exporter
sql
# prometheus.yml 抓取配置
scrape_configs:
- job_name: 'mysql_slave'
static_configs:
- targets: ['slave_host:9104']
核心监控指标:
sql
# 从库延迟
mysql_slave_status_seconds_behind_master
# IO 线程状态(1=Running,0=异常)
mysql_slave_status_slave_io_running
# SQL 线程状态
mysql_slave_status_slave_sql_running
# 并行复制 worker 等待
mysql_slave_status_slave_worker_count
8.2 Grafana 告警规则建议
sql
# 告警阈值参考
- alert: MySQLReplicationLagWarning
expr: mysql_slave_status_seconds_behind_master > 10
for: 2m
annotations:
summary: "从库延迟超过 10s,当前值 {{ $value }}s"
- alert: MySQLReplicationLagCritical
expr: mysql_slave_status_seconds_behind_master > 30
for: 1m
annotations:
summary: "从库延迟超过 30s(严重),当前值 {{ $value }}s"
- alert: MySQLReplicationThreadDown
expr: mysql_slave_status_slave_sql_running == 0 or mysql_slave_status_slave_io_running == 0
for: 30s
annotations:
summary: "主从复制线程已停止"
8.3 pt-heartbeat 集成
sql
# 主库(systemd 守护进程)
pt-heartbeat --update --host=master --database=test \
--create-table --daemonize \
--pid=/var/run/pt-heartbeat.pid
# 从库监控(输出毫秒级精度)
pt-heartbeat --monitor --host=slave --database=test \
--master-server-id=1 --frames=1m,5m,15m
九、建立持续治理 SOP
解决延迟不是一次性的,需要建立持续治理机制:
变更管控
- DDL 变更必须走审批流,使用
gh-ost或pt-osc - 批量写入操作必须分批,单批不超过 1000 行
- 高峰期禁止大批量删除/更新
容量规划
- 主库写入 QPS 增长超过 20%,及时评估并行复制 worker 数量
- 监控 binlog 产生速率,提前规划磁盘和带宽
定期演练
- 每季度模拟延迟场景,验证告警链路是否畅通
- 记录历史延迟事件的根因和恢复时间(MTTR)
总结
主从延迟排查可以遵循以下优先级:
sql
1. 量化延迟(pt-heartbeat 优于 Seconds_Behind_Master)
2. 用 SHOW SLAVE STATUS 定层(网络 / IO 线程 / SQL 线程)
3. SQL 线程慢 → 优先开并行复制(80% 场景的解法)
4. 排查大事务 → 业务改造分批写
5. 检查锁竞争 → 减少从库查询干扰
6. DDL 变更 → 使用 gh-ost / pt-osc
7. 磁盘 IO → 升级 SSD + 调整 innodb_io_capacity
主从延迟没有银弹,需要结合业务写入模式、硬件配置和 MySQL 版本综合调优。建议从并行复制入手,再逐步收敛到大事务治理和监控体系完善。
如果这篇文章对你有帮助,欢迎点赞收藏 ⭐
有问题欢迎在评论区讨论,一起交流 MySQL 调优经验 🚀