MySQL 复制延迟实测:大事务提交前 Seconds_Behind_Source 是 0、提交后跳到 5 再涨到 11,拆批后从库 1.5 秒追平
在 MySQL 8.0.46 双实例异步复制上,用 100 万行自造订单表复现大事务复制延迟。源库一条 UPDATE 改 100 万行执行约 4.7 秒,期间 Seconds_Behind_Source 一直是 0,提交后第一次采样直接显示 5,最高到 11;按主键每 1 万行一批拆成 100 批后,SBS 最高 1,从库 1.5 秒内追平。附只读 performance_schema 巡检 SQL 和可复跑脚本。
MySQL 8.0.46 实测:源库一条 UPDATE 改 100 万行要 4.7 秒,这期间从库 Seconds_Behind_Source(SBS)一直是 0;事务一提交,SBS 第一次采样就是 5,随后涨到 10/11,从库再花约 6.64 秒才应用完。SBS 用的是事件在源库开始执行的时间戳,所以一提交就把源库执行时长算了进去。同样 100 万行按主键每 1 万行一批拆成 100 批,SBS 最高 1,源库最后一批提交后 1.36–1.48 秒从库追平。
适用对象
- 大事务提交前 SBS 是 0、心跳也正常,从库确实没落后——只是事务还没进 binlog,延迟还没到;一提交,SBS 会先把源库执行时长(本次约 4.7 秒)一次性算进来。
- 按主键分批(本次每 1 万行一批)能把 SBS 峰值从 10/11 压到 1,源库最后一批提交后 1.5 秒内从库追平,代价是源库总耗时多约 1 秒。
- SBS 为 NULL 不是零延迟,是接收线程断了算不出来;监控应单独告警,同时结合 receiver_state、心跳延迟和 applying_behind_commit_s。
- 读从库的读写分离应用如果需要读到自己刚写的数据,必须走主库或先确认延迟,否则可能读到旧版本。
源库执行大事务时 SBS 为什么是 0,一提交就跳到 5?
一条 UPDATE 直接改 repl_lab.orders 的 100 万行,源库执行 4708ms / 4704ms。在源库执行这 4.7 秒里,从库 SHOW REPLICA STATUS 的 Seconds_Behind_Source 两轮都稳定显示 0,心跳延迟只有 0.01–0.03 秒,最高 0.54 秒;这时候事务没提交,binlog 里还没有它,从库只收到别的小事务。
源库提交后的第一次采样,SBS 直接 = 5,同一时刻心跳延迟 1.06 / 1.04 秒,正在应用事务落后提交 0.73 / 0.86 秒。之后 SBS 每秒约 +1,最高到 10(第一轮)/ 11(第二轮);心跳延迟最高 6.69 / 6.68 秒。从库在源库提交后约 6.64 秒(两轮一致)才应用完。
应用期间 Replica_SQL_Running_State 大部分时间显示 "Waiting for dependent transaction to commit",忙碌 worker 只有 1–2 个。我的理解(经验判断):源库是 COMMIT_ORDER 依赖跟踪、从库开了 preserve_commit_order,心跳小事务在源库排在大事务之后提交,从库就得等大事务先提交,4 个 worker 帮不上忙。
另跑了一轮(源库 4.43 秒),在提交后 2 秒、4 秒各跑一次本文的只读 SQL:applying_behind_commit_s = 2.23 / 4.24,同一时刻 SBS = 6 / 8,SBS 正好多出约 4 秒。应用完后 worker 1 的 last_applied_lag_s = 6.566、last_applied_took_s = 6.353——从库应用这一个事务就用了 6.35 秒,比源库执行还慢。
本节判断
- 源库还在跑大事务时,SBS=0 和心跳延迟 0.01–0.03 秒是一致的:从库这时真的没落后,但你也看不出「6 秒后会落后」。想提前知道,得在源库侧盯长事务(例如 information_schema.innodb_trx,经验做法、本次未实测),从库指标给不了预警。
- SBS 比「落后源库提交时间」多出来的约 4 秒,约等于源库执行这条语句的时间:行格式下事件时间戳是语句在源库开始执行的时间,不是提交时间(见文末官方文档)。
| 阶段 | 第 1 轮 | 第 2 轮 | 备注 |
|---|---|---|---|
| 源库执行耗时 | 4708 ms | 4704 ms | 事务未提交 |
| 源库执行期间 SBS | 0 | 0 | 心跳延迟 0.01–0.03 s |
| 提交后第一次采样 SBS | 5 | 5 | 心跳延迟 1.06/1.04 s |
| SBS 最高 | 10 | 11 | 心跳延迟最高 6.69/6.68 s |
| 从库应用完延迟 | 约 6.64 s | 约 6.64 s | 两轮一致 |
同样 100 万行拆 100 批:SBS 最高 1,源库最后一批提交后 1.5 秒追平
把同一条 UPDATE 拆成按主键每 1 万行一个事务,一共 100 批。源库总耗时 5767ms / 5887ms,比一条 UPDATE 慢约 1 秒,多出来的就是 100 次提交和客户端往返。
从库侧两轮 SBS 最高都只有 1;心跳延迟最高 1.58 / 1.52 秒;源库最后一批提交后 1.48 / 1.36 秒从库就应用完。两端 SUM(amount) 一致。
对比很直观:大事务把 SBS 峰值打到 10/11,拆批后只有 1;源库提交后到从库应用完,从约 6.64 秒降到 1.36–1.48 秒。代价是源库多花约 1 秒,还要自己写循环控制批次。
本节判断
- 拆批不是把延迟变成零,而是把峰值压到 1–2 秒,让从库边写边跟;读写分离场景里读到旧数据的窗口小很多。
- 批次大小要自己试:1 万行在本机表现可以,生产上要结合单行大小、索引更新、从库算力调整。
| 指标 | 一条大事务 | 每 1 万行一批 × 100 |
|---|---|---|
| 源库总耗时 | 4708 / 4704 ms | 5767 / 5887 ms |
| SBS 最高 | 10 / 11 | 1 / 1 |
| 心跳延迟最高 | 6.69 / 6.68 s | 1.58 / 1.52 s |
| 末批提交后追平 | 约 6.64 s | 1.48 / 1.36 s |
STOP REPLICA IO_THREAD 后 SBS 变成 NULL:不是没延迟,是算不出来
我执行 STOP REPLICA IO_THREAD 模拟接收线程断开,源库继续写 5 秒心跳。SHOW REPLICA STATUS 显示:Replica_IO_Running: No、Replica_SQL_Running: Yes、Seconds_Behind_Source: NULL、Replica_SQL_Running_State: Replica has read all relay log; waiting for more updates。
只读 SQL 第 1 段显示 receiver_state = OFF,since_last_heartbeat_s = 30.3。从库心跳表算出来是 110.95 秒——这 5 秒里源库写的 10 次心跳一条都没到从库,从库手里最新的心跳还是上一个实验结束时写的,所以是 111 秒前。这个数说明「断了」,但不等于真实积压了 111 秒的数据;心跳延迟只有在源库持续写心跳时才是可信的延迟。
START REPLICA IO_THREAD 后 2 秒,IO Running 恢复 Yes,SBS 显示 0,心跳延迟 2.54 秒。这 2.54 秒不是积压:心跳循环在 STOP 期间那 5 秒写完就停了,从库收到的最后一条心跳本来就是约 2.5 秒前写的。断开期间只有 10 条小更新,恢复后很快就补上了。
本节判断
- SBS = NULL 是「算不出来」:接收线程或应用线程没在跑时就是 NULL。监控如果把 NULL 当 0 或直接跳过,会漏掉整段中断。
- 心跳表的时间差要配合「源库一直在写心跳」才有意义;心跳任务自己停了,这个差值会一直涨,看起来像延迟。
| 时刻 | Replica_IO_Running | SBS | 心跳延迟 | 说明 |
|---|---|---|---|---|
| STOP 后源库写 5 秒心跳 | No | NULL | 110.95 s(最新心跳是上个实验留下的) | receiver_state=OFF,since_last_heartbeat_s=30.3 |
| START 后 2 秒 | Yes | 0 | 2.54 s | 接收恢复;2.54 s 是距最后一次写心跳的时间 |
我在只读 SQL 和采样脚本里踩的两个坑
第一个坑出现在只读 SQL 第 3 段,我想用 NULLIF(APPLYING_TRANSACTION_START_APPLY_TIMESTAMP, '0000-00-00 00:00:00.000000') 处理空闲 worker,结果从库直接报错:ERROR 1525 (HY000) at line 28: Incorrect TIMESTAMP value: '0000-00-00 00:00:00.000000',第 3 段整段没出结果。改成 CASE WHEN APPLYING_TRANSACTION <> '' THEN ... END 判断事务列是否为空后正常。
第二个坑在采样脚本:mysql 客户端带了 -N(不输出列名),结果 SHOW REPLICA STATUS\G 的输出没有「Seconds_Behind_Source:」这个前缀,awk 什么都没抓到,第一轮只拿到心跳数据,SBS 全空,只能重跑。
两个都是小问题,但都会让人误判:第一个让最关键的 worker 数据整段缺失,第二个是脚本「看起来在跑」、其实什么都没采到。
本节判断
- 空闲 worker 的时间列是 0 值,但在本机 8.0.46 上拿 '0000-00-00 00:00:00.000000' 字面量去比较就直接报 ERROR 1525;先判断 APPLYING_TRANSACTION / LAST_APPLIED_TRANSACTION 是否为空更稳。
- 解析 SHOW REPLICA STATUS\G 的那条命令不要加 -N;或者干脆只用 performance_schema 的表,按列取值,不靠文本解析。
我的建议:监控和拆分怎么落地
基于上面的三组实验,我会把延迟监控拆成三层:接收层看 receiver_state / Replica_IO_Running 和 since_last_heartbeat_s;应用层看 applying_behind_commit_s 和 worker 状态;对外层看 SBS 和心跳延迟。SBS 为 NULL 要单独告警,不能当 0 也不能忽略。
大批量 UPDATE/DELETE 先按主键分批(本次每 1 万行一批),每批提交;批次之间可以 sleep 看从库延迟再继续。读写分离的应用,如果读从库时可能要读到自己刚写的数据,就走主库,或者先确认延迟小于业务容忍窗口。
从库出现延迟时我先做四步
- 01
1
确认接收线程是否正常
看 Replica_IO_Running 和 receiver_state;如果是 OFF 或 NULL,先解决网络/主库可用性,不是调 SQL。
- 02
2
看应用层落后提交时间
查 performance_schema 里的 APPLYING_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP 和 last_applied_lag_s,判断是单个大事务卡住还是一直积压。
- 03
3
和心跳延迟交叉验证
源库持续写心跳时,从库心跳延迟就是端到端滞后;它和 SBS 对不上时(比如大事务刚提交,SBS 比心跳延迟高约一个源库执行时长),以心跳和 applying_behind_commit_s 为准。
- 04
4
大事务拆分或调整业务窗口
确认是大事务造成,按主键拆批并 sleep 控制节奏;如果业务不允许拆分,考虑错峰执行或先通知读写分离方。
参考依据
以下来源用于确认市场趋势、政策背景和术语边界;具体落地方案仍以客户的数据范围、权限和交付目标为准。
常见问题
Seconds_Behind_Source 是不是只要为 0 就代表从库没延迟?
SBS=0 只说明此刻从库没在追旧事件。本次大事务在源库执行的 4.7 秒里 SBS 一直是 0,提交后第一次采样就是 5,随后涨到 10/11,从库又用了约 6.64 秒才追平。
SBS 最高 10/11,心跳延迟最高只有约 6.7 秒,为什么对不上?
SBS 按事件在源库上开始执行的时间算,心跳按写入(提交)时间算。大事务一提交,SBS 就把源库执行时长(本次约 4.4–4.7 秒)算进去了,所以比心跳延迟高。
我监控里 SBS 显示 NULL,是不是可以忽略?
不能。NULL 表示接收线程或应用线程没在跑、延迟算不出来,本次 STOP REPLICA IO_THREAD 后就是 NULL。它比一个大数值更容易被漏掉,建议单独告警。
大 UPDATE 拆成小批有什么要注意的?
拆批后就不再是一个原子事务,中途失败会只改了一部分,所以要按主键范围记录进度、保证可重跑。本次按主键拆 100 批,两端 SUM(amount) 一致,源库总耗时多约 1 秒(5767/5887ms 对 4708/4704ms)。
我能不能把这些数字直接当生产环境 SLA?
不能。这是本机测试实例、100 万行自造数据,只说明相对关系。生产需用同一套只读 SQL 在从库上复测,按实际延迟和心跳延迟定告警阈值。
下一步
推荐动作
你提供从库上这份只读 SQL 的输出和 SHOW REPLICA STATUS(可脱敏),我们判断延迟是接收、应用还是大事务造成,给出分批和告警口径建议;生产变更由你方授权执行。