MySQL主从复制延迟监控:四个指标与判据

一个站跑到第三年,数据库迟早会从「一台机器」变成「两台机器」。原因通常不是访问量暴涨,而是两种更现实的场景:一是需要做时间点恢复与热备,二是要把备份、统计报表、导出这类重查询从主库上挪走。

主从复制配起来很快,三十行配置就能跑通。麻烦的是跑通之后你怎么知道它还是好的。多数站点的主从第一次断掉是在半年后,断的原因有两种:从库因为某个 SQL 失败而停止复制,或者从库仍然在「复制」,但比主库落后了六个小时。前者会有进程状态,后者只有一个数字,而这个数字恰恰最容易被忽略。

这篇讲的是自建站场景下的从库健康监控:复制延迟到底该怎么算、四个必须盯的指标、一个不需要装任何组件的监控脚本,以及延迟变大后如何判断是网络、是大事务还是单线程回放。以 MySQL 8.0 为例,涉及 5.7 的地方会单独标注。

一、先搞清楚「延迟」有两个完全不同的含义

很多人看到 SHOW SLAVE STATUS 里的 Seconds_Behind_Master 是 0,就认为从库是实时的。这个判断在两种情况下会直接骗人。

第一种情况是复制线程已经停了。当从库的 SQL 线程因为主键冲突、表不存在、磁盘满而停止时,Seconds_Behind_Master 会变成 NULL。如果你的监控脚本只判断「这个值是否大于某个阈值」,那么 NULL 在任何比较里都是不成立的,于是「复制完全停止」这个最严重的情况反而静默通过。这是主从监控里最经典的 bug,我在不止一个站点见过。

第二种情况是它只衡量回放进度,不衡量取日志进度。Seconds_Behind_Master 的计算方式是「从库当前执行到的事件时间戳」与「从库当前系统时间」之差。如果从库已经很久没有从主库拉到新的 binlog 事件(比如网络中断),那么「当前执行的事件」还停在很久以前,此时这个数字确实会变大——但如果主库根本没有新写入,比如凌晨三点,它就会显示 0,而实际上从库已经和主库失联了。

所以一个可靠的判断需要两类指标同时看:基于时间的延迟(Seconds_Behind_Master)和基于位置的延迟(主库当前 binlog 位置与从库已读位置的差)。后者才是「连接是否健康」的直接证据。

二、四个必须盯的指标

把从库健康拆成四个可量化的数字,每一个都有明确的失败形态。

指标一:两个复制线程是否都在运行。Slave_IO_Running 和 Slave_SQL_Running 必须都是 Yes。IO 线程负责从主库拉 binlog 写到本地 relay log,SQL 线程负责回放。任何一个不是 Yes,从库就不再前进了。注意 MySQL 8.0.22 之后这两个字段改名了,新名字是 Replica_IO_Running 和 Replica_SQL_Running,老名字仍然作为别名保留。如果你的脚本用 grep 抓字段,要同时兼容两种。

指标二:时间延迟。Seconds_Behind_Master,处理方式是「NULL 视为严重」、「大于 60 秒告警」、「大于 300 秒视为不可用于读」。特别注意 NULL 的处理,这是前面提到的陷阱。

指标三:位置差。用 SHOW MASTER STATUS 拿到主库当前 binlog 文件与偏移量,与从库的 Relay_Master_Log_File 和 Exec_Master_Log_Pos 对比。两者相等说明从库已经追平;差得越来越多说明回放跟不上;主库位置一直不变而从库也不变,说明没有新写入,此时应该看指标四而不是指标二。

指标四:最后一次成功同步的时间。这个指标需要你自己维护:在监控脚本里记录「上次位置发生变化的时间」,如果超过 N 分钟位置完全没动,同时主库的位置却在变,那就是失联。这个指标能抓住「IO 线程活着但拉不到数据」这种状态。

下面是一条把前三个指标一次取出的查询。相比 SHOW SLAVE STATUS 的一百多列,用 performance_schema 更便于脚本解析。

SELECT
  SERVICE_STATE,
  SOURCE_HOST,
  LAST_ERROR_NUMBER,
  LAST_ERROR_MESSAGE,
  REPLICA_IO_STATE,
  REPLICA_SQL_STATE
FROM performance_schema.replication_connection_status AS c
JOIN performance_schema.replication_applier_status AS a;

实际使用中,SHOW REPLICA STATUS 在脚本里更好解析,因为它返回的是「字段名 + 值」的两列结构,可以直接塞进关联数组。MySQL 8.0.22 之后推荐用 SHOW REPLICA STATUS,5.7 用 SHOW SLAVE STATUS。

三、一个不装任何组件的监控脚本

个人站不一定有 Prometheus,也不值得为了一台从库去搭一整套监控。下面这个脚本只用 MySQL 客户端与标准 shell,跑在从库上,把结果写成一行 CSV 追加到文件,再由 cron 每天汇总或者直接推送到 IM。

