一次Redis Cluster脑裂故障的完整复盘:从网络分区检测到数据一致性修复

发布时间:2026/7/8 13:49:27
一次Redis Cluster脑裂故障的完整复盘:从网络分区检测到数据一致性修复 一次Redis Cluster脑裂故障的完整复盘从网络分区检测到数据一致性修复一、故障现象与初步影响评估某日凌晨3:17监控系统告警订单服务P99延迟从50ms飙升至5s。同时出现Redis连接超时。错误日志显示READONLY You cant write against a read only replica。但更奇怪的是。两个不同的Redis Cluster节点同时声称自己是Master。脑裂(Split-Brain)已经发生。故障影响范围如表所示。订单创建接口成功率降至67%。购物车读写全部失败。用户会话信息丢失导致强制重新登录。支付回调写入失败引发重复扣款风险。持续时间约48分钟。直接影响的业务交易金额约120万元。应急响应按三步执行。第一步隔离问题集群。通过防火墙规则将故障集群从流量中摘除。防止写入冲突导致数据进一步损坏。第二步切换到备用Redis集群。验证备用集群数据完整性后切流。业务逐步恢复。第三步进行故障排查。导出故障集群的日志、快照、集群拓扑信息。开始根因分析。复盘的目标不是追责。而是找到系统的薄弱环节。防止同类故障再次发生。从设计、监控、应急三个层面寻找改进点。sequenceDiagram participant A as 主数据中心 participant N as 网络设备 participant B as 备数据中心 participant M1 as Redis Master-1 participant M2 as Redis Master-2 participant C as 哨兵集群 Note over A,B: 正常状态: 主数据中心持有Master M1-C: 心跳正常(每2s) C-M2: 哨兵确认角色为Slave Note over N: 03:17 - 网络链路故障 N--xA: 主数据中心断连 N--xB: 备数据中心断连 C-M2: 哨兵超时(30s无响应) Note over C: 03:17:35 哨兵发起故障转移 C-M2: 提升为新Master M2--M2: 角色切换: Slave → Master Note over N: 03:18 - 网络链路恢复 N--A: 主数据中心恢复 N--B: 备数据中心恢复 Note over M1,M2: 脑裂状态: 两个Master同时接收写入 M1-M1: 继续处理写入请求 M2-M2: 也处理新写入请求 Note over M1,M2: 数据分歧: 两个Master持有不同版本数据二、网络分区检测与诊断过程网络分区(Network Partition)是脑裂的直接原因。当集群节点之间的网络通信中断。各分区内的节点无法相互感知。各自认为自己是集群的唯一存活着。诊断从网络层开始。第一步检查节点间的连通性。使用redis-cli cluster nodes对比各节点视角的集群拓扑。在正常状态下。所有节点看到的是同一个拓扑。脑裂状态下。不同分区的节点看到的拓扑完全不同。节点A认为B已下线。节点B认为A已下线。第二步分析哨兵日志。Redis Sentinel是脑裂检测的关键组件。查看每台哨兵节点的日志。定位故障转移(Failover)的精确时间。从日志中发现关键的时序03:17:00 最后一个正常心跳03:17:35 哨兵主观下线(sdown)判定03:17:40 哨兵客观下线(odown)判定03:17:42 新Master选举完成03:17:48 旧Master的网络恢复第三步分析网络链路。检查机房之间的专线日志。发现03:17-03:18期间。两机房之间的交换机发生STP(Spanning Tree Protocol)重计算。导致约55秒的链路中断。但数据中心内部网络正常。这种部分网络分区最危险。因为Sentinel无法区分Master真的挂了和只是网络不通了。#!/bin/bash # Redis Cluster脑裂诊断脚本 # 用于事故后现场信息收集 REDIS_CLIredis-cli OUTPUT_DIR/tmp/redis-splitbrain-$(date %Y%m%d_%H%M%S) mkdir -p $OUTPUT_DIR # 节点列表(按实际环境填写) NODES( 10.0.1.1:6379 10.0.1.2:6379 10.0.1.3:6379 10.0.2.1:6379 10.0.2.2:6379 10.0.2.3:6379 ) echo 开始诊断信息收集 $(date) for node in ${NODES[]}; do host${node%:*} port${node#*:} node_id${host}_${port} echo --- 收集节点: $host:$port --- # 1. 集群拓扑视角 $REDIS_CLI -h $host -p $port \ --cluster-timeout 5000 \ cluster nodes $OUTPUT_DIR/${node_id}_nodes.txt 21 # 2. 集群信息 $REDIS_CLI -h $host -p $port \ cluster info $OUTPUT_DIR/${node_id}_info.txt 21 # 3. 慢查询日志(分析故障期间的异常操作) $REDIS_CLI -h $host -p $port \ slowlog get 128 $OUTPUT_DIR/${node_id}_slowlog.txt 21 # 4. 角色信息 $REDIS_CLI -h $host -p $port \ role $OUTPUT_DIR/${node_id}_role.txt 21 # 5. 复制偏移量(判断数据分歧程度) $REDIS_CLI -h $host -p $port \ info replication | grep -E master_repl_offset|slave_repl_offset \ $OUTPUT_DIR/${node_id}_replication.txt 21 # 6. 键数量差异(快速评估数据丢失量) $REDIS_CLI -h $host -p $port \ dbsize $OUTPUT_DIR/${node_id}_dbsize.txt 21 echo 收集完成 done # 7. 哨兵日志收集 echo --- 收集哨兵日志 --- for sentinel in ${NODES[]}; do host${sentinel%:*} port26379 # 哨兵默认端口 $REDIS_CLI -h $host -p $port \ sentinel masters $OUTPUT_DIR/sentinel_${host}_masters.txt 21 $REDIS_CLI -h $host -p $port \ sentinel sentinels mymaster $OUTPUT_DIR/sentinel_${host}_peers.txt 21 done # 8. 分析集群拓扑一致性 echo echo 集群拓扑一致性检查 # 提取各节点的Master列表检查是否一致 prev_masters consistenttrue for node in ${NODES[]}; do host${node%:*} node_id${host}_6379 file$OUTPUT_DIR/${node_id}_nodes.txt if [ -f $file ]; then masters$(grep master $file | awk {print $1} | sort) if [ -z $prev_masters ]; then prev_masters$masters elif [ $masters ! $prev_masters ]; then echo [失败] 节点 $host 的拓扑与其他节点不一致! consistentfalse fi fi done if $consistent; then echo [通过] 所有节点集群拓扑一致 fi echo echo 诊断信息已保存到: $OUTPUT_DIR echo 诊断完成 $(date) 三、数据一致性评估与修复方案脑裂期间两个Master并行写入。数据产生分歧。需要评估数据不一致的程度。然后选择修复方案。数据分歧评估。第一步计算两个Master的键数量差异。DBSIZE显示写入量差异。第二步采样对比键值一致性。在两个Master上执行SCAN遍历采样部分键。对比相同键的值是否一致。第三步分析业务后果。根据日志分析哪些键在脑裂期间被修改。评估业务数据损坏的范围。本案例中。旧Master在56秒内接收了约2300个写入。新Master在42秒内接收了约1800个写入。其中约400个键在两边都被修改。值不同。另有1900个键仅写入旧Master。1400个键仅写入新Master。不处理直接合并会产生严重的业务数据问题。修复方案选择。有三种修复方案。方案A全量回滚。放弃脑裂期间的所有写入。恢复到脑裂前的数据快照。优点是最安全。缺点是数据丢失量大。适用于金融类或无法容忍数据不一致的场景。方案B基于时间戳的合并。对比同一键的修改时间。保留最新的修改版本。适用场景有限。要求业务对数据时序性有要求。方案C业务日志回放。从业务系统的binlog或事件日志中重放操作。确保业务逻辑的完整性。最精确但实现复杂。本案例采用方案C。因为订单和支付数据必须保证一致性。操作流程为从MySQL binlog提取脑裂期间的写入记录。按时间顺序在修复后的集群上重放。验证每笔操作的结果与原集群一致。#!/usr/bin/env python3 Redis Cluster脑裂后数据一致性检查与修复工具 import redis from redis.cluster import RedisCluster from typing import List, Dict, Set, Tuple from collections import defaultdict import hashlib import time class SplitBrainFixer: 脑裂后数据修复工具 def __init__(self, node_a_coord, node_b_coord): 连接两个分区(两个Master) try: self.cluster_a RedisCluster.from_url( fredis://{node_a_coord}, decode_responsesTrue ) self.cluster_b RedisCluster.from_url( fredis://{node_b_coord}, decode_responsesTrue ) except redis.RedisError as e: raise ConnectionError(fRedis连接失败: {e}) def audit_diff(self, sample_size10000) - Dict: 审计两个集群的数据差异 result { only_in_a: [], only_in_b: [], different: [], identical: 0, } # 采样对比全量扫描可能非常耗时 cursor_a 0 scanned 0 try: while scanned sample_size: cursor_a, keys_a self.cluster_a.scan( cursor_a, count1000 ) for key in keys_a: if scanned sample_size: break try: val_a self.cluster_a.get(key) val_b self.cluster_b.get(key) if val_b is None: result[only_in_a].append(key) elif val_a ! val_b: result[different].append({ key: key, val_a: val_a[:200], val_b: val_b[:200] }) else: result[identical] 1 except redis.RedisError as e: print(f读取key {key} 失败: {e}) scanned 1 if cursor_a 0: break except redis.RedisError as e: print(f扫描中断: {e}) # 查找仅存在于B的键(反向扫描采样) try: cursor_b 0 scanned_b 0 while scanned_b sample_size: cursor_b, keys_b self.cluster_b.scan( cursor_b, count1000 ) for key in keys_b: if scanned_b sample_size: break if not self.cluster_a.exists(key): result[only_in_b].append(key) scanned_b 1 if cursor_b 0: break except redis.RedisError as e: print(f反向扫描中断: {e}) return result def merge_with_preference(self, prefer_clustera, dry_runTrue) - Dict: 以指定分区为准进行合并(另一个分区的独有数据被覆盖) source self.cluster_a if prefer_cluster a \ else self.cluster_b target self.cluster_b if prefer_cluster a \ else self.cluster_a stats {merged_keys: 0, skipped_keys: 0, errors: []} cursor 0 while True: try: cursor, keys source.scan(cursor, count1000) except redis.RedisError as e: stats[errors].append(f扫描失败: {e}) break for key in keys: try: value source.get(key) if value is None: stats[skipped_keys] 1 continue if not dry_run: # 使用管道批量写入减少网络往返 pipe target.pipeline() pipe.set(key, value) pipe.execute() stats[merged_keys] 1 except redis.RedisError as e: stats[errors].append( f迁移key {key} 失败: {e} ) if cursor 0: break return stats四、预防措施与架构改进脑裂无法完全避免。分布式系统中的网络分区是CAP定理的体现。但可以通过以下措施降低发生概率和影响范围。配置优化。调整Sentinel的down-after-milliseconds。不建议设置过短(如1s)。网络抖动会导致频繁误判。建议生产环境设置为5000ms-10000ms。增加min-replicas-to-write和min-replicas-max-lag参数。当可用从节点数不足时。Master拒绝写入。避免脑裂期间的单Master写入被业务接收。架构改进。将Sentinel节点部署在三个以上可用区。避免单一网络故障影响多数哨兵。引入redis-failover的外部仲裁机制。哨兵决策前查询外部状态服务确认。防止网络分区导致的错误选举。配置Master节点在Sentinelparallel-syncs合理值。防止故障恢复时的同步风暴。监控增强。新增关键告警指标。主从偏移量差值超过阈值告警。哨兵主观下线事件告警(尤其是单个哨兵的sdown)。集群拓扑变化告警(任何角色变更)。脑裂自动检测对比多节点视角的集群状态。发现不一致立即告警。预案建立。编写脑裂场景的应急操作手册。包含隔离故障集群的精确命令。数据备份与恢复流程。业务侧降级与切换SOP。定期进行脑裂演练。验证预案的可行性和时效性。确保故障发生时运维人员不会手忙脚乱。五、总结本次Redis Cluster脑裂故障的根因是跨数据中心网络链路中断。中断期间Sentinel完成了Master选举。两个Master并行写入导致数据分歧。故障持续时间48分钟。影响交易金额约120万元。关键教训有三。第一Sentinel配置不能过于激进。过短的超时设置会增加误判概率。需要根据网络质量合理配置down-after-milliseconds。第二min-replicas-to-write是防止脑裂写入的有效手段。应该在生产环境中启用。第三监控必须覆盖拓扑一致性检查。仅监控单节点的存活状态不够。需要对比多节点视角的拓扑。改进措施包括将所有Redis节点的min-replicas-to-write设置为1。将Sentinel超时从2s调整为5s。增加跨可用区的Sentinel节点。新增集群拓扑一致性监控告警。建立脑裂应急预案并定期演练。分布式系统中没有不会故障的架构。只有故障后可恢复的架构。正视脑裂的可能性。做好检测、隔离和恢复的充分准备。这才是运维工程师对待故障的正确态度。