集群脑裂排查实战:Redis 与 MySQL 双主写冲突根治

在分布式系统的世界里,脑裂(Split-Brain) 是运维最不愿遇到的故障之一。当网络发生分区,原本”一主多从”的集群出现”两个主节点同时对外提供服务”的局面,客户端写请求被双写,数据在两边各说各话,最终合并时往往意味着永久性的数据丢失。本文以最常用的 Redis 与 MySQL 两类集群为例,从成因、识别到根治,给出一套可落地的排查与防御方案。

一、什么是脑裂:一次写入,两份真相

脑裂指的是:在一个依赖”唯一主节点”做写入的集群里,由于网络、进程假死或切换程序误判,原本唯一的主节点被切走,但旧主并未真正下线,仍然在接受写入,而新主也已选出并开始接收写入。于是同一份数据在两边各自演进,谁也无法回滚到对方的状态。等到网络恢复,两个”主”面对面时,冲突已经既成事实。

它与普通的”主从延迟”本质不同:主从延迟只是数据晚到,主的身份始终唯一;而脑裂是主的身份出现了两个。这正是它比单纯宕机更隐蔽、更危险的原因。

二、脑裂为什么比宕机更可怕

一次干净的宕机,最坏结果是”服务不可用”,但数据是一致的、可追溯的;而脑裂的表象往往是”服务还活着”,于是它能在无人察觉的情况下悄悄制造脏数据:

  • 双写冲突:同一个用户余额、同一笔订单,在两边被分别修改。
  • 主键/唯一键冲突:两边都自增主键,恢复后主键重叠,写入直接报错。
  • 下游脏读:读流量被负载到旧主,读到的是已经”被废弃”的时间线。
  • 难以察觉:网络分区通常在几十秒到几分钟内自愈,故障窗口短,日志分散,事后定位极费劲。

三、Redis 集群脑裂:最典型的场景

3.1 异步复制 + 网络分区

Redis 主从默认是异步复制:主节点写完就返回客户端成功,再异步把命令发给从节点。当主节点与 Sentinel 之间的网络抖动,Sentinel 误判主”下线”,把一个从节点提升为新主;而此时旧主其实只是网络不通,进程还活着、仍在接受客户端写入。分区恢复后,旧主带着这段”孤儿写入”重新上线——脑裂就此发生。关于 Redis 集群本身的部署与故障转移机制,可参考 Redis 集群搭建实战:哈希分片与故障转移。

3.2 如何识别 Redis 脑裂

第一现场是 Sentinel 日志里出现选主记录,随后在旧主上用 info replication 对比复制 ID,会发现它仍然自认为是 master:

redis-cli -h old-master info replication
# 关注 master_replid 与 connected_slaves
# 若 connected_slaves 为 0 且自身 role:master,但 Sentinel 已切换,则高度疑似脑裂

# Sentinel 侧排查切换时间线
grep -E "+\(s\|o\)down|switch-master" /var/log/redis/sentinel.log

四、根治 Redis 脑裂:用 min-replicas 兜底

Redis 提供了 min-replicas-to-write 与 min-replicas-max-lag 两个参数,作用非常直接:当可用从节点数量不足或复制延迟过大时,主节点直接拒绝写入。这样在网络分区、从节点失联的瞬间,旧主就失去了写能力,从根源掐断单边写入。

# redis.conf
min-replicas-to-write 1
min-replicas-max-lag 10

含义是:至少要有 1 个从节点、且复制延迟不超过 10 秒,主节点才允许写入。配置后分区期间旧主会返回 (error) READONLY You can't write against a read only replica,看似”写失败”,实则是保护了数据一致性。这与 Redis 读写分离实战 中强调的”主负责写、从负责读”边界是一脉相承的。

4.1 旧主复活后必须先降级

无论用 Sentinel 还是 Redis Cluster,旧主网络恢复后绝不能原样上线。正确做法是让它以从节点身份重新加入:先 redis-cli -h old-master REPLICAOF new-master 6379 把它降级为从,待复制追上后再视情况决定是否参与选主。跳过这一步,就是给下一次脑裂埋雷。

五、MySQL 主从脑裂:双主写冲突

5.1 典型场景:切换程序提升新主后旧主复活

