Administrator
发布于 2022-12-04 / 9495 阅读
85

一次主从延迟导致的线上数据不一致

用户投诉:刚改完昵称又变回去了

那天下午客服转来 7 个工单,都是"改了资料刷新又回旧值"。我第一反应是缓存,清了一遍没用。查监控才发现,是主从延迟在作怪——前端查到了还没同步过去的旧数据。更诡异的是,有的用户反馈"改完看是对的,过两秒刷新又变回去",典型的读到了从库旧快照。

定位过程

用户在主库改完,前端立刻带着刚改的数据去从库查(读请求按 userId 哈希到从库),而从库比主库慢了 3 秒,查到的还是旧行。我们路由层是"写主读从",且读从库是按 userId 取模选的,恰好改完立刻读就落到延迟最大的那个从库。

show slave status\G
-- Seconds_Behind_Master: 3
-- 大事务期间一度飙到 40+

正常 3 秒还能忍受,但一旦运营跑批量任务,延迟瞬间飙到 40 秒以上,这时候用户改完资料短时间内查回来全是旧的。

根因:一个大事务

运营那边的批量标签任务,一个事务里 update 了 80 万行,binlog 在主库秒完,从库回放是单线程(旧版)慢慢追,积压严重。主库写完就返回了,从库还在吭哧吭哧回放那 80 万行,期间所有读从库的请求都读到旧数据。

解决手段

  • 拆分大事务:80 万行改成每 2000 行一提交,单事务回放时间从分钟级降到秒级;运营侧改造后,批量任务从"卡 40 秒"变成"持续 5 分钟但每批只延迟 1 秒";
  • 开启并行复制(MySQL 8.0 的 WRITESET 模式),按事务写集冲突检测并行回放,而不再是单线程串行:
STOP SLAVE;
SET GLOBAL binlog_transaction_dependency_tracking = WRITESET;
SET GLOBAL replica_parallel_type = LOGICAL_CLOCK;
SET GLOBAL replica_parallel_workers = 8;
START SLAVE;
  • 读己写一致性:用户刚写完的资料,接下来 2 秒内强制走主库(用注解路由),过了窗口再回到从库。这是用一个小时间窗换一致性,代价是刚改完那 2 秒主库多扛点读。

效果

并行复制开启后,Seconds_Behind_Master 峰值从 40+ 降到 1 以内;配合读主库兜底,同类工单归零。我们还顺手在 Prometheus 里加了主从延迟告警:

alert: SlaveLag
expr: mysql_slave_status_seconds_behind_master > 5
for: 2m

更深层的思考

主从延迟本质是"最终一致"和"用户期望立即看到"的冲突。除了技术手段,我们还和产品达成了共识:资料类强一致场景,干脆就不走从库;只有报表、日志这类弱一致场景才读从库。把"哪些读可以容忍延迟"在架构层面定清楚,比事后打补丁省事。

另外,单线程复制是老问题,MySQL 5.7 就有多线程复制,但我们一直没开。这次事故鞭策我们把所有从库都升到 WRITESET 并行复制,算是意外收获。

读己写的具体实现

"刚写完 2 秒走主库"怎么落地?我们在写操作后往 Redis 记一个标记 userId → 过期 2 秒,读请求进来先查这个标记,有就强制走主库:

if (readYourWriteCache.has(userId)) {
    return masterTemplate.get(...);
}
return slaveTemplate.get(...);

2 秒是经验值,覆盖绝大部分"改完立刻看"的交互。超过 2 秒还查到旧值的概率极低,且那时候用户多半已经离开页面。这个标记用 Redis 存,天然过期,不占应用内存。

为什么不用半同步复制

有人建议开半同步复制(rpl_semi_sync_master)彻底消除延迟。我们评估过:半同步保证至少一个从库收到 binlog 才返回,会拖慢主库写 RT(多一次网络往返),对写多的订单库不划算。而且半同步只保证"收到",不保证"回放完",延迟仍在。所以我们没开,靠并行复制 + 读己写更务实。主从延迟本质是"最终一致"和"用户期望立即看到"的冲突,架构上区分强弱一致读才是根治。

监控指标怎么配

主从延迟的监控不能只看平均值。我们配了两个告警:一是 Seconds_Behind_Master 持续 > 5 秒超 2 分钟告警;二是更灵敏的"主从数据校验"——定时抽 100 行比对主从一致性,不一致立即告警。后者能抓到"延迟恢复了但数据已经偏了"的情况。光看延迟可能以为恢复了,实际中途丢过同步,校验才暴露。建议延迟监控 + 抽样校验双保险。

一次险些扩大的事故

主从延迟修复后不久,运营又跑了次更大的批量(200 万行),虽然已经拆了事务,但 WRITESET 并行复制在写集冲突多时也并不动——因为 200 万行都改同一张大表的不同行,WRITESET 认为可并行,实际回放时行锁冲突还是串行。延迟又到 15 秒。我们加了"批量任务限流速":每小时最多提交多少行,错峰执行,把延迟压在 3 秒内。教训:并行复制不是万能,高冲突写仍会串行,业务侧限流才是兜底。

写在后面

现在回头看,《一次主从延迟导致的线上数据不一致》本身不算多难,难的是线上真出问题那十分钟里的判断。经验都是这么来的。

参考