Redis Cluster的故障转移:从PFAIL到FAIL再到主从切换的全过程 📅 2026/7/16 18:33:37 Redis Cluster的故障转移从PFAIL到FAIL再到主从切换的全过程一、故障检测的第一道防线Gossip协议与PFAILRedis Cluster的故障检测体系是一个去中心化的分布式系统。没有中心化的监控节点每个节点都在和它周围的节点持续通话。这种通话的核心就是Gossip协议。每秒钟每个节点会随机选取几个节点发送PING消息。PING消息中携带了发送者所知道的集群拓扑信息——包括它认为哪些节点处于疑似故障PFAIL状态。接收者通过PONG消息回应同时也会传播自己的拓扑视图。PFAIL是疑似故障FAIL是确认故障。从PFAIL到FAIL的升级需要多个节点的交叉验证// Redis源码中的PFAIL检测逻辑src/cluster.c 简化版 void clusterCron(void) { // 每100ms执行一次 mstime_t now mstime(); // 遍历所有已知节点 dictIterator *di dictGetSafeIterator(server.cluster-nodes); while ((de dictNext(di)) ! NULL) { clusterNode *node dictGetVal(de); // 跳过自己和握手状态的节点 if (node-flags (CLUSTER_NODE_MYSELF | CLUSTER_NODE_HANDSHAKE)) continue; mstime_t delay now - node-ping_sent; // 超过cluster-node-timeout/2 但未超过timeout发送PING if (delay server.cluster_node_timeout / 2 node-link node-ping_sent 0) { clusterSendPing(node-link, CLUSTERMSG_TYPE_PING); continue; } // 超过cluster-node-timeout标记为PFAIL if (delay server.cluster_node_timeout) { if (!(node-flags (CLUSTER_NODE_PFAIL | CLUSTER_NODE_FAIL))) { serverLog(LL_NOTICE, Node %.40s possibly failing, node-name); node-flags | CLUSTER_NODE_PFAIL; // 更新故障报告时间 node-fail_time now; } } } dictReleaseIterator(di); }二、从PFAIL到FAIL多数派的确认机制当节点A将节点B标记为PFAIL后它会通过Gossip消息将这一判断传播给集群中的其他节点。每个收到PFAIL消息的节点都会独立地去PING节点B进行确认。FAIL升级条件当集群中的大多数Master节点都确认了B的PFAIL状态时某个节点会将B的状态升级为FAIL。// PFAIL到FAIL的升级逻辑src/cluster.c void clusterCron(void) { // ... 紧接上面的PFAIL检测 // 检查是否需要将PFAIL升级为FAIL if (node-flags CLUSTER_NODE_PFAIL !(node-flags CLUSTER_NODE_FAIL)) { int needed_quorum (server.cluster-size / 2) 1; int failures 0; // 统计有多少个Master报告了该节点的PFAIL dictIterator *report_di dictGetSafeIterator(node-fail_reports); while ((report_de dictNext(report_di)) ! NULL) { clusterNodeFailReport *fr dictGetVal(report_de); // 只统计在 cluster-node-timeout * 2 内的报告 if (now - fr-time server.cluster_node_timeout * 2) { failures; } } dictReleaseIterator(report_di); // 满足多数派条件升级为FAIL if (failures needed_quorum) { serverLog(LL_WARNING, Node %.40s is FAIL (quorum reached), node-name); node-flags ~CLUSTER_NODE_PFAIL; node-flags | CLUSTER_NODE_FAIL; node-fail_time now; // 立即通过Gossip广播FAIL消息 clusterSendFail(node-name); } } }cluster-node-timeout配置的关键影响# redis.conf # 默认15000毫秒15秒 # 保守环境网络稳定、硬件可靠 cluster-node-timeout 15000 # 云环境网络抖动较多 cluster-node-timeout 20000 # 高要求环境必须快速检测故障 cluster-node-timeout 5000取值故障检测速度误判风险适用场景5000ms快~15秒完成切换较高同机房、高性能网络15000ms中等~45秒中等大多数场景的默认选择30000ms慢~90秒较低跨地域部署、网络不稳定三、主从切换的选举算法Epoch机制当Master被标记为FAIL后它的Slave节点会发起选举。Redis Cluster使用Raft-like的Epoch机制来保证选举的安全性/** * Redis Cluster故障转移选举算法的Java模拟实现 * 用于理解选举的时序和投票逻辑 */ public class ClusterFailoverElection { private final long currentEpoch; private final long configEpoch; private final String nodeId; private final MapString, ReplicaState replicas; /** * Slave节点发起选举 */ public ElectionResult startElection(String failedMasterId) { // 1. 计算选举延迟 500ms rank * 1000ms // rank基于复制偏移量复制进度最靠前的Slave rank0 int rank calculateReplicationRank(failedMasterId); long electionDelay 500 rank * 1000; // 2. 等待延迟确保数据最新的Slave最先发起 sleep(electionDelay); // 3. 检查是否已经有其他Slave赢得了选举 if (isFailoverInProgress(failedMasterId)) { return ElectionResult.lost(已有其他Slave接管); } // 4. 递增currentEpoch并发起投票请求 long candidateEpoch incrementAndGetCurrentEpoch(); // 5. 向所有Master节点索要投票 int votes 0; int requiredVotes (getActiveMasters() / 2) 1; for (MasterNode master : getActiveMasters()) { VoteResponse response master.requestVote( new VoteRequest( candidateEpoch, // 候选者epoch nodeId, // 候选者ID failedMasterId, // 故障Master的ID getLastReplOffset() // 候选者的复制偏移量 ) ); if (response.isGranted()) { votes; } } // 6. 判断是否获得多数票 if (votes requiredVotes) { // 设置configEpoch为最大的currentEpoch configEpoch candidateEpoch; // 发起配置变更将自己提升为Master promoteToMaster(failedMasterId); return ElectionResult.won(); } return ElectionResult.lost(得票不足: votes / requiredVotes); } /** * Master节点的投票逻辑 */ public VoteResponse handleVoteRequest(VoteRequest req) { // 投票条件判断 // 1. 在当前term内尚未投票 if (votedInEpoch req.candidateEpoch) { return VoteResponse.reject(当前epoch已投票); } // 2. 故障Master确实被标记为FAIL if (!isNodeFailed(req.failedMasterId)) { return VoteResponse.reject(故障Master未被确认); } // 3. 候选者拥有足够的复制数据 if (req.lastReplOffset getReplOffset(req.failedMasterId)) { return VoteResponse.reject(候选者复制偏移量不足); } // 通过所有检查投票 votedInEpoch req.candidateEpoch; votedFor req.candidateId; return VoteResponse.grant(); } }四、脑裂的预防脑裂是分布式系统中最危险的故障模式。Redis Cluster通过以下机制来预防# 关键配置防止脑裂后数据丢失 # 1. min-replicas-to-write: Master至少需要N个Slave才接受写入 # 当网络分区导致Master看不到Slave时拒绝写入而非冒险 min-replicas-to-write 1 # 2. min-replicas-max-lag: Slave的复制延迟不超过N秒 # 两个条件同时满足才接受写入 min-replicas-max-lag 10 # 3. 配合 cluster-require-full-coverage 使用 # 当任一slot不可用时拒绝请求默认yes cluster-require-full-coverage yes/** * 脑裂预防的客户端实现确认Master合法性 */ public class SplitBrainPrevention { /** * 写入前的安全检查 */ public boolean isSafeToWrite(RedisClusterNode master) { // 1. 检查 min-replicas-to-write 条件 int connectedReplicas master.getConnectedReplicasCount(); if (connectedReplicas master.getMinReplicasToWrite()) { logger.warn(Master {} 只有 {} 个Slave连接低于要求 {}拒绝写入, master.getId(), connectedReplicas, master.getMinReplicasToWrite()); return false; } // 2. 检查Slave的复制延迟 for (ReplicaNode replica : master.getReplicas()) { long lag System.currentTimeMillis() - replica.getLastAckTime(); if (lag master.getMinReplicasMaxLag() * 1000L) { logger.warn(Slave {} 复制延迟 {}ms 超过阈值, replica.getId(), lag); return false; } } // 3. 检查 cluster-require-full-coverage 条件 if (master.getClusterState() ! ClusterState.OK) { logger.warn(集群状态异常: {}, master.getClusterState()); return false; } return true; } }故障转移全流程时间预算分析阶段耗时说明PFAIL检测cluster-node-timeout等待PING超时FAIL确认cluster-node-timeout × 2收集多数派确认选举延迟500ms rank × 1000ms数据最新的Slave优先投票收集~200ms向各Master索要投票配置广播~500ms新Master广播配置变更总计约3×timeout 2stimeout15s时约47s五、总结Redis Cluster的故障转移是一条精密的自动化流水线Gossip协议以去中心化的方式在节点间传播拓扑和故障信息每秒随机PING实现O(1)的通信复杂度PFAIL→FAIL的多数派升级需要超过半数的Master独立确认有效防止单节点误判导致的错误切换基于Epoch的选举算法保证在同一个term内只有一个Slave能获得多数票避免双写min-replicas配置在网络分区时主动拒绝写入是脑裂的最后一道防线理解这条流水线不仅有助于故障排查更重要的是在做cluster-node-timeout等配置调优时能准确评估每次调整对故障检测速度和系统稳定性的双重影响。