2744 字
约 9 分钟
15
S11-X17 · 主从复制与延迟处理
2024-12-31
2026-11-08

监控面板上 Seconds_Behind_Source 从 0 跳到 3000,报警响了,业务方说"从库读到的数据是十分钟前的"。主从延迟不是一个数字问题,而是一条链路上四个环节的相对速度问题:主库写 binlog、从库 IO 线程拉取、SQL 线程重放、以及主从硬件与负载的差异。这篇按链路拆解,顺带把"为什么这个指标会撒谎"讲清楚。以下按 MySQL 8.0 核对(8.0.22 起 MASTER/SLAVE 术语改为 SOURCE/REPLICA)。


1. 三步链路与每一段的延迟来源

SHOW REPLICA STATUS\G
-- Replica_IO_State: Waiting for source to send event
-- Source_Log_File / Read_Source_Log_Pos       → IO 线程读到哪
-- Relay_Log_File / Relay_Log_Pos              → relay log 落盘到哪
-- Relay_Source_Log_File / Exec_Source_Log_Pos → SQL 线程重放到哪
-- Seconds_Behind_Source, Retrieved_Gtid_Set, Executed_Gtid_Set
阶段 动作 可能的延迟来源
主库 事务提交写 binlog(受 sync_binlog 约束) 大事务一次写几百 MB;binlog 落盘慢
传输 从库 IO 线程连主库拉 binlog 网络带宽/抖动;主库 dump 线程被大事务占住
落盘 写入 relay log 并更新 master.info 从库磁盘慢;relay_log 所在盘 IO 饱和
重放 SQL 线程(或 worker 组)执行事件 单线程重放 vs 主库并发;从库硬件弱;从库上跑了重查询

判断卡在哪一段,看三个位点是否同步推进:Read_Source_Log_Pos 停住 → 传输段;Relay_Log_Pos 追上了但 Exec_Source_Log_Pos 不动 → 重放段。这个三分法比盯着 Seconds_Behind_Source 有用得多。


2. 异步、半同步与组复制

模式 主库何时返回成功 可用性边界 对延迟的影响
异步 本地提交即返回 主库宕机可能丢最后若干事务 无额外等待
半同步(AFTER_SYNC) 至少一个从库确认收到 binlog 后返回 超时后退化为异步 每次提交多一个网络往返
组复制(MGR) 多数派认证通过后提交 需要多数派存活,否则不可写 认证 + 传播,对网络质量敏感
-- 半同步相关(8.0 插件名带 source/replica)
SHOW VARIABLES LIKE 'rpl_semi_sync_source_enabled';
SHOW VARIABLES LIKE 'rpl_semi_sync_source_timeout';        -- 默认 10000 ms
SHOW VARIABLES LIKE 'rpl_semi_sync_source_wait_point';     -- AFTER_SYNC / AFTER_COMMIT
SHOW STATUS  LIKE 'Rpl_semi_sync_source_status';
SHOW STATUS  LIKE 'Rpl_semi_sync_source_no_tx';            -- 退化为异步的次数

Rpl_semi_sync_source_no_tx 持续增长,说明"半同步已经名存实亡"——超时后自动退回异步,监控却还在显示"半同步已开启"。这是可用性与一致性的交换点:要严格一致就得让它阻塞,要低延迟就得接受退化,中间没有第三种。

组复制把"多数派"写进了提交路径:少数派分区下的节点会拒绝写入,这保证了不脑裂,代价是任意时刻必须有多数派在线。三节点集群只能容忍一个节点故障,这是容量规划问题,不是调参问题。


3. 延迟怎么量:为什么 Seconds_Behind_Source 会撒谎

它的算法是:当前时间 − 正在重放的事件在 binlog 里的时间戳。三个失真场景:

场景 表现 原因
从库长时间空闲 显示 0,实际可能落后 IO 线程没拉到新事件时该值为 NULL 或沿用旧值
主库突发大事务 数字很小然后突然暴涨 事务内的事件时间戳相同,重放中途不体现进度
主库时钟与从库不一致 恒为 0 或恒为大数 该指标依赖两端时钟
级联复制 中间节点显示正常,末端落后 指标只反映"相对直接上游"

更可靠的度量有三种:

-- 1. GTID 差值:Executed 与 Retrieved 的集合差
SELECT @@GLOBAL.gtid_executed;

-- 2. 心跳表:主库每秒更新一行,从库读该行时间差
CREATE TABLE IF NOT EXISTS heartbeat (
  id TINYINT PRIMARY KEY,
  ts DATETIME(6) NOT NULL
) ENGINE=InnoDB;
-- 主库定时:REPLACE INTO heartbeat VALUES (1, NOW(6));
-- 从库读取:SELECT TIMESTAMPDIFF(MICROSECOND, ts, NOW(6))/1000 AS lag_ms FROM heartbeat;

-- 3. 写后读校验:写入带时间戳的探针行,从库读回比对

pt-heartbeat 就是第 2 种的成熟实现,它绕开了"事件时间戳"这一层的全部失真,代价是多一张表和每秒一次写入。


4. 四个常见成因与并行复制

成因 识别信号 处理
大事务(一次改几百万行) Exec_Source_Log_Pos 长时间不动,事务结束后瞬间追平 拆批:WHERE id BETWEEN ? AND ? 循环,每批几千行
从库单线程重放 vs 主库并发 主库 QPS 高、从库 CPU 只有一个核忙 开并行复制
从库硬件更弱 从库 IO await 高、Relay_Log_Pos 推进慢 从库配置不低于主库,至少 IO 能力对齐
从库上跑了重查询 突发延迟与报表任务时间吻合 报表走独立从库/离线库,加资源隔离

