适用场景
本文适用于 MySQL 8.0 或已启用 GTID 的 MySQL 5.7 主从/主备架构:监控显示 Seconds_Behind_Source 持续增长,报表、读库或故障切换的恢复点开始不可接受。目标是先确认延迟真实原因,再在不破坏复制一致性的前提下恢复追平。
现象描述
一次订单促销后,主库写入从平时每秒数十条升到数千条。业务读请求仍落在从库,监控显示复制延迟由 0 上升到十几分钟;应用线程持续运行,并未报错。此时直接扩大从库规格或反复重启 MySQL 往往无效,甚至会丢失最关键的现场证据。
常见原因
复制延迟通常不是单一指标问题,常见组合包括:
- 主库出现大事务,单个事务提交前不能被分拆并行应用;
- 从库 SQL 线程受磁盘 I/O、CPU 或锁等待限制;
- 从库并行复制工作线程太少,或提交顺序配置不适合当前负载;
- 报表查询占满从库缓冲池、I/O 或持有长事务,阻塞回放;
- 网络抖动导致接收日志的 I/O 线程落后,或复制线程已停止。
第一步:先确认复制链路是否健康
在从库执行以下命令。MySQL 8.0 使用 REPLICA 术语;旧版本可将其替换为 SLAVE。
SHOW REPLICA STATUS\G
重点检查以下字段:
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 720
Last_IO_Error:
Last_SQL_Error:
Retrieved_Gtid_Set: ...
Executed_Gtid_Set: ...
Replica_IO_Running 为 No 说明问题在网络、认证或源端日志;Replica_SQL_Running 为 No 时先处理 Last_SQL_Error,不要把错误复制当作性能延迟。两个线程都为 Yes 且 Retrieved_Gtid_Set 明显领先于 Executed_Gtid_Set,才说明 relay log 已收到、回放端处理不过来。
为避免 Seconds_Behind_Source 在无新事务时掩盖历史问题,可用 GTID 集合和业务心跳表交叉验证。一个简单的心跳表可以记录源端当前时间:
CREATE TABLE ops.replication_heartbeat (
id TINYINT PRIMARY KEY,
updated_at TIMESTAMP(6) NOT NULL
) ENGINE=InnoDB;
INSERT INTO ops.replication_heartbeat (id, updated_at)
VALUES (1, NOW(6))
ON DUPLICATE KEY UPDATE updated_at = VALUES(updated_at);
SELECT TIMESTAMPDIFF(SECOND, updated_at, NOW(6)) AS lag_seconds
FROM ops.replication_heartbeat
WHERE id = 1;
源库每秒更新一次;从库查询结果与当前时间的差值就是业务可感知的延迟。该表必须只由源库写入,不能在从库执行更新。
第二步:定位是大事务、资源饱和还是锁等待
先查看并行应用线程的工作状态:
SELECT
WORKER_ID,
SERVICE_STATE,
LAST_APPLIED_TRANSACTION,
LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP,
LAST_ERROR_NUMBER,
LAST_ERROR_MESSAGE
FROM performance_schema.replication_applier_status_by_worker;
多数工作线程长期空闲而某一个线程持续执行,常意味着大事务或事务依赖链限制了并行度;全部线程忙且延迟扩大,应继续查资源。
SELECT
trx_mysql_thread_id,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS running_seconds,
trx_rows_modified,
trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;
SHOW ENGINE INNODB STATUS\G
长时间运行、修改行数很大的事务会形成回放瓶颈。SHOW ENGINE INNODB STATUS 中的 LATEST DETECTED DEADLOCK、行锁等待和 history list length 能帮助判断是否有锁竞争或 purge 压力。与此同时在操作系统侧观察从库资源:
iostat -x 1
vmstat 1
pidstat -dru -p "$(pidof mysqld)" 1
iostat 中设备 %util 长期接近 100%、await 明显升高,表示磁盘延迟是主要约束;pidstat 的 CPU 或 I/O 等待异常则提示应先隔离报表负载或扩容存储,而不是盲目提高并行线程。
第三步:安全启用或调整并行复制
在变更前先记录当前参数,并确认从库版本及复制拓扑支持 GTID:
SHOW VARIABLES WHERE Variable_name IN (
'replica_parallel_workers',
'replica_parallel_type',
'replica_preserve_commit_order',
'binlog_transaction_dependency_tracking'
);
MySQL 8.0 中可以先以保守值增加应用线程:
STOP REPLICA SQL_THREAD;
SET GLOBAL replica_parallel_type = 'LOGICAL_CLOCK';
SET GLOBAL replica_parallel_workers = 8;
SET GLOBAL replica_preserve_commit_order = ON;
START REPLICA SQL_THREAD;
LOGICAL_CLOCK 利用源库写入集的依赖信息让无冲突事务并行;replica_preserve_commit_order=ON 保持提交可见顺序,适合读流量会切换到该从库的场景。线程数应从 4 或 8 起逐级增加,每次观察 10 至 15 分钟的追平速度、CPU、I/O 延迟和锁等待。线程数超过 CPU 核数或存储承载能力后,通常只会增加上下文切换与 I/O 竞争。
如果源库也是 MySQL 8.0,可确认源端已提供较好的事务依赖信息:
SHOW VARIABLES LIKE 'binlog_transaction_dependency_tracking';
对写入模式允许时,可在维护窗口评估设为 WRITESET;这会影响 binlog 的依赖跟踪方式,应先在预发布环境验证,不能把它当作线上紧急止血手段。
一个完整的处置示例
某从库延迟 18 分钟,GTID 显示已接收但未执行的事务约 40 万个。replication_applier_status_by_worker 显示 4 个工作线程均忙,iostat 显示数据盘 await 只有 2ms、CPU 仍有余量。检查配置发现 replica_parallel_workers=4。
处理过程如下:
- 暂停报表任务,避免与回放竞争缓冲池和磁盘;
- 按上述命令将并行工作线程提高到 8,保持提交顺序;
- 每 5 分钟记录心跳延迟、GTID 差距、工作线程状态和 I/O 指标;
- 延迟开始稳定下降后保持观察,确认追平且持续 30 分钟无反弹;
- 将报表迁移到独立只读副本,再恢复报表任务。
如果看到单个事务运行数十分钟,即使线程数翻倍也不会显著改善。此时应先停止继续制造同类大事务:将批量更新拆成可提交的小批次,并为批处理设置限速与可重试边界。
预防措施
- 监控心跳延迟、GTID 差距、复制工作线程利用率、磁盘
await和长事务时长,并设置分级告警; - 大批量写入按主键范围分批提交,避免单事务修改数百万行;
- 报表、备份和在线 DDL 尽量放到独立副本,避免挤占复制回放资源;
- 在容量评审中记录峰值写入量、最长允许恢复时间和副本追平能力,定期进行压测;
- 切换流量前同时检查 SQL/IO 线程、心跳延迟和 GTID 一致性,不只依赖单个
Seconds_Behind_Source指标。
总结
复制延迟的正确处理顺序是:先确认链路和 GTID 差距,再区分大事务、锁等待与资源饱和,最后有节奏地调整并行回放能力。并行复制能解决可并行事务的吞吐瓶颈,却不能绕过单个大事务、慢存储或错误复制;保留现场、逐级变更并用业务心跳验证,才能让副本恢复既快又稳。
Discussion
评论