运营同学反馈:后台发布的商品,前台列表要过好几秒甚至几十秒才刷出来;财务报表跑的是从库,数字总比主库少一截。SHOW SLAVE STATUS\G 一看,Seconds_Behind_Master 从平时的 0 变成 300 多,还在缓慢上涨。这就是典型的 MySQL 主从复制延迟。延迟本身不会让数据丢,但会让读写分离变成"读旧数据",严重时还会因为 relay log 堆积把磁盘写满。下面从怎么准确测量延迟讲起,梳理六类根因和对应的落地方案。
一、先把延迟测量准:Seconds_Behind_Master 的局限
绝大多数人的第一步是:
mysql> SHOW SLAVE STATUS\G # MySQL 8.0.22 起推荐等价写法:SHOW REPLICA STATUS\G
输出里和延迟直接相关的字段有这几个:
- Seconds_Behind_Master:SQL 回放线程落后主库的秒数,最常用,但它只在 SQL 线程正常运行时才有意义。
- Slave_IO_Running / Slave_SQL_Running:两个线程是否为 Yes。任何一个不是 Yes,Seconds_Behind_Master 都会显示为 NULL,此时不能读作"没有延迟",而应读作"复制已经断了"。
- Read_Master_Log_Pos 与 Exec_Master_Log_Pos:已读取和已执行的主库 binlog 位置,两者的差值是更原始的字节级延迟。
- Relay_Log_Space:relay log 占用的磁盘总量。持续增长意味着 SQL 线程追不上,要注意
relay_log_space_limit与磁盘水位。 - Last_IO_Errno / Last_SQL_Errno:错误码,决定能否自动恢复。1032(找不到记录)、1062(主键冲突)、1236(主库 binlog 已被清理)是最常见的三种。
Seconds_Behind_Master 有三个已知失真场景:其一,它以 SQL 线程当前正在回放的事件时间戳计算,如果 SQL 线程空闲等待事件,可能显示 0;其二,大事务在提交前不会体现延迟,一旦提交立刻暴涨;其三,主从机时钟不一致时数值不可信。所以生产环境推荐用 Percona 的 pt-heartbeat 做真实延迟测量:它每隔固定时间在主库写一条带时间戳的记录,从库比较当前时间与记录时间的差值。
# 1) 主库写心跳(daemonize 常驻) pt-heartbeat -h 主库IP -u repl -p'密码' -D percona --create-table --update \ --interval=1 --daemonize # 2) 任意位置监控从库落后多少秒 pt-heartbeat -h 从库IP -u repl -p'密码' -D percona --monitor pt-heartbeat -h 从库IP -u repl -p'密码' -D percona --monitor --interval=5 --frames=1m,5m,15m # 3) 停止心跳 pt-heartbeat --stop
--frames 给出的 1 分钟 / 5 分钟 / 15 分钟滑动窗口值,比单点快照更能反映真实情况。一般把告警阈值设为:5 分钟窗口均值大于 3 秒告警,大于 10 秒并且持续 5 分钟以上电话告警。
二、延迟为什么会大:六类根因
| 类型 | 典型表现 | 快速验证方法 |
|---|---|---|
| 主库大事务 | 延迟瞬间跳到几百秒然后缓慢回落 | 解析主库 binlog 找大事务:mysqlbinlog --base64-output=decode-rows -v mysql-bin.0000xx | grep -c 'INSERT' |
| 从库配置低于主库 | 全天持续缓慢落后,业务高峰加剧 | 对比两机的 CPU、内存、磁盘 iostat 与 MySQL 参数 |
| 单线程回放 | 主库 CPU 只用了 30%,从库单核打满 | SHOW VARIABLES LIKE 'slave_parallel_workers' 返回 0 或偏小 |
| 表没有主键 | ROW 格式下每行变更都全表扫描一次 | 查 information_schema 找无主键表(见第三节命令) |
| 从库负载干扰 | 备份、报表查询、慢 SQL 抢占 IO 与 CPU | SHOW PROCESSLIST 看从库自身连接 |
| 网络与 binlog 体积 | Read_Master_Log_Pos 落后明显(IO 线程慢) | 对比网络带宽与 binlog 每秒生成量 |
2.1 最隐蔽的一类:主库大事务
主库一条 DELETE FROM logs WHERE create_time < '2024-01-01' 删掉 800 万行,在主库 20 秒完成并写入一个数百 MB 的事务到 binlog;从库必须把这个事务原样回放,同样的删除要跑 20 秒以上,而这段时间内从库对外呈现的延迟就是 20 秒以上。更糟的是:从库在这个事务提交之前,Seconds_Behind_Master 可能一直是 0,看起来"很健康"。所以拆事务永远是第一优先级。
2.2 次隐蔽的一类:ROW 格式下的无主键表
当 binlog_format = ROW(MySQL 5.7.7 之后的默认值,也是生产推荐值)时,binlog 里记录的是行变更前后的镜像。回放时要靠主键或唯一键定位到具体行;如果表既没主键也没合适的二级索引,每变更一行就要做一次全表扫描。删 1 万行就是 1 万次全表扫描,延迟必然失控。
三、动手前的检查清单
-- 1) 确认复制线程状态与延迟
SHOW SLAVE STATUS\G
-- 2) 对比 IO 线程读取位置与 SQL 线程执行位置(字节级差距)
SELECT @@gtid_mode, @@binlog_format, @@slave_parallel_type, @@slave_parallel_workers;
-- 3) 找出没有主键的 InnoDB 表(ROW 格式下必查)
SELECT t.table_schema, t.table_name, t.table_rows
FROM information_schema.tables t
WHERE t.engine = 'InnoDB'
AND t.table_schema NOT IN ('mysql','sys','performance_schema','information_schema')
AND NOT EXISTS (
SELECT 1 FROM information_schema.statistics s
WHERE s.table_schema = t.table_schema AND s.table_name = t.table_name
AND s.index_name = 'PRIMARY');
-- 4) 看从库自身有没有长查询抢资源
SELECT id, user, host, db, command, time, LEFT(state,40) AS state, LEFT(info,80) AS sql_head
FROM information_schema.processlist
WHERE command != 'Sleep' AND time > 10
ORDER BY time DESC LIMIT 20;
-- 5) 看是否卡在特定事务(比如备份导致的 FLUSH TABLES WITH READ LOCK)
SELECT trx_id, trx_mysql_thread_id, trx_started, trx_rows_locked, LEFT(trx_query,60)
FROM information_schema.innodb_trx ORDER BY trx_started LIMIT 10;
四、六类优化手段
4.1 开启并行复制(收益最直接)
MySQL 5.6 起支持基于库(schema)的并行回放,跨库才有效,实际收益有限。真正有用的是 MySQL 5.7 引入的 LOGICAL_CLOCK(组提交依赖)与 MySQL 8.0 引入的 WRITESET(基于行冲突检测)。配置如下:
[mysqld] # MySQL 5.7 slave_parallel_type = LOGICAL_CLOCK slave_parallel_workers = 8 slave_preserve_commit_order = 1 # MySQL 8.0(8.0.26 之后参数名改为 replica_*,旧名多数版本仍兼容) slave_parallel_type = LOGICAL_CLOCK slave_parallel_workers = 8 binlog_transaction_dependency_tracking = WRITESET slave_preserve_commit_order = 1 master_info_repository = TABLE relay_log_info_repository = TABLE relay_log_recovery = ON
-- 在线调整(无需重启) STOP SLAVE SQL_THREAD; SET GLOBAL slave_parallel_workers = 8; SET GLOBAL binlog_transaction_dependency_tracking = 'WRITESET'; START SLAVE SQL_THREAD;
worker 数怎么定?经验值是从 CPU 逻辑核数的 1/4 起步,常见区间 4-16。超过 16 通常收益递减,还带来更多协调开销。判断标准:观察从库 SHOW PROCESSLIST 里有多少个 Coordinator 处于忙碌状态,如果总有 worker 闲着且延迟还在涨,说明瓶颈不在并发度上。
4.1.1 三个并行复制参数的含义与生效前提
很多团队把参数配上之后发现延迟毫无变化,原因通常是忽略了 LOGICAL_CLOCK 的依赖来源。slave_parallel_type = LOGICAL_CLOCK 的并行粒度取决于主库 binlog 里记录的 last_committed / sequence_number,而这两个值来自主库的两阶段提交与组提交(binlog group commit)。也就是说:
- slave_parallel_workers:真正执行回放的 worker 线程数,默认 0(串行),MySQL 8.0 多数版本的默认值为 4。设为 0 时前面两个参数都失效,这是最常见的原因。
- slave_parallel_type:
DATABASE按库并行(只有跨多库写入才有用,实际基本无效);LOGICAL_CLOCK按"主库上同一组提交的事务"并行,这是 5.7 的默认可用方案。 - slave_preserve_commit_order = 1:保证从库的事务提交顺序与主库一致,代价是轻微的等待;配合它才能让从库自身的 GTID 集合不出现空洞,对要做"从库再挂从库"和故障切换的场景建议开启。
- binlog_transaction_dependency_tracking = WRITESET(MySQL 8.0):不依赖"是否同时提交",而是按事务修改行集合(hash)判定是否冲突。即使主库并发度很低、每组只有一两个事务,它也能找出互不冲突的事务并行回放,因此它对低并发但大事务多的场景提升最明显。
COMMIT_ORDER是默认值,WRITESET_SESSION额外保证同一会话的顺序。
判定主库是否具备组提交条件,可以看主库每秒的提交数与 binlog 写入频率;如果主库本身是串行小批量写入(如每秒几十个事务、且每批只有一个事务),LOGICAL_CLOCK 基本拆不出并行度,此时必须切换到 WRITESET 或者改造业务批量提交。
-- 确认并行参数是否真的生效 SHOW VARIABLES LIKE 'slave_parallel_type'; SHOW VARIABLES LIKE 'slave_parallel_workers'; SHOW VARIABLES LIKE 'slave_preserve_commit_order'; SHOW VARIABLES LIKE 'binlog_transaction_dependency_tracking'; -- 看 worker 是否真的在干活(每个 worker 对应一个 Slave_worker 线程) SELECT id, user, host, command, time, LEFT(state,50) AS state FROM information_schema.processlist WHERE user = 'system user' ORDER BY id; -- 主库侧确认 binlog group commit 相关参数(默认即开启) SHOW VARIABLES LIKE 'binlog_group_commit_sync_delay'; SHOW VARIABLES LIKE 'binlog_group_commit_sync_no_delay_count'; SHOW VARIABLES LIKE 'binlog_order_commits';
一个可选的加速手段:binlog_group_commit_sync_delay(微秒,默认 0)让主库稍微等一等再刷盘,从而把更多事务并进同一个组,提高从库并行度;代价是主库事务提交延迟增加,一般只在写入密集且主从延迟长期居高时谨慎尝试,取值建议在几百到几千微秒之间。
4.2 从库放宽持久化参数
从库的数据可以通过重做主库来找回,所以在可接受"宕机后重建从库"的架构里,可以适当放宽刷盘策略换取回放速度:
[mysqld] innodb_flush_log_at_trx_commit = 2 # 每秒刷一次 redo,宕机可能丢 1 秒 sync_binlog = 1000 # 每 1000 次提交同步一次 binlog innodb_flush_neighbors = 0 # SSD 场景必设,避免无谓的相邻页合并刷 innodb_io_capacity = 2000 # 默认 200 对 SSD 严重低估 innodb_buffer_pool_size = 32G # 尽量不低于主库
警告:这些参数一定要只写在从库的配置文件里,并且要在部署文档中标注。曾经有团队把从库的 innodb_flush_log_at_trx_commit=2 配置随自动化的模板推回了主库,直接把持久性降级。
4.3 拆分大事务
批量删除、归档、数据订正统一改成"分批 + 间隔"。推荐使用 pt-archiver 这类成熟工具,它自带限速和行数控制:
# 按主键分批归档,每批 1000 行,事务之间睡 0.5 秒给从库喘息
pt-archiver --source h=主库IP,D=app,t=logs,u=archiver,p='密码' \
--dest h=归档库IP,D=archive,t=logs \
--where "create_time < '2024-01-01'" \
--limit 1000 --txn-size 1000 --bulk-delete --progress 10000 --sleep 0.5
没有工具时用 LIMIT 循环也可以,但务必保证每批之间有短暂 sleep,否则瞬间写压力依然会把延迟顶上去。
4.4 补主键、减 binlog 体积
给无主键表加自增主键是 ROW 格式下的刚需。另外 binlog_row_image 默认是 FULL(记录整行前后镜像),对宽表来说体积很大;在确保表有主键且只做部分列更新的前提下,可以设为 MINIMAL,binlog 体积通常能下降三到六成,IO 线程的传输压力随之下降:
SET GLOBAL binlog_row_image = 'MINIMAL'; # 需评估备份工具与闪回工具是否兼容
4.5 隔离从库负载
不要在承担读流量的从库上跑物理备份、跑报表、跑定时任务。正确姿势是再挂一个专用从库(或延迟从库)。延迟从库可以用 Percona Toolkit 或 MySQL 自带的 CHANGE MASTER TO MASTER_DELAY = N(8.0 为 SOURCE_DELAY)设置故意延迟 N 秒,专门用来应对误删。
4.6 架构层兜底
对一致性要求高的读,不要走从库。常见做法是按场景分流:用户自己刚写入的数据("我的订单")强制走主库,公共列表(商品列表)走从库;或者在中间件层面做 GTID 等待,例如 ProxySQL 的 mysql-wait_timeout 配合 GTID 一致性读。半同步复制(rpl_semi_sync_master_enabled / MySQL 8.0 为 rpl_semi_sync_source_enabled)能降低主库故障丢数据的风险,但它不解决延迟,只是保证至少一个从库收到了事件,回放到数据可用之间还是有时间间隔。
五、延迟应急处理步骤
- 确认不是"复制已断":Sl* 两个线程必须都是 Yes;Seconds_Behind_Master 为 NULL 先看 Last_IO_Errno / Last_SQL_Errno。
- 用 pt-heartbeat 的 1m/5m 窗口值确认延迟的真实幅度和趋势,排除时钟误差干扰。
- 区分瓶颈在 IO 线程还是 SQL 线程:对比 Read_Master_Log_Pos 与 Exec_Master_Log_Pos,前者落后说明是网络或 binlog 体积问题,后者落后说明是从库回放能力问题。
- 查当前有无大事务在执行:
SHOW PROCESSLIST+information_schema.innodb_trx,找到源头后评估能否终止。 - 临时提高并行度:
SET GLOBAL slave_parallel_workers = 8后重启 SQL 线程。 - 若从库有备份或慢查询在跑,先停掉,观察 30 秒。
- 若 relay log 已经堆到磁盘告警,评估主库 binlog 保留时间,必要时先从主库重新拉取一个从库,而不是干等追平。
- 业务侧临时降级:把强一致的读流量切回主库,避免用户看到脏数据。
- 追平后复盘:定位本次触发点是大事务、还是配置不足,写进变更规范。
- 把巡检固化:pt-heartbeat 接入监控系统,配置分级告警。
小结
MySQL 主从复制延迟的处理顺序是"先测准、再定位、先架构后参数":Seconds_Behind_Master 会失真,生产要用 pt-heartbeat;瓶颈要先分清是 IO 线程(网络与 binlog 体积)还是 SQL 线程(回放能力);见效最快的三件事是开启 WRITESET 并行复制、补齐表主键、把大事务拆成小批量。relay_log_recovery = ON 可以让从库异常崩溃后自动恢复,建议常态化开启。日常避坑:不要指望半同步解决延迟;不要在同一台从库上既跑备份又承担线上读;不要把从库的宽松刷盘参数误推给主库;也不要在无主键的大表上做一次性百万行的 UPDATE 和 DELETE。
企业QQ咨询




