MySQL 主从延迟监控的顶级谎言:Seconds_Behind_Master
场景:从库 Seconds_Behind_Master=0,业务反馈刚写入的数据查不到。主从同步监控显示一切正常,实际数据延迟了 3 分钟。 路径:SBM 更新机制 → 时钟偏差多线程复制/relay log 堆积 → GTID 双指标 → pt-heartbeat 方案 → 监控代码替换
上篇我们聊了 MySQL 隔离级别选型——RC 和 RR 在并发写入场景下的血泪教训。这篇从主从复制说起:一个几乎所有 MySQL 用户都踩过的坑——Seconds_Behind_Master。
凌晨 2 点,告警平台响了。 不是 CPU 爆了,不是 OOM,是业务线反馈:用户在 APP 下单后查不到订单,持续了接近 3 分钟。等 DBA 登录从库一看,复制状态全是 Yes,SBM=0。每个看起来都正常,但数据就是没到。 这种"正常但数据不一致"的幽灵问题,比明确告警更难排查——因为所有监控工具都说它没问题。
mysql> SHOW SLAVE STATUS\G
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Log_File: mysql-bin.000123
Read_Master_Log_Pos: 458762
Relay_Log_File: relay-bin.000456
Relay_Log_Pos: 458762
Relay_Master_Log_File: mysql-bin.000123
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 0
Seconds_Behind_Master=0,一切正常——对吗? 但这时候在主库 INSERT 一条数据,从库 3 分钟后才查得到。
如果你是刚入门的 DBA,看到 SBM=0 你会放心。如果你有几年经验,你知道 SBM 不完全靠谱。但如果你真的踩过这个坑,你会发现:SBM 不是"不完全靠谱",是大多数场景下完全不能信。
以下分析基于 MySQL 8.0.32,5.7 和 8.0 的差异会单独标注。