并行复制的原理是:只要两个事务在主库上不冲突,从库就可以并行重放。8.0 用 LOGICAL_CLOCK 记录事务的提交组,配合 WRITESET 让冲突检测更细:

SHOW VARIABLES LIKE 'replica_parallel_workers';        -- 8.0 默认 4
SHOW VARIABLES LIKE 'replica_parallel_type';           -- LOGICAL_CLOCK
SHOW VARIABLES LIKE 'replica_preserve_commit_order';   -- 需为 ON 才能保证提交顺序
SHOW VARIABLES LIKE 'binlog_transaction_dependency_tracking';  -- WRITESET

限制同样明确:同一个提交组内的事务不能并行(它们互相可见)、DDL 只能单线程、大事务整体是一个并行单位。所以"开了并行复制还是延迟",多半是写路径上有个几百 MB 的大事务,或者一堆 DDL 排队。


5. 读写分离下的一致性

策略 实现 代价 适用
强制走主 写后 N 秒内的读路由到主库 主库压力上升 支付、下单等强一致场景
延迟阈值切主 从库延迟 > 阈值时该库摘除 阈值内的不一致仍存在 通用列表页
会话一致性 记录本会话最后写入的 GTID,读前等待 需要中间件支持 用户"改完立刻看"
关键读走主 只把详情页/我的订单走主 需按接口标注 成本最低的折中
-- 会话级等待:确保本会话之前的写入已在从库执行
SET @gtid = '3e11fa47-71ca-11e1-9e33-c80aa9429562:1-5';
SELECT WAIT_FOR_EXECUTED_GTID_SET(@gtid, 1);   -- 超时 1 秒,返回 0 表示已追平

第三种是最精确的:它不猜延迟,而是问"我关心的那个事务到了没"。代价是应用要传递 GTID,且等待本身会拉长响应时间。


6. 监控告警与主从切换

告警阈值不该按"秒数"拍,而该按业务可接受的陈旧度:商品列表能忍 5 秒,余额不能忍 0.5 秒。同一个集群里不同接口的阈值本来就不同,用一个全局 lag > 10s 报警,结果只能是要么漏报要么天天响。

-- 健康检查四件套
SHOW REPLICA STATUS\G     -- IO/SQL 线程是否 Running,Last_Error 是否为空
SHOW SLAVE HOSTS;         -- 8.0 仍可用,看拓扑
SELECT @@GLOBAL.gtid_executed;  -- 与主库比对差集
SHOW STATUS LIKE 'Replica_running_%';

切换的注意点按顺序:

步骤 动作 漏做的后果
1 确认从库 Executed_Gtid_Set 已追平主库 切过去丢最后几个事务
2 主库 SET GLOBAL super_read_only=ON 双写,数据分叉
3 提升从库:SET GLOBAL read_only=OFF 新主仍只读,业务全挂
4 其余从库 CHANGE REPLICATION SOURCE TO 指向新主 复制链断
5 应用重连(改 VIP/DNS/连接串,重启连接池) 应用连着旧主,写入丢失

第 5 步是最常被忘的一步:数据库切完了,连接池里的旧连接还指向老主库,应用侧表现为"写入成功但查不到"。切换预案里必须包含"连接池刷新"这一条,并且要能被验证。


7. 排错:五种复制异常

现象 原因 处理
Seconds_Behind_Source 长期 0 但数据旧 指标失真(空闲/时钟/级联) 改用心跳表或 GTID 差集
延迟周期性打满后瞬间归零 大事务/大批量更新 拆批提交,单批几千行以内
Last_IO_Error 报认证失败 账号权限、密码、caching_sha2_password 插件 用 mysql_native_password 或配好 TLS
Last_SQL_Error 报主键冲突 双写或从库被直连写入 定位写入源,重建从库
切换后应用报"表不存在/数据旧" 应用未重连,或连到了错误的实例 刷新连接池,校验 @@server_id
并行复制开了仍单线程 大事务、DDL、或 WRITESET 未生效 查写路径,确认 binlog_transaction_dependency_tracking

8. 边界:什么时候复制方案本身不合适

场景 更合适的方案 理由
跨地域强一致读 一致性读 + 就近写,或分布式数据库 异步复制无法给跨地域强一致
读多写少的报表 列式仓库 / 只读分析副本 行存从库扛不住大聚合
秒级 RPO 容灾 半同步/组复制 + binlog 异地归档 异步复制必然有窗口
分库分表后的聚合查询 汇总表 / 宽表 / 搜索索引 复制解决不了跨库 join
单机写入瓶颈 分片(sharding) 加从库只扩读不扩写

复制解决的是"读扩展 + 备份 + 容灾"三件事,扩不了写。把它当写扩展方案用,最终一定会卡在主库的单点上。


9. 自测

  1. 复制链路的四个阶段分别是什么?看到 Relay_Log_Pos 追上但 Exec_Source_Log_Pos 不动,卡在哪一段?
  2. 半同步超时后退化为异步,监控上怎么发现这件事?
  3. Seconds_Behind_Source 在哪三种情况下会失真?更可靠的两种替代度量是什么?
  4. 并行复制为什么"同提交组内不能并行"?大事务为什么无法被拆开并行?
  5. 会话一致性用什么机制实现?它比"延迟阈值切主"精确在哪?
  6. 主从切换的五步里,哪一步最容易被漏掉?漏掉后应用侧表现是什么?
S11-X17 · 主从复制与延迟处理
https://www.clxhxhhr.top/posts/5936/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。