#!/bin/sh
# /root/bin/replica_check.sh —— 从库健康检查,输出一行 CSV
MYSQL="mysql -uroot -pPASSWORD -N -B"
OUT=/var/log/replica_health.csv
NOW=$(date +%s)

ROW=$($MYSQL -e "SHOW REPLICA STATUS\G" | awk -F': ' '
  /Replica_IO_Running/  {io=$2}
  /Replica_SQL_Running/ {sql=$2}
  /Seconds_Behind_Source/ {lag=$2}
  /Last_Error_Number/   {err=$2}
  END {print io, sql, lag, err}')

IO=$(echo "$ROW" | cut -f1)
SQL=$(echo "$ROW" | cut -f2)
LAG=$(echo "$ROW" | cut -f3)
ERR=$(echo "$ROW" | cut -f4)

# NULL 或空视为异常,不是「0 秒延迟」
case "$LAG" in
  ""|NULL) LAG=-1 ;;
esac

echo "$NOW,$IO,$SQL,$LAG,$ERR" >> "$OUT"

# 三重判定:线程停了 / 有错误 / 延迟超阈值
if [ "$IO" != "Yes" ] || [ "$SQL" != "Yes" ] || [ "$ERR" != "0" ] || [ "$LAG" -gt 60 ] || [ "$LAG" -lt 0 ]; then
  echo "REPLICA_ALERT io=$IO sql=$SQL lag=$LAG err=$ERR" >> /var/log/replica_alert.log
fi

脚本里三个设计点值得说明。

第一,LAG=-1 统一表示「不可用」。这样后面所有判断都可以用数值比较,不必处理空值。同时把 -1 单独作为异常,因为它意味着复制线程停了,而不是延迟为零。

第二,把原始数据落盘,而不是只输出告警。有了 CSV 才能回答「上周三凌晨延迟是多久」这种问题,也才能在延迟频繁抖动时看出规律。这个文件本身很小,一年不到 1MB。

第三,告警写独立日志而不是直接发消息。发送动作交给另一个脚本,避免「发送失败导致检查脚本也失败」。这一点在告警链路里很重要:监控脚本的职责是判断,不是投递。

用 cron 每分钟跑一次:

# crontab -e
* * * * * /root/bin/replica_check.sh >/dev/null 2>&1

每分钟的频率看起来很高,但这个脚本的开销只有一次数据库连接,对一台从库来说完全可以忽略。真正的成本在磁盘上:每分钟一行,一天 1440 行,一年 52 万行,配合 logrotate 或按月切分文件就没问题。

四、延迟变大之后:三种成因与判据

看到延迟超过阈值,下一步不是重启复制,而是判断原因。重启复制线程不会让落后的数据消失,它只是把「回放」这个过程重新开始。如果落后的是六个小时的数据,那六个小时的 binlog 还是要一条一条执行。而在延迟已经很大的时候重启,只会让追赶过程重新从磁盘读一遍 relay log。

成因一:单线程回放跟不上并发写入。这是最典型的场景。主库上有多个连接同时写,而从库默认只有一个 SQL 线程按顺序回放。判据是主库的写入 QPS 明显高于从库的回放速率,而且延迟呈现「线性增长后突然归零」的锯齿形。解决方式是开并行复制:MySQL 5.7 以后可以按组提交(LOGICAL_CLOCK)并行,需要主库的 binlog_group_commit_sync_delay 配合,让同一批事务落在同一个组里。

# 从库上开启并行回放(5.7 与 8.0 通用)
SET GLOBAL replica_parallel_workers = 4;
SET GLOBAL replica_parallel_type = 'LOGICAL_CLOCK';
SET GLOBAL replica_preserve_commit_order = ON;
# 5.7 用 slave_ 前缀:slave_parallel_workers/slave_parallel_type

replica_preserve_commit_order 必须为 ON,否则并行回放会打乱事务的提交顺序,在有外键或唯一约束的场景下会直接报错。parallel_workers 的数量不要超过 CPU 核数,一般 4 到 8 之间,再多收益很小。

成因二:单个大事务。一条 DELETE FROM t WHERE create_time < '2023-01-01' 删掉三百万行,会在 binlog 里生成一个巨大的事务。从库必须把这一个事务完整执行完才能前进,期间延迟会从 0 直接跳到几十分钟甚至几小时。判据是延迟呈「阶梯状」:长时间为 0,然后突然一次跳到很大,再花很久慢慢回落。

这类延迟无法通过并行复制解决,因为并行复制的粒度是事务,单个事务本身无法拆分。唯一的办法是从源头避免大事务:把批量删除改成按主键分批,每批几千行,批与批之间 sleep 一下;把大表的 DDL 用 gh-ost 或 pt-online-schema-change 做。

成因三:从库自身慢。从库的硬件比主库差、或者从库上跑了重查询(报表、导出),都会导致回放变慢。判据是从库的 SHOW PROCESSLIST 里能看到长时间运行的 SELECT,或者 iostat 显示磁盘 util 接近 100%。这种情况下加并行线程没用,需要把重查询挪走。