SBM 的计算逻辑听起来极其简单:从库 SQL 线程执行完一个 relay log event 后,拿当前系统时间减去该 event 在 Master 上记录的时间戳。
公式:SBM = now() - event_timestamp
但这条公式在源码层面的执行路径,每一环都有坑。
时间戳从哪来:binlog event header 的 4 个字节
每个 binlog event 的头部都有一个固定格式,由 MySQL 源码 libbinlogevents/include/event_header.h 定义:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp (4 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| event_type | server_id (4 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| event_length (4 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ... payload (variable length) ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
前 4 个字节是 event 创建时的 Unix timestamp(秒级精度)。由 Master 在写入 binlog 时打上,Slave 用这个时间戳和当前系统时间做差。
这里已经有第一层精度损失:timestamp 精度只有秒级。一个事务提交在 10:00:00.001,event header 里写的是 10:00:00。Slave 在 10:00:01.999 执行完这个 event,SBM 可能是 0 或 1——因为两边都是向下取整,时间差被吞掉了最多 2 秒。
第二层:不是每个 event 都有独立的时间戳。在一个事务内部,BEGIN、行变更、COMMIT 等 event 共享同一个 timestamp(事务开始的时间)。如果一个事务包含 1 万行变更,最后一行变更的 event 和第一行的时间戳一样——但实际它们跨越了可能几秒的执行时间。
这里我已经给了两层精度损失,但 SBM 还有更大的问题。
SQL 线程 event loop:SBM 只在特定时刻更新
SBM 不是实时指标。它只在 SQL 线程从 relay log 取出一个 event 并执行完成后才计算一次。这段逻辑集中在 MySQL 源码的 sql/rpl_slave.cc 中。简化后的 event loop:
while (running) {
event = read_next_event_from_relay_log(); // ← 可能阻塞(relay log 已空)
apply_event(event); // ← 执行这个 event
calc_seconds_behind_master(); // ← 刷新 SBM —— 只有这步才更新
update_positions(); // 更新位点
}
关键:SBM 的更新频率 = SQL 线程执行 event 的频率。如果 SQL 线程因为 relay log 为空而阻塞在 read_next_event_from_relay_log(),SBM 不会更新——它保持上次执行完的值。
这个"保持旧值"的行为在实践中比你想的更危险:
T0: SQL 线程执行完最后一个 event,SBM=0
T0~T3: relay log 为空,SQL 线程等待
T3: IO 线程拉取到一个新 binlog event,写入 relay log
T3~T5: SQL 线程执行这个 event,SBM=now()-event.timestamp=0(因为 event 时间戳接近当前时间)
T5: relay log 再次为空,SQL 线程等待
T0~T5:SBM 始终显示 0
但 Master 在 T0~T5 之间可能已经写入了 50 个新事务。
SBM 不测量 IO 线程从 Master 拉取了多少数据,它只测量 SQL 线程还有多少没执行完。

上图中的关键时间窗口: - T0:Master 写入 binlog - T1:IO 线程收到并写入 relay log(SBM 盲区) - T2:SQL 线程执行 event 并刷新 SBM - T3~T5:SBM=0,但新的 binlog 还在网络上
SBM=0 只代表 SQL 线程无积压,不代表 IO 线程无积压,更不代表 Master 和 Slave 数据一致。
Seconds_Behind_Master 不是延迟测量工具——它是一个 SQL 线程空闲指示器。
时钟偏差:SBM 对系统时钟的脆弱依赖
SBM 的计算依赖主库和从库的系统时间差,但这两个服务器的 clock 几乎不可能精确同步。
# 主库时间比从库快 30 秒
# event timestamp = 2026-07-26 10:00:30
# slave 当前时间 = 2026-07-26 10:00:00
# SBM = -30 → MySQL 把负数处理为 0
# 主库时间比从库慢 30 秒
# event timestamp = 2026-07-26 10:00:00
# slave 当前时间 = 2026-07-26 10:00:30
# SBM = 30 → 虚高 30 秒
MySQL 官方文档明确写了:如果主从时钟不一致,SBM 不可信。
你可以直接检验你的环境:
-- 主库时间
SELECT NOW() AS master_time, @@GLOBAL.server_id;
-- 从库时间
SELECT NOW() AS slave_time, @@GLOBAL.server_id;
-- 看差值
大部分团队的 MySQL 服务器没有统一 NTP,或 NTP 配置了不同的时间源。两个服务器之间的时钟差几分钟到几十分钟很常见。在这种环境下谈 SBM 没有任何意义。
但还有一个更隐蔽的问题:即使 NTP 同步了,NTP 本身的同步机制也有误差。 ntpdate 做时间跳跃(可能导致 SBM 瞬间跳变),ntpd 做时间渐变(在渐变期间 SBM 持续不准确)。云服务器的 chrony 通常误差在 1-10ms 级别——对大多数应用来说够了,但对秒级精度的 SBM 来说,这个误差会被放大到不可接受的水平。
多线程复制下的语义漂移
MySQL 8.0 默认启用 slave_parallel_workers=4(MTS——Multi-Threaded Slave)。
单线程复制时,SBM 的语义是清晰的:最后一个执行完的 event 离现在多久。
MTS 下,多个 worker 并行重放 relay log event。SBM 的计算方式变了——它不再取最后一个 event,而是取所有 worker 中 last_event_timestamp 的最小值:
# MTS 模式下 SBM 的实际计算逻辑
workers_last_time = [
worker_1.last_event_timestamp, # 10:00:00
worker_2.last_event_timestamp, # 09:59:50
worker_3.last_event_timestamp, # 09:59:40 ← 最慢的 worker
]
sbm = now() - min(workers_last_time) # = now() - 09:59:40
看到问题了吗?按这个公式,SBM 会显示最慢 worker 的滞后时间。
但在实际实现中,slave_preserve_commit_order 参数加剧了这个问题。当这个参数为 ON,worker 必须按 Master 上的事务提交顺序提交到 relay log。如果 worker-3 先执行完但必须等 worker-1 的提交完成才能提交自己的,这段时间 worker-3 已经更新了自己的 last_event_timestamp,但 SBM 的最小值依然卡在较早的时间点上。
worker-1: event@10:00:00(正在等待 worker-2 提交)
worker-2: event@09:59:50(正在重放)
worker-3: event@09:59:40(已执行完,但等待 worker-1 先提交)
SBM = now() - 09:59:40 → 这个值看起来正常(有延迟),但不反映哪个 worker 导致了延迟
与之相关的还有 slave_parallel_type 参数:
| 类型 | MySQL 版本 | 逻辑 | 对 SBM 影响 |
|---|---|---|---|
| DATABASE | 5.6+ | 同一 database 内的 event 串行执行,不同 database 并行 | SBM 波动相对小 |
| LOGICAL_CLOCK | 5.7+ | 基于事务的锁信息判断并行度 | SBM 波动大,依赖 workload |
LOGICAL_CLOCK 模式下,SBM 的不确定性更高:如果两个事务在 Master 上没有任何锁冲突,它们可以在 Slave 上完全并行。但如果 workload 突然变成串行密集型(大量 DDL 或行锁争用),并行度急剧下降,SBM 可能瞬间跳高。
直接检查每个 worker 的实际进度:
SELECT WORKER_ID, LAST_APPLIED_TRANSACTION_END_TIMESTAMP,
LAST_APPLIED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP,
APPLYING_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP
FROM performance_schema.replication_applier_status_by_worker
ORDER BY LAST_APPLIED_TRANSACTION_END_TIMESTAMP ASC;
MTS 模式下,SBM 的计算语义从"一个游标的位置"变成了"多个游标的最小值"——这不是同一个指标,也不能用同样的方式来理解。
Relay log 堆积:SBM 的盲区
当 IO 线程拉取 binlog 的速度远超 SQL 线程消费速度时,relay log 持续堆积。但 SBM 不受 relay log 大小影响。
Relay_Log_Space=2GB(严重堆积)
SQL 线程当前在处理 event@10:00:00(10 分钟前的 event 还没执行)
SBM = now() - 10:00:00 = 30 秒
30 秒的 SBM,2GB 的 relay log 堆积——这 30 秒告诉你"轻度延迟",但 2GB 告诉你"快撑不住了"。

relay_log_space_limit 参数是一个容易被忽略的陷阱。它的默认值是 0(不限制),但如果有设置,当 relay log 大小触达限制时,IO 线程会停止拉取新 binlog,直到 SQL 线程消费释放空间。在这个阶段,SBM 不会突然跳高——它只是缓慢增长,看起来像轻度延迟,但整条复制链路实际上已经半瘫痪了。
SBM 的问题不是"它有时候不准",而是你没法知道它什么时候准、什么时候不准。
大事务场景:SBM 的一个独特表现
有一种场景会让 SBM 出现完全没有意义的数值:大事务。
假设 Master 执行了一个 DELETE FROM orders WHERE create_time < '2025-01-01',删除了 500 万行。在 binlog_format=ROW 模式下,这会产生一个巨大的单个事务,所有变更写入一个或多个 binlog event。
Master 端:
BEGIN → event@10:00:00
变更 100 万行 → 写入 binlog event
...
变更 500 万行 → 写入 binlog event
COMMIT → event@10:02:30(事务实际结束时间)
SBM 在这个事务中表现:
从库收到第一个 event(10:00:00),开始重放
在重放过程中,SBM 持续增长(now() - 10:00:00)
等到所有 500 万行重放完成,SBM 可能显示 3 分钟甚至更高
关键误导:SBM 的 3 分钟不等于"从库落后主库 3 分钟"
它只说明"这个事务重放了 3 分钟"——而 Master 上这个事务也执行了 2.5 分钟
更常见的例子是 ALTER TABLE 在 pt-online-schema-change 下,触发 trigger 产生的批量更新。一个大 DDL 引起的 SBM 飙升,80% 的情况下不代表从库真的"落后"了——它只是在如实执行这个大事务。
大事务场景下,GTID 缺口检测同样会受影响:如果这个大事务是一个 GTID 编号,那么在它完成前,GTID 缺口始终存在。但 relay log 堆积量是可靠信号——IO 线程可能几秒就拉完了,但 SQL 线程需要几分钟来重放。
那为什么大家都在用 SBM?
这不是设计失误,是设计假设和实际使用的错位。
Seconds_Behind_Master 在 MySQL 4.0/5.0 时代被引入,那时: - 单线程复制是唯一模式(无 MTS 语义模糊) - 主库 QPS 在几百到几千,远低于今天的上万甚至几十万 - 网络链路简单,relay log 堆积很少出现 - 服务器数量少,NTP 同步相对容易保证
那个场景下,SBM 基本靠谱。
但在今天: - MTS 是默认配置,SBM 语义已变 - 主库 QPS 上万,大事务频繁,relay log 堆积成为常态 - 云原生架构下主从跨可用区甚至跨地域,网络延迟波动更大 - 大部分团队连主从时钟同步都没检查过
SBM 没有变——是数据库架构变了,它被时代抛下了。
MySQL 官方也一直在提示 SBM 的局限性。MySQL 8.0 引入 performance_schema.replication_applier_status 等更精确的表,但默认的 SBM 字段无法轻易替换。它仍然是 SHOW SLAVE STATUS 中最显眼的字段,仍然是大多数监控工具的默认告警指标。
正确的延迟检测方案
放弃 SBM 作为唯一指标。建立双指标体系。
指标一:基于 GTID 的精确延迟检测
MySQL 5.6+ 引入 GTID(全局事务标识符),格式为 server_uuid:transaction_id。每个事务分配一个全局唯一 ID。通过比较主库和从库的 gtid_executed 集合,可以直接算出从库还缺哪些事务——不依赖时钟,不依赖 event 执行速度,纯粹的事务级精确测量。
-- Master 端
SELECT @@GLOBAL.gtid_executed;
-- +-------------------------------------------+
-- | 3E11FA47-1FCA-11EC-B2A8-0242AC130002:1-100 |
-- +-------------------------------------------+
-- Slave 端
SELECT @@GLOBAL.gtid_executed;
-- +-------------------------------------------+
-- | 3E11FA47-1FCA-11EC-B2A8-0242AC130002:1-95 |
-- +-------------------------------------------+
从库缺少事务 96-100,共 5 个事务。这就是真实延迟——不受时钟偏差影响,不受 MTS 排序影响,不受 SBM 刷新时机影响。
gtid_executed 会包含多个 server_uuid:
3E11FA47-1FCA-11EC-B2A8-0242AC130002:1-5000,
A7B23C11-2DCA-12EC-C3B9-0342AC130005:1-200
GTID_SUBTRACT 会同时处理所有 uuid 的交集和差集

补充指标:gtid_purged 的边界
gtid_purged 记录了已从 binlog 中清理的事务 GTID。在大多数场景下 GTID_SUBTRACT(master_gtid, slave_gtid) 已足够,这个函数内部已处理了 gtid_purged 的边界。但如果你的主库在从库连接到它之前已经 purge 了不少 binlog,gtid_purged 需要作为参考。
-- 检查主库 GTID 范围
SELECT GTID_SUBTRACT(@@GLOBAL.gtid_executed, @@GLOBAL.gtid_purged) AS active_gtid_set;
GTID 监控方案生产注意事项
几个在真实环境中会遇到的问题:
1. GTID_SUBTRACT 在超大规模 GTID 集合上的性能
如果你的 Master 已经运行了好几年,gtid_executed 可能包含几十个 server_uuid,每个 uuid 的事务编号范围可能达到亿级。GTID_SUBTRACT 函数需要在内存中解析这些集合,当两个集合差距很大时,计算可能耗时几百毫秒甚至几秒。
经验值:在包含 15 个 server_uuid、事务编号到 5000 万的 GTID 集合上,GTID_SUBTRACT 的典型耗时是 50-200ms。这对监控采集中可以接受(每分钟执行一次),但不适合高频(每秒执行)的实时检测。
2. 非 GTID 环境的替代方案
如果 gtid_mode=OFF(大量现存 MySQL 5.5/5.6 集群不上 GTID),GTID 检测不可用。替代方案是文件+位点对比:
-- Master 端
SHOW MASTER STATUS;
-- +------------------+----------+--------------+------------------+
-- | File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
-- +------------------+----------+--------------+------------------+
-- | mysql-bin.000123 | 458762 | | |
-- +------------------+----------+--------------+------------------+
-- Slave 端:看 Relay_Master_Log_File + Exec_Master_Log_Pos
SHOW SLAVE STATUS\G
-- Relay_Master_Log_File: mysql-bin.000123
-- Exec_Master_Log_Pos: 458762
-- 如果两者一致 → 从库执行到了 Master 的最新位点
-- 但这仍然不精确(无法判断从库是否真的执行了所有事务)
文件+位点方式比 GTID 落后一代,但在非 GTID 集群上是唯一可用的方案。
3. 通过 Prometheus mysqld_exporter 获取 GTID 指标
mysqld_exporter 默认不暴露 GTID 指标。需要启用 --collect.global_status 并添加 PromQL:
# recording rule: 计算 GTID 缺口的事务数量
- record: mysql_gtid_gap_count
expr: mysql_global_status_gtid_executed{instance=~"slave.*"}
# 需要 exporter 端处理 GTID 集合的解析,或通过外部脚本暴露
实际部署中更常用的方式是通过 cron 脚本计算缺口,push 到 Prometheus Pushgateway。
指标二:Relay log 堆积量
Relay log 堆积是一个简单有效的延迟信号——如果 SQL 线程消费速度跟不上 IO 线程拉取速度,relay log 持续增长。
-- 从 performance_schema 直接获取
SELECT VARIABLE_VALUE AS relay_log_space_bytes
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Relay_log_space';
-- 从 SHOW SLAVE STATUS 间接获取
SHOW SLAVE STATUS\G
-- Relay_Log_Space: 524288000 ← 500MB
关键监控规则: Relay_Log_Space 持续增长(每 5 分钟上涨超过 100MB),即使 SBM=0,也从库存在复制延迟。
-- relay log 文件数量(多文件堆积也是信号)
SHOW VARIABLES LIKE 'relay_log_space_limit';
-- 默认 0 = 不限制
建议:在从库配置 relay_log_space_limit=4GB,同时监控当前使用率超过 80% 时告警。
但 relay log 堆积检测也有盲区:当 IO 线程拉取速度本身就很慢(比如网络带宽不足),relay log 也可能一直很小,但延迟已经很大了。所以 relay log 堆积必须配合 GTID 缺口一起使用。
指标三:pt-heartbeat(最推荐的生产方案)
Percona Toolkit 的 pt-heartbeat 是目前生产环境最靠谱的复制延迟检测方案。
原理:
- 在主库创建心跳表,定期写入带主库时间戳的记录
- 从库读取这条记录(通过复制同步过去),用从库当前时间减去主库时间戳
- 差值就是真实延迟
Master: "我 10:00:00.002 写了这条记录"
└── binlog ──→ relay log ──→ SQL thread ──→ Slave table
Slave: "读到记录是 10:00:03.150,差值 3.148 秒"
为什么不依赖时钟同步: 从库读到的 tsv 是主库写入时打的时间戳。计算在从库上完成:now() - tsv。虽然用了从库的 now(),但 tsv 是主库的时间——它们是同一个参考系下的两个时间点,不跨机器比较。
# 主库启动心跳写入(每秒一次,后台运行)
pt-heartbeat --update --daemonize --interval=1 \
-h master-host -u monitor -p '<password>' -D percona
# 从库检测延迟
pt-heartbeat --check -h slave-host -u monitor -p '<password>' -D percona
# 0.02s
# 大事务批量写入后
pt-heartbeat --check -h slave-host -u monitor -p '<password>' -D percona
# 12.47s
# 同时段的 SBM
mysql -h slave-host -e "SHOW SLAVE STATUS\G" | grep Seconds_Behind_Master
# Seconds_Behind_Master: 3
pt-heartbeat 的心跳表结构:
CREATE TABLE percona.heartbeat (
id INT PRIMARY KEY,
master_id VARCHAR(64) NOT NULL,
file VARCHAR(255) DEFAULT NULL,
position BIGINT DEFAULT NULL,
relay_master_log_file VARCHAR(255) DEFAULT NULL,
exec_master_log_pos BIGINT DEFAULT NULL,
tsv DECIMAL(26,6) NOT NULL -- ← 微秒级精度的时间戳
);
核心字段 tsv 存储主库写入时的精确时间戳(微秒精度)。从库程序计算 now() - tsv。这个计算在从库上完成,tsv 来自主库,所以不受时钟偏差影响。
pt-heartbeat 的性能影响:
--interval=1(每秒写入 1 条),对主库的写入负载几乎可忽略不计。但如果你有 100 个从库,每个从库都通过 pt-heartbeat --check 读同一张表,读负载会集中在从库上。
经验值:100 个从库同时 --check,从库的 percona.heartbeat 表会产生约每秒 100 次查询。这个负载对现代 MySQL 实例来说微不足道(< 0.1% QPS)。
高级用法:--check 也支持 --master-host 参数,可以直接在主库上检查任意从库的延迟,适用于集中式监控架构。

三种方案的优缺点对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| GTID 缺口 | 精确到事务级;零额外写入负载 | 需要 GTID 模式;大集合上计算慢;不能反映执行时间 | 所有 GTID 集群的基础检测 |
| Relay log 堆积 | 简单;反映 IO 线程积压 | 不能反映执行延迟;盲区(IO 线程本身慢) | 配合 GTID 使用 |
| pt-heartbeat | 毫秒级精度;不依赖时钟;反映真实执行延迟 | 额外写入负载;需要安装 Percona Toolkit | 生产环境首选 |
最佳实践:GTID 缺口作为事务级基础检测 + pt-heartbeat 作为秒级实时延迟检测 + Relay log 堆积作为 IO 线程积压检测 = 零盲区。
半同步复制下的 SBM 表现
半同步复制(Semisynchronous Replication)在 MySQL 5.5+ 引入,5.7+ 通过插件形式支持。在半同步模式下,Master 提交事务后,必须等待至少一个 Slave 确认收到 binlog event 后才返回客户端成功。
这会影响 SBM 吗?直接影响不大,但间接影响值得注意。
半同步复制不影响 SBM 的计算逻辑——SBM 仍然由 SQL 线程在 event 执行后计算。但半同步复制保证 IO 线程的 relay log 积压不可能太大(因为 Master 会等 Slave 确认后才提交)。
这会创造一个新的陷阱:半同步复制 + SBM=0 会让你觉得"复制的延迟肯定很小",但半同步只保证 binlog 传输到了 Slave 的 relay log,不保证 SQL 线程已经执行了。如果 Slave 上恰好有一个大事务在执行,半同步模式下的 SBM=0 和异步模式下的一样不可信。
MySQL 5.7 新增的 rpl_semi_sync_master_wait_for_slave_count 参数可以配置 Master 等待多少个 Slave 确认。但这同样不解决 SQL 线程消费延迟的监控问题。
大事务场景的复制定性
大事务是复制延迟最常见的诱因,但也是 SBM 最容易被误解的场景。
一个常见的误解:DBA 看到 SBM 飙高,执行 SHOW PROCESSLIST 发现 SQL 线程在 Running,以为是复制延迟。但实际查看 performance_schema.replication_applier_status_by_worker,发现 worker 正在执行一个来自 3 分钟前的事务——SBM 的 3 分钟不是"落后",是"这个事务正在执行"。
区分标准: - 如果 GTID 缺口包含多个事务编号 → 确实有积压 - 如果 GTID 缺口只有 1 个编号且 SBM 很高 → 大事务执行中,不是积压 - 如果 Relay_Log_Space 持续增长 → IO 线程积压 - 如果 Relay_Log_Space 稳定且 GTID 缺口为空 → 无延迟
大事务在 binlog_format=ROW 模式下对 relay log 的影响也值得注意:ROW 模式下,一个大的 UPDATE/DELETE 可能产生几 GB 的 binlog event。传输这个 event 到 Slave 的过程本身就需要时间。如果网络带宽是 1Gbps,传输 2GB 的 event 至少需要 16 秒。这段时间内 IO 线程持续活跃,但 SQL 线程可能已经空闲——SBM 显示 0。
告警策略如何设置
解决了用什么指标的问题,还需要回答阈值设多少。
| 场景 | 指标 | 阈值 | 响应 |
|---|---|---|---|
| 正常复制 | pt-heartbeat < 1s | 信息级 | 无操作 |
| 轻度延迟 | GTID 缺口 1-5 个事务 | 警告级 | 检查大事务 |
| 中度延迟 | pt-heartbeat 1-10s 或 relay log 持续增长 | 警告级 | DBA 介入 |
| 严重延迟 | pt-heartbeat > 30s 或 relay log 超过 2GB | 告警级 | 立即处理 |
| 灾难 | GTID 缺口 > 1000 或 relay log 超 limit | P0 告警 | 停读从库 |
不要设置单一的 SBM 阈值。SBM 在 0 和 300 之间动态波动太普遍了,用它做告警会导致大量误报和漏报。
推荐的三指标汇合告警规则:
groups:
- name: mysql_replication_alerts
rules:
- alert: MySQLReplicationGap
expr: mysql_gtid_gap{instance=~".+"} > 50
for: 1m
labels:
severity: warning
annotations:
summary: "从库 GTID 缺口已超过 50 个事务"
- alert: MySQLRelayLogGrowing
expr: rate(mysql_global_status_relay_log_space_bytes[5m]) > 1000000
for: 2m
labels:
severity: warning
annotations:
summary: "relay log 持续增长,速率超过 1MB/s"
- alert: MySQLReplicationStopped
expr: mysql_slave_status_slave_io_running != 1
or mysql_slave_status_slave_sql_running != 1
for: 0m
labels:
severity: critical
annotations:
summary: "从库复制线程已停止"
GTID 缺口告警 + relay log 增长告警 + 复制线程状态告警,三者配合才能覆盖复制延迟的全场景。
进阶:基于 Machine Learning 的异常检测
如果你的集群规模较大(50+ replicas),可以考虑基于历史数据的基线告警。GTID 缺口数在不同时段波动很大——凌晨低峰期可能一直是 0,白天高峰期 10 个缺口算正常,50 个才算异常。
使用 Prometheus 的 predict_linear 函数可以预测 relay log 的增长趋势:
- alert: MySQLRelayLogWillFill
expr: predict_linear(mysql_global_status_relay_log_space_bytes[1h], 3600) > 4000000000
for: 10m
labels:
severity: warning
annotations:
summary: "relay log 预计 1 小时内将超过 4GB"
这个告警在 relay log 快速增长时可以提前预警,而不是等它满了再通知。
从 SBM 迁移到新方案的步骤
如果你的监控系统目前基于 SBM,替换需要分 3 步走:
第一阶段:双写兼容(第 1 周)
保持现有 SBM 告警不变,部署新指标。在 Grafana 中并排放 SBM 仪表盘和新指标仪表盘。收集 SBM 和新指标之间的差异数据。
* * * * * /usr/local/bin/check-gtid-gap.sh 2>&1 | logger -t gtid-gap
第二阶段:切换告警源(第 2 周)
将告警规则从 SBM 切到新指标。SBM 降级为 info 级观察。
- alert: MySQLSBMDeviation
expr: mysql_slave_status_seconds_behind_master > 30
labels:
severity: info # 从 warning 降级
第三阶段:移除 SBM(第 3 周)
确认新指标稳定一周后,从所有监控仪表盘和告警规则中移除 SBM。
如何改造你的监控系统
# grep 你的监控代码——找到所有用 Seconds_Behind_Master 的地方
grep -rn "Seconds_Behind_Master\|sbm\|seconds_behind_master" \
--include="*.py" --include="*.sh" --include="*.yaml" \
/path/to/monitoring/
# 预期发现在:
# - prometheus mysqld_exporter 的告警规则
# - 自研监控脚本(shell / python)
# - 自动化巡检平台的告警配置
# - Grafana 面板的查询语句
替代策略速查表:
| 原指标 | 替代方案 | 数据类型 | 精确度 |
|---|---|---|---|
| SBM < 30 → 正常 | GTID_SUBTRACT 为空 | 整数 | 精确到事务级别 |
| SBM > 300 → 告警 | pt-heartbeat > 5s | 浮点数 | 毫秒级 |
| SBM 单告警 | GTID 缺口 + relay log 体积 + 复制状态 | 三指标汇合 | 无盲区 |
| SBM 仪表盘 | pt-heartbeat 趋势 + relay log 增长率 | 时序图 | 秒级刷新 |
判断是否已替换完毕:如果报警群里还有 SBM 相关的告警,说明你还没改完。如果它消失了,你成功了。
下篇我们聊 GTID 模式下主从切换失败——GTID 集合不一致导致切换后数据损坏的完整排查过程。
附:完整命令清单
# 1. 查看 SBM 和相关字段
SHOW SLAVE STATUS\G
# 2. 查看 GTID 集合(主从分别执行)
SELECT @@GLOBAL.gtid_executed;
SELECT @@GLOBAL.gtid_purged;
SELECT GTID_SUBTRACT(@@GLOBAL.gtid_executed, @@GLOBAL.gtid_purged) AS active_gtid;
# 3. 检查 relay log 堆积趋势
mysql -e "SHOW SLAVE STATUS\G" | grep -E "Relay_Log_Space|Seconds_Behind_Master"
watch -n 30 'mysql -e "SHOW SLAVE STATUS\G" | grep -E "Relay_Log_Space|Seconds_Behind_Master|Executed_Gtid"'
# 4. 查看从库复制线程状态
SELECT * FROM performance_schema.replication_applier_status\G
SELECT WORKER_ID, LAST_APPLIED_TRANSACTION_END_TIMESTAMP,
LAST_APPLIED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP
FROM performance_schema.replication_applier_status_by_worker\G
# 5. GTID 缺口计算
mysql -e "SELECT GTID_SUBTRACT('MASTER_GTID', 'SLAVE_GTID')\\G"
# 6. pt-heartbeat 检测
pt-heartbeat --check -h slave-host -u monitor -D percona
# 7. 检查主从时钟偏差
mysql -h master -e "SELECT NOW()" && mysql -h slave -e "SELECT NOW()"
# 8. relay_log_space_limit 参数
SHOW VARIABLES LIKE 'relay_log_space%';
# 9. 检查大事务(binlog 中事务大小)
mysqlbinlog --base64-output=DECODE-ROWS mysql-bin.000123 |
grep -E "^# at |GTID.*last_committed|^COMMIT" | tail -30
# 10. 半同步复制状态
SHOW STATUS LIKE 'Rpl_semi_sync%';
🔗 个人博客: https://opencao.cn 📺 公众号: Ai拆代码的曹操 🌟 知识星球: Ai拆代码的曹操