适用场景

本文适用于 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_RunningNo 说明问题在网络、认证或源端日志;Replica_SQL_RunningNo 时先处理 Last_SQL_Error,不要把错误复制当作性能延迟。两个线程都为 YesRetrieved_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

处理过程如下:

  1. 暂停报表任务,避免与回放竞争缓冲池和磁盘;
  2. 按上述命令将并行工作线程提高到 8,保持提交顺序;
  3. 每 5 分钟记录心跳延迟、GTID 差距、工作线程状态和 I/O 指标;
  4. 延迟开始稳定下降后保持观察,确认追平且持续 30 分钟无反弹;
  5. 将报表迁移到独立只读副本,再恢复报表任务。

如果看到单个事务运行数十分钟,即使线程数翻倍也不会显著改善。此时应先停止继续制造同类大事务:将批量更新拆成可提交的小批次,并为批处理设置限速与可重试边界。

预防措施

  • 监控心跳延迟、GTID 差距、复制工作线程利用率、磁盘 await 和长事务时长,并设置分级告警;
  • 大批量写入按主键范围分批提交,避免单事务修改数百万行;
  • 报表、备份和在线 DDL 尽量放到独立副本,避免挤占复制回放资源;
  • 在容量评审中记录峰值写入量、最长允许恢复时间和副本追平能力,定期进行压测;
  • 切换流量前同时检查 SQL/IO 线程、心跳延迟和 GTID 一致性,不只依赖单个 Seconds_Behind_Source 指标。

总结

复制延迟的正确处理顺序是:先确认链路和 GTID 差距,再区分大事务、锁等待与资源饱和,最后有节奏地调整并行回放能力。并行复制能解决可并行事务的吞吐瓶颈,却不能绕过单个大事务、慢存储或错误复制;保留现场、逐级变更并用业务心跳验证,才能让副本恢复既快又稳。