Redis脑裂全链路解析:从网络分区到数据丢失的防护策略

发布时间:2026/10/3 14:08:39
Redis脑裂全链路解析:从网络分区到数据丢失的防护策略 先纠正一个拼写标题里的“Reids”就是 Redis这个手滑我第一眼也愣了一下但线上遇到“脑裂”时可就笑不出来了。Redis 脑裂Split Brain是我这几年排过的分布式故障里最折磨人的一种它不会像 OOM 那样把服务一拍子打死而是让你在“写入返回成功”之后眼睁睁看着那批数据在几秒后被清空。最近社区里讨论这个的热度又上来了但很多人对它的理解还停留在“网络分区导致出现了两个主库”这层。脑裂窗口里数据到底是怎么没的、Sentinel 是怎么一步步把旧主推下台的、min-replicas 系列参数到底怎么救场这些关键点很少有人一次讲透。这篇文章就围绕我亲身经历的一次凌晨事故把 Redis 脑裂的完整链路、防丢数据的配置组合拳、以及验证和复盘方法系统地梳理一遍。看到这里的读者无论你是正在维护主从架构的运维、后端开发还是准备面试想把这个点说清楚都应该能从中拿到直接能用的东西。1. 一次凌晨事故从“写成功了”到数据消失那是某次版本迭代上线后的第三天线上拓扑不复杂一主两从三个 Sentinel 分布在两个机房里主库在 A 机房两个从库分别放在 A、B 机房业务读写都走主库每秒大约 1500 个写请求。这套架构在平时看起来很稳Redis 主从复制本来就很成熟Sentinel 高可用也跑了大半年没出过岔子。可问题偏偏出在深夜的 A 机房网络设备上。凌晨 0 点 40 分左右A 机房到 B 机房之间开始间歇性丢包再往后直接双向不通。Sentinel 和 B 机房从库一下子都联系不上主库了但主库本身进程没挂内存、CPU、连接数都正常还在继续接收来自其他网络路径的客户端写入。Sentinel 这边按配好的规则走了正常的故障转移流程先主观下线再客观下线最后把 B 机房的从库提升成新主。整个过程大概花了 40 秒。麻烦就出在这 40 秒以及后续的一个多小时里。因为业务写库用的还是主库的固定 IP网络分区只是隔离了主库和从库、主库和 Sentinel客户端到主库的那条链路并没有断。于是新主已经在 B 机房开始服务了A 机房的老主也还在老老实实地接收写入并返回 OK。两边都在写数据而且互相不知道对方存在——这就是教科书级的脑裂。凌晨 1 点出头A 机房网络恢复。老主重新连上 Sentinel发现自己已经被“降级”于是转成新主的从库。由于脑裂期间老主积累了大量新主完全没有的写入数据复制拓扑无法做增量对齐只能触发全量同步。全量同步的第一步就是把老主现有数据清空。那一刻脑裂期间所有写进老主的数据约 2 万多个 key 的变更直接没了。事后复盘我在 Sentinel 日志里看到了标准的sdown、odown、switch-master事件链也在老主日志里看到了全量同步的痕迹。结论没有任何争议分布式系统里最经典、也最容易在 Redis 上发生的 Split Brain。真正刺痛我的不是故障本身而是配置上明明可以加一道拦截却因为“从没出过事”而没有加。如果当时老主在发现自己没有任何可用从库时就拒绝写入这 2 万个 key 根本不会丢。2. 脑裂全链路拆解网络分区、误判、旧主复活三幕剧要把脑裂讲清楚光知道“两个主同时写”是不够的得把三个阶段分开看网络分区发生了Sentinel 做了什么决策老主恢复后经历了什么。2.1 第一幕主从之间那根“看不见的网线”断了Redis 主从之间不是简单的心跳而是一条持续的数据复制流。从库每秒会通过REPLCONF ACK向主库上报自己当前的复制偏移量主库也会不断把新的写命令推给从库。Sentinel 则用 PING 来探测 Redis 实例是否活着默认每秒一次。当网络出现分区B 机房 Sentinel 和从库都收不到主库的 PONG但主库进程本身完全正常它能继续接受客户端连接、处理命令、返回结果。这个阶段最容易让人犯迷糊判断一个节点“挂了”的标准到底是什么Sentinel 和从库看到的是网络不可达客户端看到的是服务正常。三个角色各自拿到了互相矛盾的事实可谁也没有全局视角。就好比你和一个同事在同一个办公室里中间突然竖起一道单向玻璃墙你能看到自己干得热火朝天墙外所有人都觉得你消失了。Redis 的脑裂就是在这样一道“玻璃墙”两端同时发生的。补充一个细节所谓网络分区不一定是物理网线断了。常见的诱因包括机房交换机故障、跨可用区专线抖动、防火墙策略误变更甚至是长时间大对象引发的 GC 停顿导致哨兵心跳超时。所以别指望“我们网络很稳定”来避免脑裂配置层面的防护才是唯一可靠的。2.2 第二幕Sentinel 的“客观下线”决策过程Sentinel 的判定分两级这是很多人没有仔细区分的点。第一级叫主观下线SDOWN。单个 Sentinel 在down-after-milliseconds配置的时间内没有收到主库的有效响应就会把这个主库标记为主观下线。注意这只是一个 Sentinel 的“个人意见”网络抖动、瞬时高负载、进程短暂的假死都可能造成误判。第二级叫客观下线ODOWN。主观下线状态下Sentinel 会通过SENTINEL is-master-down-by-addr命令询问其他 Sentinel 对该主库的看法。当同意下线的 Sentinel 数量达到quorum参数阈值后主库才会被确认为客观下线。随后 Sentinel 集群会选举一个 Leader 来执行故障转移从符合选举条件的从库中挑一个提升为新主并通知其他从库和客户端。这里必须认清一个残酷事实Sentinel 切换的依据是“网络可达性”不是“节点健康状况”。主库进程是否活着、内存是否正常Sentinel 根本不知道。它只知道“我联系不上它了”然后按规则做高可用切换。在网络分区这类场景下这个决策本身没有错但它天然会产生一个时间窗口旧主还活着新主已经上线。Sentinel 面向的是控制面的真相可数据面承担的是另外一个真相。2.3 第三幕旧主复活后被强制清空的那一瞬间网络恢复后旧主会重新连上 Sentinel随即收到“你现在已经是新主的从库”的指令。Redis 的主从复制世界里一个本来在主库位置的节点一旦变成从库就要无条件向新主对齐数据。对齐方式有两种。如果旧主的复制偏移量恰好落后得不多而且它的数据集可以看作新主复制历史的前缀子集那么可以做部分重同步只补增量。但脑裂期间旧主接收了大量新主从未见过的写入这台实例的数据集已经处于一个“分叉”状态既不是新主历史的前缀偏移量也远超repl-backlog能覆盖的范围。于是主从协商后只能选择全量同步新主生成一份 RDB 快照发给旧主旧主在接收前先把本地所有数据清空。清空的那一瞬间就是脑裂期间所有“成功写入”的终结。如果你在客户端看到的响应是OK但在新主的数据目录里根本找不到这条记录那么恭喜你你已经拿到了脑裂事故最标准的“纪念品”一条没有任何痕迹消失的数据。下面这张表可以帮你快速理顺整个时间线时间点控制面状态数据面状态0:40:00A/B 机房网络分区旧主正常写数据从库失联0:40:10Sentinel 标记旧主 SDOWN客户端仍写入旧主0:40:25Sentinel 达成 quorum 标记 ODOWN客户端仍写入旧主0:40:35触发故障转移新主上线部分客户端可能开始写新主0:40:52新主正式接管旧主仍在写取决于客户端路由1:01:30网络恢复旧主转为从库1:02:10全量同步开始旧主数据被清空1:02:20同步完成脑裂期间写入旧主的数据永久消失3. 异步复制的“债”为什么写入成功却守不住数据很多开发者第一次听到脑裂导致丢数据时第一个反应是“Redis 不是有复制吗从库也有数据啊”。这里的关键就在“异步复制”四个字上。3.1 主节点写完就返回的真相Redis 默认采用异步复制。主库每执行一条写命令会先写入自己的内存然后立刻向客户端返回成功之后再异步地把这条命令传播给从库。主库不会等待任何从库完成同步再返回结果。这个设计在绝大多数场景下是对的它保证了 Redis 的低延迟和高吞吐。但在脑裂场景下它变成了最大的风险放大器旧主进程活着它就认为自己依然拥有法理上的主库地位继续接受写入并返回OK。哪怕它此时已经没有任何一个从库能同步一台孤零零的“光杆司令”依然可以把所有写入吞进内存然后告诉业务“写好了。”如果当时主库能感知到“我已经没有任何副本了”并拒绝写入脑裂的损失几乎可以降到零。但默认配置下Redis 不会做这个判断。3.2 部分同步还是全量同步偏移量决定了旧主命运Redis 主从复制通过复制偏移量replication offset来追踪进度。主库的偏移量随着写命令不断增加从库会通过 ACK 上报自己已同步到的偏移量。网络分区期间新主在继续接受写入偏移量持续增长旧主则停留在断连前的偏移量。等网络恢复两者之间的差距已经非常大。如果差距维持在repl-backlog-size范围内理论上可以走部分重同步只把从库缺的增量命令推过去。但正如前面所说部分重同步有一个隐藏前提从库的数据集必须是主库复制历史的前缀子集。脑裂让旧主产生了大量新主没有见过的写入这些数据不属于新主历史的一部分数据集已经分叉。因此无论偏移量差多少旧主都只能走全量同步。全量同步就意味着先把旧主内存里的数据全部清空再导入新主快照这也直接宣告了脑裂窗口期内所有旧主写入的死刑。有些同学会问那能不能让旧主在降级之前先把脑裂期间的数据导出备份一份理论上有操作空间但故障恢复场景下你根本没有从容的时间窗口。先做备份再同步等于人为拉长业务不可用时间运维决策上很难执行。所以真正该做的还是防患于未然。3.3 脑裂窗口到底有多长脑裂窗口可以简单定义为“旧主断连”到“旧主数据被清空”之间的时长。它由几个因素叠加决定网络分区本身的持续时间Sentinel 从发现失联到完成故障转移的总时长down-after-milliseconds、failover-timeout等参数网络恢复后旧主重连、主动发现降级、执行同步的准备时间在这个窗口内每秒钟写入旧主的数据都可能丢失。假设 QPS 是 1000脑裂窗口是 1 分钟那就是 6 万次写入。脑裂问题最残酷的地方就在这里它的危害不取决于你写了多少而取决于你“在错误的主上写了多久”。如果非要把这套机制放进 CAP 理论的框架里传统 Redis 主从复制默认选的是可用性优先AP数据一致性需要你用额外的配置和业务手段去挣回来。这就是WAIT和min-replicas存在的原因。4. 止血组合拳min-replicas 配置、WAIT 与客户端路由防脑裂不是靠“祈祷网络稳定”而是要主动把“孤主写入”这条路堵死。我个人实践下来最有效的组合是下面三拳。4.1 第一拳min-replicas-to-write 把“孤主”打入只读状态Redis 提供了一对非常关键但经常被忽略的参数min-replicas-to-write 1 min-replicas-max-lag 10含义是主库只有在“当前在线的从库数量”不少于min-replicas-to-write并且这些从库的最大复制延迟不超过min-replicas-max-lag秒时才允许执行写命令。一旦从库数量掉到阈值以下或从库延迟超过阈值主库会直接拒绝写命令客户端会收到错误而不是OK。把这对参数套进脑裂场景里旧主和所有从库都失联了它视角里的在线从库数量是 0小于配置的 1于是它的可写性被立即掐断。客户端继续往旧主写入会得到失败响应业务就会走失败重试或切换而不会产生一份注定要被清空的数据。这里有一个非常值得说透的权衡min-replicas-to-write 1意味着如果唯一的一个从库宕机了主库也会拒绝写入等于在“可靠性”和“可用性”之间做了取舍。生产环境里我建议至少保留两个从库再启用这个参数否则主库可能在从库重启、网络抖动时出现短暂写入不可用那种告警也很闹心。但相比脑裂丢数据的代价这点可用性损失完全值得。官方默认这两个参数是0和10也就是默认不启用写保护——所以这句话再说一遍不配置就没有保护。4.2 第二拳WAIT 命令让关键写入等一等WAIT命令是 Redis 提供的同步复制确认机制基本用法如下 SET order:12345 paid WAIT 1 5000 (integer) 1WAIT 1 5000的意思是阻塞当前客户端最多 5000 毫秒等待刚才的写命令被至少 1 个从库确认。如果指定时间内达到目标返回实际确认的从库数如果超时通常返回的数字小于期望值。业务可以依据这个返回值决定是否把操作视为成功。脑裂场景下旧主已经和所有从库断开WAIT根本凑不满从库确认数它会超时或失败。这样即便min-replicas因为某些极端情况没有拦住写入WAIT也会把最后一道关守住拿不到足够副本确认就不要向业务报成功。但也要清醒认识WAIT的局限。它等待的是“任意从库”的确认而不是“法定多数从库”的确认它保证的是写入已经到达 N 个从库但不能保证从库不会转主、不会丢数据。它只是缩小了故障窗口并不能彻底实现强一致。所以我一般只建议在关键业务路径上对写操作使用WAIT而不是全局无脑开启毕竟每次写都要阻塞等待延迟代价是很真实的。4.3 第三拳客户端别再把固定 IP 当“永远的主库”这一拳往往被忽视但很多脑裂事故的后果恰恰是被“客户端直连固定主库 IP”这个小设计放大到了不可收拾。主从切换后如果客户端还执着地连旧主就等于持续给一台注定要被清空的机器喂数据。正确做法是让客户端具备“发现主库”的能力Sentinel 模式客户端连接地址指向 Sentinel 列表通过命令获取当前主库地址并订阅 master 切换事件。常见的 Jedis Sentinel、Lettuce 都支持。代理模式如果使用 Codis、Twemproxy 之类代理要确保代理层本身能感知后端拓扑变化。Cluster 模式JedisCluster、Lettuce Cluster 等客户端会在节点重定向时更新拓扑缓存。如果因为历史包袱一时改不了客户端至少要在故障期间快速执行人工切换把写流量摘掉确认新主状态后再恢复业务。短期可以靠监控辅助但长期看固定 IP 直连主库几乎是必然踩坑的架构债。5. Sentinel 与 Redis Cluster不同架构下的脑裂防守主从加 Sentinel 不是唯一的 Redis 高可用方案不少人也在用 Redis Cluster。两种架构下脑裂的表现和防守重点其实不太一样。5.1 Sentinel 参数调优切换太快和太慢都不是好事Sentinel 的很多参数直接影响脑裂窗口的长度和发生概率。最有代表性的三个down-after-millisecondsSentinel 判定节点主观下线的时间阈值。设得过短比如 1 秒网络一抖动就会频繁误判白白触发不必要的故障转移设得过长主库真挂了业务又要等很久才切换。个人经验是先看监控里的 P99 网络时延一般设 5000 到 10000 毫秒比较稳妥。quorum客观下线所需的同意票数。建议大于 Sentinel 总数的一半比如 3 个 Sentinel 就设 2避免单个 Sentinel 误判直接造成切换。failover-timeout故障转移超时控制从选举到完成切换的上限。太小会让切换在复杂的网络状况下反复失败重试太大则会让脑裂窗口变长。不过必须强调Sentinel 参数调得再好也只能“减少因为误判而触发的不必要切换”。对于真实网络分区Sentinel 的切换行为是正确且必要的它带来的脑裂窗口仍然只能靠min-replicas、WAIT和客户端感知去兜底。5.2 Redis Cluster 的 epoch 机制如何压制脑裂Redis Cluster 没有 Sentinel节点之间的可用性协商靠 Gossip 协议和配置纪元configEpoch。当一个主库失联它的从库会发起新的主库选举胜出后拿到的configEpoch会大于旧主。网络分区恢复后旧主发现新主的配置纪元比自己新会立刻承认对方身份放弃自己的主库地位并转为从库。这套机制让 Cluster 在“旧主复活”的瞬间能更快收敛到单一主库从配置层面压制了长期双主的存在。但有个坑要记住Cluster 同样存在脑裂窗口。在分区期间访问旧主的客户端仍然可能写入成功这部分写入在收敛后一样会被新主的数据覆盖掉。所以 Cluster 模式不等于对脑裂免疫min-replicas这类参数依然有配置价值。5.3 监控指标与告警配置很多脑裂事故不是发生了才被发现的而是发生了之后过了很久通过业务反馈才倒查出来。监控体系应该把这些指标看死master_link_status主从复制链路状态一旦down立即告警。master_repl_offset和slave_repl_offset两者差值持续拉大说明复制延迟异常。connected_slaves从库数量小于min-replicas-to-write时触发告警。Sentinel 事件日志中的switch-master一出现就必须人工介入确认。网络层指标跨机房专线的丢包率、TCP 重传率。告警规则不能只停留在“Redis 进程挂了”更需要关注“主库写多副本少”这类状态漂移。等到业务反馈才去翻日志脑裂窗口早就过去了。6. 模拟故障与复盘把脑裂演练成肌肉记忆理论讲再多不如亲手把网络分区制造一次。只有亲眼看到旧主被降到只读、新主接管、然后旧主数据被清空的全过程运维人员才会真正对脑裂产生敬畏。6.1 用 iptables 模拟网络分区最简单的办法是在主库机器上屏蔽掉特定从库或 Sentinel 的 IP。比如我在测试环境模拟“主库与 B 机房从库失联”# 在主库上屏蔽从库方向的包 iptables -A INPUT -s 10.0.0.10 -j DROP iptables -A OUTPUT -d 10.0.0.10 -j DROP # 恢复网络 iptables -D INPUT -s 10.0.0.10 -j DROP iptables -D OUTPUT -d 10.0.0.10 -j DROP注意一定要 INPUT 和 OUTPUT 两个方向都屏蔽否则做不到“完全失联”的效果。如果想模拟更真实的双向分区可以在主库和从库两侧同时加上规则。执行屏蔽后观察三个地方Sentinel 日志是否出现sdown、odown、switch-master新主是否正常接管旧主在当前视角下是否还能收到写入。如果已经配置了min-replicas-to-write 1在旧主上执行SET会直接收到拒绝响应——这就是防线生效的直接证据。6.2 验证配置是否真的兜住了模拟实验里要特别验证三件事脑裂窗口内旧主是否拒绝写入。如果拒绝说明min-replicas生效。对启用了WAIT的调用方超时后是否有正确的降级或重试逻辑。Sentinel 完成切换后客户端是否能在几秒内感知到新主并切换写入目标。这三个实验做下来你的架构才算真正经过了一次“脑裂演习”。很多团队故障预案写得漂亮但压根没在真实网络异常下跑过真出事时手忙脚乱原因就在这里。6.3 兜底的对账补偿方案即使防线都做了我依然建议准备一套兜底方案。脑裂丢失数据后最忌讳的操作是试图把旧主残留的数据直接导回新主——因为旧主往往已经开始全量同步残存状态不可信。更靠谱的路径是业务侧的补偿。脑裂窗口往往可以精确到分钟甚至秒通过对比这段窗口内的业务日志、消息队列、数据库流水重新生成丢失的写操作。比如订单系统可以依据支付回调重新投递事件计数系统可以根据上游埋点重新统计。Redis 承担的角色越接近“缓存”补偿越轻松越接近“存储”补偿越痛苦。这个定位问题值得在架构评审时想清楚。另外如果公司合规允许给 Redis 开 AOF 并定期把 RDB/AOF 归档到对象存储也是一种事后追查的底牌。AOF 文件哪怕被全量同步清掉之前归档的部分依然可能帮你还原脑裂窗口的部分数据。别把鸡蛋全放在主从复制这一个篮子里。最后分享一点个人心得。那次事故之后我把所有负责的 Redis 集群都过了一遍配置强制要求开启min-replicas-to-write关键业务路径加上WAIT客户端全部切换到 Sentinel 自动发现模式。后来确实又遇到过跨机房网络抖动Sentinel 照常切换但旧主在失联瞬间就拒绝了写入业务几乎没有感知到异常。脑裂这个东西说白了就是一个“你以为写成功了其实数据注定消失”的模型。它不会因为你的 Redis 版本高、机器性能好就自动消失。真正能挡住它的从来不是运气而是那些不讨喜但必要的配置以及对每个细节的认真验证。