把三种成因压成一条判断链:先看延迟曲线的形状(锯齿 = 并发不够,阶梯 = 大事务,持续高位 = 从库慢),再看主库最近有没有执行过批量操作,最后看从库自身的负载。这个顺序能覆盖九成的情况。

五、复制断了之后:先读错误号,再决定动作

SQL 线程停止时,Last_Error_Number 会给出具体原因。不同错误号的处理方式完全不同,盲目 SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1 跳过错误是最危险的做法——它会让主从数据永久不一致,而且没有任何告警告诉你已经不一致了。

错误号 1062(主键冲突)。说明从库上已经存在要插入的行。常见原因是有人直接在从库上写了数据,或者上一次复制中断后手工补过数据。处理方式取决于冲突的那一行是不是「应该存在」的:如果是从库被误写,删掉那一行再让复制继续;如果确实无法判断,就把这一行导出留底,再跳过。

错误号 1032(找不到要更新的行)。说明从库缺了主库有的行,通常是之前跳过过错误导致的。这种情况基本无法自动修复,正确做法是重新做一次全量同步。

错误号 1236(无法读取 binlog)。主库上的 binlog 已经被清理(expire_logs_days),从库要的位置不存在了。这时唯一的办法是全量重做,或者用最近的一次备份加 binlog 做时间点恢复。这一条也是「expire_logs_days 必须大于最长故障恢复时间」的原因。

重做全量同步是重操作,一定要在主库低峰期做,并且用 --single-transaction 与 --source-data(8.0.26 之后)或 --master-data 保证备份点与 binlog 位置一致:

mysqldump -uroot -p --single-transaction --source-data=2 \
  --databases zz1984 \
  | gzip > /backup/zz1984_full_$(date +%Y%m%d_%H%M).sql.gz

导出的文件头部会有一行注释记录 binlog 文件名与位置,导入从库之后用它配置复制起点。不要用 --flush-logs 之外的任何方式「手工推断」起点,猜错了就会静默重复执行一段事务。

六、延迟对业务的实际影响:读请求怎么分流

监控做得再好,也要回答一个业务问题:延迟到多少就不能读从库了?

这取决于你的读请求能不能容忍旧数据。文章列表、历史归档这类读,容忍几分钟延迟完全没问题;用户的个人中心、后台的操作列表这类读,如果读到旧数据会给用户造成「我刚才改的东西怎么没了」的困惑,容忍度接近零。

实用的做法是按延迟分档切换读源:延迟小于 10 秒时全部读从库;10 到 60 秒时只把「容忍旧数据」的查询路由到从库;超过 60 秒直接全部读主库,并接受主库压力上升。这个切换逻辑不必写在应用里,可以由一个配置项控制,监控脚本在越线时修改它。

还有一类查询永远不应该走从库:刚刚写完立刻要读的场景。写主库、读从库的架构里,写完立刻读会读不到,因为复制需要时间。解决办法是写完之后的一小段时间内强制读主库,或者干脆把这类「写后读」的接口全部指向主库。

七、把检查接进例行:三个数字与一条命令

监控的价值不在于数据有多全,而在于每天有人真的会看一眼。我把从库健康压成三个数字,写在每日报告的第一行。

  • 今天的最大延迟(秒)。正常应该在个位数,如果某天出现几百秒,就去看那天主库执行了什么。
  • 今天复制中断的次数。正常的站点应该是 0,出现 1 次就要查错误号。
  • 从库与主库的位置差(字节)。这个数字能补上「延迟为 0 但实际失联」的盲区。

这三个数字都可以从前面那个 CSV 里算出来:

awk -F, -v d=$(date +%Y-%m-%d) '
  substr(strftime("%Y-%m-%d", $1),1,10) == d {
    if ($4 > max) max = $4
    if ($2 != "Yes" || $3 != "Yes") bad++
  }
  END {print "max_lag=" max " break=" bad+0}' /var/log/replica_health.csv

一条命令给出两个数字,接在每日报告里就够了。第三个数(位置差)需要在脚本里额外记录主库的 SHOW MASTER STATUS,成本是一次远程连接,值得加上。

八、小结

主从复制的健康监控,核心是不要相信单一指标。Seconds_Behind_Master 既会在复制停止时变成 NULL 而让阈值判断失效,也会在没有新写入时显示 0 而掩盖失联。把线程状态、时间延迟、位置差、最后同步时间四个数字一起看,才能覆盖完整的失败形态。延迟变大之后先看曲线形状判断成因(锯齿是大并发、阶梯是大事务、持续高位是从库自己慢),而不是急着重启线程;复制真的断了先读错误号,永远不要习惯性地跳过错误。最后,把「今天的最大延迟」和「今天中断次数」这两个数字放进每天必看的那一屏里——监控的意义不在于采集,而在于有人会因为那个数字不对而动手。

Last modification:November 9th, 2026 at 12:31 pm

Leave a Comment