读写分离延迟排查全流程:根因定位+并行复制调优+路由策略,实战干货一篇搞定
大家好,我是数据库小学妹 👋
上个月大促那天,凌晨两点监控群突然弹了一条告警:“库存数据异常”。我连上系统看了一眼,客服后台查不到刚下的订单,仓库那边库存没扣减,前端页面还显示"有货"。就这么短短几分钟,超卖了三百多单。老板凌晨三点打电话问我怎么回事,我脑子里第一个念头是"binlog断了"。
后来查了一圈,不是断,是慢。读写分离延迟从平时的几十毫秒飙到了 3.2 秒。3秒听起来不多,但高并发场景下,3秒足够让用户看到完全过期的数据。用户下单时读到的是3秒前的库存,库存明明已经扣完了,读到的还是有货。这就是超卖的根因。
之前我们聊过主从复制的原理和搭建,也讲了主从延迟怎么监控。但延迟真正出了事、影响到业务的时候,到底怎么排查怎么治理?今天把这次排障的完整过程整理出来。有读写分离架构的朋友建议收藏,关键时刻翻出来能用。
一、延迟从哪来——四个源头叠加
读写分离的基本逻辑不难。主库负责写,从库负责读。主库的变更通过 binlog 传给从库,从库拿到 binlog 后重新执行一遍,这个重放过程就是延迟的诞生地。
第一个源头:网络传输。 binlog 从主库到从库要过网络,同城机房还好,跨地域的话单次延迟可能几十毫秒。大促期间 binlog 量暴增,传输队列一积压,延迟就往上爬。
第二个源头:从库回放瓶颈。 这是最常见的瓶颈点。主库可以并发写入,性能很高。但从库默认是单线程回放 binlog。主库一秒写入一万条,从库一秒只能回放一千条,差距越拉越大。我之前以为开了并行复制就万事大吉了,结果发现配的粒度不对,实际上还是单线程在跑。
第三个源头:大事务阻塞。 主库跑一个批量更新十万行的事务,binlog 会一次性发给从库。从库得花很长时间慢慢回放,回放期间其他小事务都得排队。
第四个源头:DDL 操作。 主库执行 ALTER TABLE 加索引,在主库上可能走 Online DDL 很快。但传到从库后,从库要重建整张表,期间所有读请求都会被阻塞。
这四个源叠加在一起,延迟就从毫秒级变成秒级,大促期间尤其明显。
二、排查过程:从报警到定位根因
接到报警后,我按"先看延迟有多大,再找延迟从哪来"的思路一步步查。
第一步:确认延迟量级
-- 在从库执行
SHOW SLAVE STATUS\G
重点看 Seconds_Behind_Master。那天查出来是 3.2 秒。
这里有个细节:Seconds_Behind_Master 是从库当前时间戳减去它正在重放的那条 binlog 事件的时间戳。如果 binlog 传输断了(网络问题或者主库 binlog 被清理了),这个值会显示 NULL。所以看到 NULL 不是延迟无限大,是同步链路断了,得先查网络。
第二步:定位阻塞源
SHOW PROCESSLIST;
从库的 SQL 线程一直在跑同一条 UPDATE。查了一下,是运营凌晨跑的批量更新活动,更新了二十万行商品数据。这个大事务还没回放完,后面所有的操作都在等它。
这里要注意一个容易搞错的点:SHOW PROCESSLIST 里看到 SQL 线程在跑,不代表它跑得快。要看 Relay_Log_Space 和 Exec_Master_Log_Pos 的差距,才能判断回放进度。
第三步:查并行复制配置
SHOW VARIABLES LIKE 'slave_parallel%';
结果让我有点意外。slave_parallel_type 配的是 DATABASE 级别。也就是说每个数据库只有一个回放线程。这个客户所有表都在同一个库里,所以实际上还是单线程回放。
MySQL 8.0 的并行复制有两种策略:
- DATABASE:按库并行,不同库的事务可以并发回放。单库场景下等同单线程。
- LOGICAL_CLOCK:按主库的并行组回放,同一组内无冲突的事务可以并发。这是真正的并行。
- WRITESET(8.0.20+):基于行哈希判断冲突,比 LOGICAL_CLOCK 更细粒度,但对无主键表无效。
改成 LOGICAL_CLOCK 后,从库可以并发回放同一库下不同表的事务,回放能力提升好几倍。
第四步:确认 binlog 格式
SHOW VARIABLES LIKE 'binlog_format';
确认是 ROW 格式,这个没问题。但 ROW 格式有个特点:每条变更都记录完整的行数据。批量更新二十万行,binlog 体积非常大,传输和回放都更慢。如果是 STATEMENT 格式,binlog 只记录 SQL 语句本身,体积小很多,但 ROW 更安全,能避免存储过程和函数导致的复制不一致。所以不能为了延迟改回 STATEMENT,得从其他地方想办法。
第五步:应用层路由逻辑
翻了开发团队的代码,他们的读写分离路由非常简单:所有 SELECT 走从库,所有 INSERT/UPDATE/DELETE 走主库。
但有个致命问题。用户下单后,立刻查询订单状态,这个查询也走了从库。订单刚写入主库,binlog 还没传到从库,当然查不到。
这就是典型的"写完立刻读"场景。我之前做项目的时候也犯过这个错——以为读写分离中间件会自动处理这种情况,实际上大部分中间件默认不会做"会话内读主库"的判断,得自己写规则。
三、三层治理方案
排查完根因,接下来就是治理。我分了三层来做,每层解决不同的问题。
架构层:开启并行复制
STOP SLAVE;
SET GLOBAL slave_parallel_type = 'LOGICAL_CLOCK';
SET GLOBAL slave_parallel_workers = 4;
START SLAVE;
worker 数量别拍脑袋设。经验值是 CPU 核数的 1/2 到 2/3。4核机设2-3个,8核机设4-5个。设太多反而会有线程切换开销,得不偿失。
如果 MySQL 版本是 8.0.20+,可以考虑 WRITESET 模式,冲突检测更精确。但前提是所有表都有主键——没主键的表在 WRITESET 模式下哈希值算不出来,冲突检测直接失效。这又是一个"所有表必须有主键"的理由。
配置层:半同步复制兜底
并行复制解决了回放速度,但没法保证从库"跟得上"。万一网络抖动或者主库写入暴增,从库还是可能落后。这时候需要半同步复制来兜底。
半同步的机制是:主库写完数据后,至少等一个从库确认收到 binlog,才返回给应用。这样从库不会落后太多,最坏情况也就是一个网络 RTT 的延迟。
-- 主库
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_master_timeout = 1000;
-- 从库
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_slave_enabled = 1;
timeout 设为 1000 毫秒。如果从库 1 秒内没确认,主库自动降级为异步复制,不影响业务可用性。这个降级机制很重要——半同步不能变成"不同步就卡死",否则从库一挂主库也跟着瘫。
实际部署中,我习惯用 SHOW STATUS LIKE 'Rpl_semi_sync%'; 监控降级次数。如果 Rpl_semi_sync_no_tx 持续增加,说明从库经常超时,得查网络或者考虑增加从库节点。
应用层:关键读操作走主库
这是最关键的一步。不是所有读都必须走从库,有几类读操作必须读主库:
- 写完立刻读的场景,比如下单后查订单状态
- 强一致性要求的场景,比如支付后查余额
- 数据量不大的读操作,主库完全扛得住
开发团队加了一个路由规则:带"强一致"标记的查询直接路由到主库,其他普通查询才走从库。改完这一行代码,超卖问题再没出现过。
这里顺便提一下,金仓数据库在读写分离集群场景下,提供了会话级的一致性读路由能力,同一会话内的写后读自动路由到主库,不用开发在应用层手动标记。这种底层能力能省不少代码,但也别完全依赖——应用层的判断逻辑还是要有的,毕竟不是所有中间件都提供这种能力。
四、延迟容忍度怎么定
这个问题没有标准答案,得看业务类型。
强一致场景,延迟容忍度是零。 用户下单、支付、扣库存,这些必须立刻读到最新数据,一秒都不能等。这类读操作直接走主库。
最终一致场景,延迟容忍度是秒级。 商品详情页的销量数字,用户看到"已售1000件"和"已售1005件",差几秒完全不影响购买决策。这类读操作走从库没问题。
离线分析场景,延迟容忍度可以到分钟级。 BI 报表、数据看板跑从库完全没问题。
判断标准其实就一句话:这个数据晚几秒读到,业务会不会出问题?会出问题就读主库,不会就交给从库。别搞一刀切。
五、日常监控怎么做
别等出事了才去查延迟,平时就得监控起来。
必看的三个指标
-- 1. 延迟秒数
SHOW SLAVE STATUS\G
-- 关注 Seconds_Behind_Master
-- 2. 从库回放状态
SHOW SLAVE STATUS\G
-- 关注 Slave_SQL_Running 和 Slave_IO_Running 是否都为 Yes
-- 3. 主从 binlog 位置对比
SHOW MASTER STATUS;
SHOW SLAVE STATUS\G
-- 对比 Exec_Master_Log_Pos 和主库的 Position
告警阈值建议
| 指标 | 预警线 | 报警线 |
|---|---|---|
| Seconds_Behind_Master | 1秒 | 5秒 |
| Slave_SQL_Running / Slave_IO_Running | 断开立即预警 | 断开立即报警 |
| 主从 binlog 位置差 | 超过100MB | 超过500MB |
定时巡检脚本
#!/bin/bash
# 每5秒检查一次延迟
while true; do
delay=$(mysql -e "SHOW SLAVE STATUS\G" | grep Seconds_Behind | awk '{print $2}')
echo "$(date): 延迟 ${delay}秒"
if [ "$delay" -gt 5 ]; then
echo "告警!延迟超过5秒!"
# 这里可以加邮件或钉钉告警
fi
sleep 5
done
配合 Prometheus + mysqld_exporter 做持久化监控,Grafana 面板上同时放延迟趋势、binlog 位置差、回放速率三个指标,一目了然。
六、延迟治理避坑清单
这几年做读写分离踩过的坑不少,整理几条出来。
大事务放在业务高峰期跑。 运营同学喜欢在白天跑批量更新,觉得方便。但大事务的 binlog 在从库回放很慢,高峰期从库本来就吃紧,再来个大事务直接雪崩。批量操作一定要放到低峰期,凌晨两三点跑,没人跟你抢资源。
写完立刻读还走从库。 这个前面讲过了,但真的太多人犯这个错。用户刚注册完跳个人中心,刚下完单跳订单详情,这些场景千万别走从库。直接读主库,性能完全扛得住。
以为开了并行复制就一劳永逸。 这是最大的误区。并行复制不是万能药,它只解决回放速度,不解决网络延迟,也不解决大事务。而且并行复制本身也有坑——没主键的表在 WRITESET 模式下会出问题,worker 线程设太多反而性能下降。得配合半同步复制和应用层路由一起用,才能真正稳。
从库当备份库用。 有些团队把从库用来跑定时报表、导出任务,甚至在上面建临时表。这些操作不占用主库资源,但会消耗从库的回放资源。从库被业务查询拖慢,延迟自然就上来了。建议单独搭一个分析节点,别跟复制从库混用。
监控只看 Seconds_Behind_Master。 这个指标有个致命缺陷:它依赖从库的系统时间。如果主从服务器时钟不同步,这个值就不准。所以一定要同时看 binlog 位置差,用 Exec_Master_Log_Pos 和主库 Position 的差距来判断,这个才是客观的。
总结
读写分离延迟治理,核心就三句话:
并行复制解决回放瓶颈,别让单线程拖后腿。半同步复制做兜底,保证从库不会落后太多。应用层路由最关键,强一致读主库,最终一致走从库。
这三层配合起来,延迟基本能控制在毫秒级。只靠调数据库参数是解决不了根本问题的,得从架构、配置、应用三个维度一起动手。
你在读写分离架构里遇到过什么坑?延迟最高飙到过多少?评论区一起交流,咱们互帮互助!
我是数据库小学妹,咱们下篇见 👋
本文案例基于 MySQL 8.0,不同数据库版本略有差异。核心思路通用,具体命令请参考对应数据库文档。
- 点赞
- 收藏
- 关注作者
评论(0)