复习这套Redis知识的时候,我习惯把脑裂问题放在高可用这一节的最前面。原因是它太容易踩中了:表面上看主从切换一切正常,哨兵该选举选举、该通知通知,但当你去比对数据时才发现,切换期间写入旧主的几万条数据已经无声无息地没了。更麻烦的是,Redis集群的脑裂不像ZooKeeper那样表现为“两个主节点同时对外服务”,它的主角往往是“一个已经失联但进程还活着的旧主节点”。这个问题不搞清楚,Redis的高可用架构就是纸糊的。
这篇文章从脑裂的成因讲起,把数据丢失的时间线、防脑裂参数配置、真实故障排查思路一次性聊透。适合正在复习Redis面试题的同学,也适合线上Redis集群出过“灵异丢数据”事故、想弄明白根因的运维和开发。
1. 脑裂到底是什么:分布式系统的经典故障
1.1 从“双领导”类比说起,Redis里脑裂长什么样
脑裂这个词,最早是从分布式系统里“Split Brain”直译过来的。想象一个两千人的公司,董事会因为通讯线路故障分成两派,两派都联系不上对方,于是各自都觉得“现在公司是我说了算”,同时发布了两套完全不同的经营指令。等通讯恢复,员工发现收到过两套互相矛盾的指令,但实际执行下去的已经收不回来了。
Redis里的脑裂很类似,但有个容易被新手忽略的细节:多数时候旧主节点并没有宕机,它只是“失联”了。比如一台Redis的master和它的哨兵、从节点在网络上被切开了,master这边进程照常运行,还在正常接收客户端的写请求,它根本不知道自己已经被其他节点“抛弃”了。与此同时,哨兵那边通过多数派机制判断出master可能挂了,于是从剩下可达的从节点里选出一个新master,开始对外提供服务。
于是尴尬的局面就出现了:旧master还在傻乎乎地写数据,新master也在正常接收写入。通信恢复后,哨兵会把旧master降级成slave,让它重新去同步新master的数据。降级的第一步是清空本地数据,旧master在失联期间写入的数据,就这么被当成“异物”直接抹掉了。
这就是Redis版本脑裂最经典的面貌:不是两个master一直同时活着,而是一个“假死”的master被强制退休,顺带把它假死期间的数据全部带走了。
1.2 Redis三种高可用架构下的脑裂形态
Redis的高可用方案看着多,其实脑裂的底层逻辑是一致的:分区 + 异步复制 + 自动切换,三个条件凑齐,丢数据就成了大概率事件。
主从加哨兵(Sentinel)是最常见的形态。哨兵进程通过PING来探活,当master在down-after-milliseconds配置的时间内没有响应,单个哨兵会先标记它“主观下线”。当达到quorum数量的哨兵都认为master不可达时,才升级为“客观下线”,触发故障转移流程,从候选从节点里选出一个晋升为新master。这里有个关键点:从旧master失联到新master被选出来,中间隔着一段时间窗口。假设哨兵判定耗时5秒,选举加通知耗时3秒,那这8秒里旧master如果还在接受写入,这些写请求就全部处在“待删除”状态。
Redis Cluster集群模式的脑裂思路也类似,只是判定机制换成了Gossip协议和cluster-node-timeout参数。默认情况下,一个master超过15000毫秒没被其他节点感知到,就会被标记为pfail(疑似下线),随后被传播成fail(确定下线),并引发从节点选举。注意这里的15000毫秒是很大的,如果网络分区持续了几分钟,旧master累积的写入量会非常可观。
还有一种容易被忽略的情况是纯主从架构,没有哨兵。如果master和slave网络断开,master继续写自己的,slave继续同步旧的,等网络恢复后slave自动重新全量同步,期间slave的数据其实已经悬空了。这种情况严格说也是脑裂的一种变体,因为没有自动切换,不会出现“双主”,但数据不一致问题一点没少。
我在排查故障的时候总结了一句话:只要存在“网络分区 + 主从数据异步复制 + 某个角色自动切换”这三个元素,就必须把脑裂放进排查清单里。
2. 为什么脑裂最怕的是“写丢失”
2.1 一次故障的时间线推演,把丢失数据算给你看
光说理论没感觉,我们推演一个生产场景,把数字算出来就直观了。
假设某个Redis集群峰值写入约2000 QPS,master和slave部署在同一机房的不同机架,三个哨兵监控。某天交换机的一个端口出现异常,master所在的机器被网络隔离,但进程没挂,本机客户端连接正常。此时时间线是这样的:
- 第0秒:网络分区发生,master失去与所有哨兵、从节点的通信能力,但还在正常接收客户端写入。
- 第5秒:3个哨兵中有2个判定master主观下线,达到quorum,升级为客观下线,开始走故障转移流程。
- 第8秒:哨兵选出优先级最高、复制偏移量最靠前的slave晋升为新master,客户端通过哨兵感知到新地址,开始切流。
- 第40秒:网络恢复,旧master重新连上集群。哨兵通过
slaveof指令强制它降级为从节点,旧master清空本地数据,尝试向新master做全量同步。
把写入量算上:2000 QPS乘以40秒,等于80000条写请求。这8万条数据在旧master上被写进去了,也返回客户端成功了,但最后被清空。客户端那边看到的是“写入成功”,实际上数据消失了。
更阴险的是,如果网络抖动反复发生,这种时间窗口会反复出现。每次窗口可能只有几秒,但架不住积少成多,最终给业务方造成“Redis偶尔丢数据”的模糊印象。
2.2 除了丢失,还有“脏数据覆盖”和“锁失效”两个隐藏问题
写丢失是脑裂最直接的危害,但还有两个不怎么被提起的副作用,同样是生产事故的种子。
第一个是脏数据覆盖。旧master降级后不会保留自己的写数据,但它的角色变成了slave,会接收新master同步过来的数据。如果接下来的写入操作命中了相同的主键,就会出现一种情况:旧master之前写的那个值已经被清掉,新master同步过来的值可能是完全不同的另一个版本。如果业务没有做幂等或版本校验,旧数据覆盖新数据、新数据覆盖旧数据,互相覆盖的混乱状态很难追查。
第二个问题在分布式锁场景下尤其危险。用Redis SETNX实现分布式锁时,脑裂会导致两个客户端同时持有同一把锁,因为两个master都对外提供了写入服务。等分区恢复,旧master被降级并清空数据,但它发出的“我拿到锁了”这个结果已经返回给客户端了。锁的互斥性在脑裂窗口内被彻底击穿。这也是为什么很多严苛的业务不会把分布式锁作为唯一方案,要么配合数据库唯一约束兜底,要么设计fencing token机制做二次校验。
2.3 哪些生产环境最容易触发脑裂
排除掉纯粹的人为误操作之后,生产环境触发脑裂的原因其实高度集中:
- 网络抖动和交换机故障排在第一位。机房里的网卡、光模块、交换机固件bug,都可能让一台机器“网断了但活得好好的”。
- 长时间的GC停顿也见过。JVM应用和Redis混部在同一台宿主机上时,如果宿主机内存压力大,GC长暂停几十秒,Redis进程虽然活着,但心跳响应已经停滞,哨兵照样会判定下线。
- 云环境的宿主机热迁移、网络策略变更,也会造成类似效果。很多云厂商的“维护事件”里都会提示网络瞬断风险,但很多团队没有把这个事件和Redis脑裂关联起来。
- 自己误操作也不少见,比如防火墙规则误改、交换机端口配置错误、运维在做网络割接时忘了通知业务方。
我印象很深的一次事故发生在Kubernetes环境里,节点之间因为NetworkPolicy配置不当互相ping不通,但Redis进程本身完全没有报错。等发现时,业务已经静默丢了几分钟的数据。所以排查脑裂时别只盯Redis日志,底层网络事件和节点事件往往能提供更关键的线索。
3. 防脑裂的核心配置:min-replicas 系列参数
3.1 参数原理:主节点也要“安全确认”
Redis官方的建议是用min-replicas-to-write和min-replicas-max-lag这两个参数,把“写入开关”和“主节点对从节点的感知能力”绑在一起。
核心思路是:master不应该无条件接受写入。它需要定期收到从节点的复制确认(ACK),ACK会带上从节点当前同步到的复制偏移量。如果master在一段时间内连一个从节点的ACK都没收到,或者收到的ACK显示从节点落后太多,master就主动拒绝写请求,直接给客户端返回错误。
这两个参数配合起来,效果就是“只有当至少N个从节点在M秒内正常和master保持复制心跳时,master才允许写入”。这样一来,一旦发生网络分区,旧master迅速丢失所有从节点的ACK,写请求会在很短的时间内被拒掉。等哨兵完成新master的选举,旧master那边已经不再接收新写入,数据丢失窗口就被大幅压缩。
这两个参数的默认值都是0,也就是说默认情况下master不会做任何限制。这也是很多线上事故的原因:集群是搭起来了,哨兵也配了,但没人把这两个参数改掉。
3.2 实战配置:从节点数1个、滞后10秒的推荐起点
最稳妥的起步配置是:
min-replicas-to-write 1 min-replicas-max-lag 10含义是:master至少要能感知到1个从节点,且这个从节点的复制延迟在10秒以内,才接受写请求。不满足就直接拒绝写,客户端会收到类似MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'或者-MISCONF Redis is configured to save RDB snapshots...这类错误提示。
如果是老版本(Redis 5.0以前),参数名是min-slaves-to-write和min-slaves-max-lag。Redis 5.0加入了min-replicas系列命名,但为了兼容,旧参数名仍然可用。集群模式的配置文件写法一样,可以直接写在redis.conf里,也可以运行时执行:
CONFIG SET min-replicas-to-write 1 CONFIG SET min-replicas-max-lag 10这里有个细节值得多说一句:为什么推荐从节点数配1、lag配10,而不是配3个从节点、lag配2秒?
min-replicas-to-write配成1,前提是你的集群里每个master至少带1个从节点。如果你配成2,但实际只挂了1个从节点,那master会一直拒绝写入,等于自断后路。所以这个值要实事求是,根据架构里的真实副本数来定。
min-replicas-max-lag配10,是平衡“丢失窗口”和“误伤概率”的结果。lag配太小,比如2秒,那么从节点只要稍微抖动一下、主从复制慢了一点,master就开始拒绝写入,正常业务会频繁看到写入失败。lag配太大,比如60秒,网络分区后旧master还能继续写将近一分钟,丢失的数据量又涨上去了。10秒是一个比较稳妥的中间值,大多数正常情况下的复制延迟都远低于这个值,而脑裂场景下旧master通常几秒内就会和所有从节点失去ACK联系。
3.3 这个方案能保证不丢吗:认识它的边界
必须说清楚,min-replicas系列参数只能尽量缩小丢失窗口,做不到零丢失。
它保护的是“旧master和它的从节点之间失去联系”的场景。如果网络分区比较特殊,比如旧master仍然能和某一个从节点保持复制心跳,但和哨兵以及其他所有节点都隔离了,那旧master会一直认为自己有健康的从节点,继续接受写入。这种情况下,min-replicas参数不会触发,写丢失照样会发生。
所以这个方案的真实定位是:在大多数常见分区场景下,把“整个分区期间都在写”变成“分区发生后几十秒内停止写入”。丢失窗口从分钟级压缩到秒级,这个效果已经足够让绝大多数业务恢复元气。
如果业务对数据一致性要求极高,需要进一步压降丢失风险,可以在客户端配合Redis的WAIT命令。WAIT numreplicas timeout可以阻塞等待指定数量的从节点确认收到某次写入,相当于把异步复制临时变成半同步复制,但代价是写延迟显著上升,吞吐量下降。再往上的方案,比如写本地消息队列做补偿、跨机房双写、直接用强一致性的存储产品,就是架构层面的取舍了,不是Redis配置能解决的问题。
3.4 集群模式还要看cluster-node-timeout
用Redis Cluster时,除了min-replicas系列参数,cluster-node-timeout也是脑裂相关的关键参数。
默认值是15000毫秒,也就是15秒。节点之间超过这个时间没收到彼此的心跳,才会开始标记疑似下线。这个值直接决定了“从网络断开到集群开始响应”的间隔。调小一点,比如5000毫秒,故障切换的响应速度会变快;但调太小也有风险,瞬时网络抖动就可能触发节点标记,引发不必要的选举切换。这个参数要和min-replicas-max-lag配合起来看,整体原则是:让master拒绝写入的速度快于集群完成切换的速度,这样即使发生切换,旧master那边也已经没什么新数据可丢了。
4. 真实复盘:排查脑裂的完整思路
4.1 一次典型故障复盘的五个步骤
我自己经历过的脑裂事故,复盘的时候基本沿着五步走,每一步都能筛掉一批干扰项。
第一步,锁定现象。业务侧反馈Redis开始大量写入失败,同时监控显示某个分片出现瞬时主从切换。这时候先不要急着重启或切流,把事故时间点记下来,方便后面和日志对上。
第二步,查Redis日志。旧master的日志里会出现类似MASTER aborted replication with an error或REPLICAOF <new-master> enabled之类的记录,表示它已经收到降级指令,开始向新master同步。哨兵日志里会有+switch-master事件,记录旧master和新master的地址变化。把这两个时间点对齐,就能看出脑裂窗口大概有多长。
第三步,对比复制偏移量。在旧master和新master上分别执行INFO replication,看master_repl_offset和slave_repl_offset。正常情况下,新master的偏移量会明显领先于旧master降级前的偏移量,两者之间的差距就是丢失数据的规模。
第四步,查客户端行为。通过CLIENT LIST观察事故时间点有哪些客户端连接在旧master上持续写入。如果有服务端连接池没有及时刷新master地址,会有一部分客户端在哨兵切换后仍然往旧master写,直到连接被拒绝。
第五步,结合网络事件下结论。把Redis日志时间线和宿主机网络事件、交换机变更记录对齐。多数情况下你会发现,Redis日志里没有任何报错,真正的根因在操作系统层面或网络设备层面。
4.2 排查命令与日志关键词速查表
| 排查对象 | 关键命令/日志 | 判断要点 |
|---|---|---|
| 当前节点角色 | INFO replication | role:master还是role:slave,connected_slaves是否正常 |
| 复制进度 | INFO replication中的master_repl_offset、slave_repl_offset | 两者差值越来越小说明在追赶,基本不变说明同步中断 |
| 哨兵视角 | sentinel master <mymaster> | 看当前master地址是否和预期一致 |
| 哨兵日志 | +switch-master | 记录旧master到新master的切换时间和地址变化 |
| Redis实例日志 | MASTER aborted replication、REPLICAOF enabled | 旧master降级、开始全量同步的标记 |
| 客户端连接情况 | CLIENT LIST | 事故时间点还有多少客户端在往旧master写数据 |
| 慢查询与假死判断 | SLOWLOG GET | 结合宿主机GC时间,排查进程假死导致的心跳无响应 |
4.3 关于脑裂的三个常见误区
第一个误区:部署了哨兵就等于不会脑裂。实际上哨兵解决的是“自动故障转移”问题,它不会阻止脑裂窗口内的数据丢失。哨兵切换做得越快,丢失窗口越小,但窗口永远存在。
第二个误区:把min-replicas-to-write配高一点就更安全。这个配置的前提和实际副本数强相关,配得比真实副本数还高,只能得到一个永远拒绝写入的集群。正确的做法是先数清楚每个master带几个slave,再决定这个值。
第三个误区:Redis没报错就不是脑裂。大量脑裂场景中,旧master的日志干干净净,因为它确实没有感知到自己失联。排查思路不能只盯Redis本身,要把网络层、宿主机层的事件一起拉出来看。
5. 架构层面怎么减少脑裂影响
5.1 客户端、应用和部署层面的配合
只调Redis参数还不够,客户端的行为在脑裂场景里同样关键。
最基础的要求是客户端必须通过哨兵或Cluster的总控节点感知master地址,不能把master地址写死在配置里。否则切换完成后,一部分客户端还握着旧地址往里写,Redis服务端会拒绝这些写入,业务方会看到一堆READONLY错误,但至少数据不会继续丢在旧master上。
应用层可以做的兜底是把写入失败的数据记录到本地或消息队列,等Redis恢复后重新投递。这个方案不复杂,但收益非常明确:即使Redis在极端情况下丢了数据,应用层还有一个可回放的数据源。
部署层面有个容易被忽略的点:master和它的slave应该避免放在同一个网络故障域里,比如同一台交换机的不同端口,或者同一个机架。如果master和slave同时被一个交换机故障带走,哨兵连候选从节点都找不到,那就不是脑裂丢数据的问题,而是整个分片不可用了。跨机架、跨可用区部署,是降低脑裂影响的有效手段。
5.2 面试被问“Redis脑裂”时怎么答
这道题是Redis面试的高频题,回答的时候不要只丢一个结论,最好按“是什么、为什么、怎么防、防到什么程度”的框架来讲。
先讲定义:脑裂是网络分区导致的多个节点同时认为自己是主节点的现象。再讲Redis特有的场景:旧master进程存活但网络失联,继续接收写入,哨兵选出新master后旧master降级并清空数据。接着讲危害:异步复制的时间窗口内,写入旧master的数据全部丢失,严重时影响分布式锁互斥性。然后讲方案:min-replicas-to-write和min-replicas-max-lag让master失去从节点ACK时拒绝写入,压缩丢失窗口;客户端通过哨兵感知master变化;应用层做消息兜底。最后补充边界:这个方案无法做到零丢失,只能缩小窗口。
能主动提到“复制偏移量对比”和“WAIT命令”,会让面试官觉得你是真处理过问题,而不是只背了八股文。
5.3 说句实在话:取舍比参数更重要
每次配置完Redis的高可用环境,我都会问自己一个问题:如果现在把网络切断,系统会丢多少数据?能接受吗?
这个问题没有标准答案。有的业务丢了10秒数据也无所谓,有的业务丢1条就是事故。min-replicas-max-lag配10还是配3,cluster-node-timeout配15000还是5000,本质上都是在延迟故障切换的误判率和数据丢失的窗口之间做取舍。参数调完以后,我强烈建议找一个演练窗口,人为断一次网或者关掉一台master的网卡,观察集群的切换行为和数据丢失量。纸上谈兵永远没有真实故障来得直观。
我自己踩过坑之后的习惯是:线上集群巡检时固定看INFO replication的lag值,只要发现主从复制延迟持续超过阈值,就立刻查网络和慢日志。脑裂这种事,防是防不住的,但能不能在窗口期发现并止损,才是真正区分经验和教训的地方。