在分布式系统的世界里,脑裂(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_only | Redis / 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 即告警”的监控,脑裂就不再是让你半夜爬起来救火的深夜噩梦。