MySQL 主从同样存在异步/半同步复制。当 MHA、Orchestrator 或 Keepalived 把从库提升为新主,而原主只是网络闪断、进程仍在,且应用连接池还缓存着原主地址继续写入,就会形成双主。更麻烦的是 MySQL 没有 Redis 那种”从节点不足即拒写”的内建开关,需要靠配置与流程兜底。主从复制本身的搭建可回顾 MySQL 主从复制与读写分离实战。

5.2 识别 MySQL 脑裂

-- 在新主上检查是否仍在追原主的日志
SHOW SLAVE STATUS\G
-- 关注 Master_Log_File / Read_Master_Log_Pos 与 GTID_EXECUTED

-- 对比新旧主的执行位点,定位"孤儿事务"
SELECT @@GLOBAL.GTID_EXECUTED;
-- 应用侧:确认连接配置是否仍指向原主 IP,必要时强制刷新连接池

如果新主的 GTID_EXECUTED 与原主对不上、且两边都有对方没有的事务,基本可以判定发生过双写。

六、根治 MySQL 脑裂:半同步 + 强制只读

第一步,开启半同步复制,把切换窗口的数据差异尽可能缩小:

INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_slave_enabled = 1;

第二步,也是最关键的一步:切换后旧主一律置为只读。无论是 MHA 的 --r 参数、还是 Orchestrator 的自动 recover,都要保证旧主上线即 read_only=ON 且 super_read_only=ON,这样即便应用误连也写不进去。关于主从延迟的成因与监控,可延伸阅读 MySQL 主从延迟排查实战。

6.1 设计内双主不是脑裂

注意区分:主动配置的 active-active 双主(双向复制、按分片避免同键冲突)是”设计内的多写”;而脑裂是”故障导致的非预期多写”。两者的防护思路不同,不要因为怕脑裂就否定合理的双主架构。

七、通用防御清单

防御措施适用对象作用
法定人数 quorum所有选主组件(Sentinel/etcd/Consul)避免少数派在网络孤岛里自嗨选主
min-replicas / read_onlyRedis / MySQL从根源切断单边写入能力
fencing / STONITH共享存储、强一致场景物理隔离旧主,防止它”死而复写”
健康检查 + 自动降级切换程序(MHA/Orchestrator)旧主复活立即降级为从或只读
客户端指向新主应用连接层刷新连接池,杜绝写旧主

八、监控与告警:把脑裂扼杀在摇篮

脑裂能不能被快速发现,取决于监控是否把”主的数量”当作一等指标。Prometheus + blackbox 探测主节点可达性,再用一条简单的规则告警”主多于一个”:

# Prometheus alert rule
- alert: SplitBrainSuspected
  expr: count(redis_master_up == 1) > 1 or count(mysql_master_up == 1) > 1
  for: 30s
  labels:
    severity: critical
  annotations:
    summary: "检测到多于一个可写主节点,疑似脑裂"

同时把复制延迟做进 Grafana 面板,配合 PostgreSQL 流复制实战:用 Patroni 实现高可用主从集群 提到的自动选主思路,让”主身份唯一”成为可度量的红线。

九、真实故障复盘

曾有一次凌晨机房网络抖动,Redis Sentinel 在 3 秒内连续误判主节点下线,把一个从提升为新主;而旧主其实只是心跳包丢了一阵,进程仍活、仍在收写约 3 分钟。网络恢复后,两边数据各有一批对方的”孤儿 key”。排查路径是:先按 Sentinel 日志里的 switch-master 时间戳锁定窗口,再比对新旧主的 master_replid 与 connected_slaves,最后评估丢失的 key 范围并决定是从备份恢复还是业务对账。教训有两点:一是给 Sentinel 的 down-after-milliseconds 加防抖、把 quorum 设高,避免抖动即切换;二是全量开启 min-replicas-to-write,让旧主在网络孤岛里直接丧失写权限。这类生产排错思路,与 数据库死锁实战:从复现、定位到根治 中”先复现再根治”的方法论是一致的。

结语

脑裂的本质,是系统在故障瞬间把”可用性”压过了”正确性”。防御的核心只有一句话:任何时刻只允许一个可写的主,且旧主复活必须先降级。把 min-replicas、read_only、quorum、fencing 这四道防线布好,再配上”主数量 > 1 即告警”的监控,脑裂就不再是让你半夜爬起来救火的深夜噩梦。

上一篇 Thanos 实战:Prometheus 长期存储与全局监控聚合
下一篇 MySQL 主从复制延迟排查与优化实战
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]