跳到主内容

主从延迟从分钟级到毫秒级:并行复制、半同步与读写分离路由的全套治理 ​

用户刚支付完成,跳转到订单详情页却提示「订单不存在」;运营改完商品价格,前端刷新半天还是旧价。这些工单背后的故障源高度一致:主从延迟。更麻烦的是,延迟往往「平时没事、大促爆雷」,因为它与写入吞吐强相关。治理主从延迟不能靠一句「把读切回主库」,而要按顺序回答三个问题:从库为什么追不上、怎么让它追得快、追不上时读请求怎么路由。这篇文章按这三层给出完整方案。

延迟从哪里来:三个被低估的来源 ​

主从复制分两段:IO 线程把主库 binlog 拉到从库中继日志,SQL 线程(或并行 worker)再回放。IO 段通常是顺带的网络传输,真正的瓶颈几乎都在回放段。三个最常见来源:一是单线程回放,传统架构从库只有一个 SQL 线程逐条执行,主库 64 线程写入、从库单线程消化,写入高峰必然积压;二是大事务,一条删五十万行的 DELETE 在主库跑两分钟,从库回放同样要两分钟起步,期间后续事务全部排队,延迟瞬间抬升几分钟;三是无主键表,ROW 格式 binlog 的每个行变更都要在从库定位目标行,binlog_row_image=FULL 时还要比对全部列,无主键表的回放等于每次变更都做一次全表扫描。

第一步:WRITESET 并行复制,把回放并行度打开 ​

8.0 主推的解法是基于写集(WRITESET)的并行复制:主库为每个事务计算修改行的哈希集合,不冲突的事务在从库被分到不同 worker 并行回放:

sql
-- 主库:开启基于写集的依赖计算,让不冲突的事务在从库并行回放
SET GLOBAL binlog_transaction_dependency_tracking = 'WRITESET';
-- 8.0.26 起默认即 XXHASH64,显式声明可确保老小版本也生效
SET GLOBAL transaction_write_set_extraction = 'XXHASH64';

-- 从库:停复制后调整并行参数,再重启复制通道
STOP REPLICA;
SET GLOBAL replica_parallel_type = 'LOGICAL_CLOCK';   -- 按逻辑时钟分组并行
SET GLOBAL replica_parallel_workers = 8;              -- worker 数按核数与冲突率调整
SET GLOBAL replica_preserve_commit_order = ON;        -- 保持提交顺序,保证从库读一致性
START REPLICA;

三个细节决定效果:replica_preserve_commit_order 必须打开,否则从库提交顺序与主库不一致,读到的事务可见性与主库不同;并行度上限受事务冲突率制约,热点单行更新的业务开再多 worker 也没用;8.0.26 之前变量名还是 slave_parallel_workers 系列,配置前先核对自己版本的手册。另外,binlog_transaction_dependency_tracking 在较新的小版本已进入废弃流程,默认依赖计算已优化,升级后要复查这些参数是否仍按预期生效。

参数调整只是第一步,真正决定并行收益的是事务形态。冲突集中在少数热点行的业务,worker 之间会互相等待,并行度天然上不去;而批量导入类负载只要拆好批次,worker 几乎能吃满核数。上线后用 SHOW REPLICA STATUS 观察每秒回放事务数的变化,再决定是否继续调 worker 数,比一次性把参数拍满更稳。

第二步:半同步复制,守住「不丢数据」的底线 ​

并行复制解决「快」,半同步解决「不丢」:主库提交时等待至少一个从库确认收到 binlog 才向客户端返回成功。8.0 推荐用 AFTER_SYNC 等待点(无损半同步):

sql
-- 主库安装并启用半同步插件(8.0.26+ 插件更名为 semisync_source,旧名仍兼容)
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = ON;
-- 等待点选 AFTER_SYNC:主库写入 binlog 后、提交前等待从库 ACK,故障切换不丢已确认事务
SET GLOBAL rpl_semi_sync_master_wait_point = 'AFTER_SYNC';
-- 从库 ACK 超时 1 秒:超时自动降级为异步,保可用性不堵提交
SET GLOBAL rpl_semi_sync_master_timeout = 1000;

要想清楚两点:半同步给每次提交增加一个到从库的往返耗时,跨机房部署时 RTT 会直接体现在写入延迟上;超时降级机制意味着半同步不是硬保证——从库全挂超一秒就静默退回异步,必须配告警,否则等于裸奔。

第三步:读写分离路由,分清「能等」与「不能等」 ​

前两步把延迟压下来之后,路由层要回答最后的问题:延迟不可能为零,哪些读能容忍旧数据、哪些不能。实用策略分三档:支付结果、账户余额这类强一致读直接路由主库,量小不会压垮;订单列表、商品详情这类准实时读走从库;对「写后立即读」的场景,用 GTID 等待把一致性责任精确到单次请求:

sql
-- 写事务提交后拿到该事务的 GTID(连接池或中间件可自动记录),再到从库等待回放完成
-- 示例:最多等待 0.5 秒,超时则把这次读降级回主库
SELECT WAIT_FOR_EXECUTED_GTID_SET('3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100', 0.5);

ProxySQL、MySQL Router 等中间件已内置基于 GTID 的粘滞路由,优先用现成能力;自研网关则按「记录写事务 GTID → 从库 WAIT_FOR_EXECUTED_GTID_SET → 超时回主库」三步实现。

监控:别只盯 Seconds_Behind_Master ​

sql
-- 每 worker 的回放延迟与事务队列,比 Seconds_Behind_Master 更能定位单 worker 拖尾
SELECT * FROM performance_schema.replication_applier_status_by_worker\G

-- 从库回放通道整体状态:是否有 worker 停滞、限流或等待
SELECT * FROM performance_schema.replication_applier_status\G

Seconds_Behind_Master 有两个著名盲区:IO 线程断开时显示为 0,大事务回放期间也长期显示 0,拿它做唯一告警指标会漏掉最危险的场景。更稳的做法是双保险:performance_schema 的 per-worker 视图加业务水位探测(业务表写入带时间戳,从库定时读取计算差值),两条曲线交叉验证。

避坑总结 ​

  1. 大事务是第一杀手,治理优先级高于一切并行参数。 批量写入与清理任务拆成 500 到 1000 行一批的小事务,批间留出间隔,主库锁持有、binlog 传输、从库回放三处同时受益。
  2. 无主键表要优先补主键。 ROW 格式加 binlog_row_image=FULL 的组合下,无主键表的回放是全表扫描级灾难,任何并行参数都救不了;历史大表改造前先用只读压测评估影响。
  3. 半同步超时降级必须配告警。 Rpl_semi_sync_master_status 变为 OFF 即表示已退回异步,不告警等于白装;同时评估 RTT 对写入吞吐的影响,跨机房场景慎重开启。
  4. worker 数不是越大越好。 冲突率高的事务流会互相等待,8 到 16 是常见区间,用压测确定终值;同时确认 replica_preserve_commit_order=ON,否则从库读一致性无保障。
  5. 路由规则要与业务逐条确认。 「能等」与「不能等」是业务判断不是技术判断,逐接口确认后写进路由配置并留出降级开关,别用一刀切的读权重。

写在最后 ​

主从延迟治理是一条完整的链路:并行复制提升回放吞吐,半同步守住数据底线,路由策略消化最后的毫秒级残差,监控体系负责在一切失效前报警。四层各司其职,延迟才能真正从分钟级稳定在毫秒级,写后读不一致的工单才会从日常变成罕见。

免责声明

本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。