Redis集群故障转移实测:PFAIL与FAIL投票机制全解析
发布时间:2026/10/12 17:15:12 锦皓数字建站

凌晨两点被告警电话叫醒第一反应肯定是先打开运维面板。屏幕上那个 Redis 集群确实红了一个主节点宕机状态被标成了 fail业务侧已经开始报 CLUSTERDOWN。当时我脑子里其实一片空白——平时背过的“投票机制”四个字真到现场根本想不起来它到底是怎么跑完的。事后我把这次故障完整复盘了一遍又专门找了一台测试机对 Redis 集群的节点宕机、fail 状态转换和投票机制做了一次完整的实测。下面我把这次实测的完整过程写出来fail 状态到底怎么来的、从节点是怎么靠投票上位的、以及整个过程中会遇到哪些边界问题和坑。如果你也负责 Redis 集群的运维或者打算在生产环境上集群模式这篇复盘值得你耐心看完。1. 先搞清楚一件事fail 状态到底是怎么来的1.1 PFAIL 与 FAIL 是两个完全不同的东西很多文档一上来就讲 PFAIL 和 FAIL但真正遇到故障的时候最让人困惑的恰恰是这两者的区别。集群里每个节点之间并不是靠客户端请求来感知对方是否存活的它们通过 cluster bus 端口周期性地互相发送 Ping/Pong 心跳默认每秒都会交换一次。如果某个节点超过了 cluster-node-timeout 时间还没收到另一个节点的响应它会在自己的本地视图里先把对方标记成 PFAIL表示“疑似下线”。这里的重点是“本地视图”。A 节点觉得 B 节点挂了C 节点可能觉得 B 节点活得好好的。PFAIL 并不是全局结论它只是单个节点的判断。接下来PFAIL 会通过心跳消息里的 gossip 信息不断扩散给其他节点。其他节点收到“A 认为 B 疑似下线”的消息后也会在自己的视图里把 B 标记为 PFAIL。当集群中持有槽位的主节点里超过一半都认为 B 疑似下线B 才会被正式标记为 FAIL。FAIL 是全局统一的状态。一旦某个主节点进入 FAIL集群就会开始处理后续的槽位交接、从节点选举甚至把该节点的路由信息从可用列表里摘除。可以这么理解PFAIL 是“我觉得你死了”FAIL 是“大家都觉得你死了所有人都同意把你从通讯录里删除”。1.2 过半确认的“过半数”到底是谁的半数Redis 集群判断一个节点 FAIL用的是“持有槽位的主节点”的过半同意而不是所有节点的过半。这是很多人看文档时容易忽略的点。也就是说如果你搭了一个 3 主 3 从的集群总共有 6 个节点但真正能参与下线确认的只有 3 个 master。某个 master 挂掉时剩下两个 master 都确认它不可达这个节点就会被判定为 FAIL。为什么不让从节点也参与投票原因很简单从节点是候选人既当运动员又当裁判这套机制没法保证公平。更重要的是只有持有槽位的主节点才真正承担了数据路由和写入责任让责任方做决策比让旁观者做决策更符合集群的一致性诉求。为什么需要过半而不是“一票就能定生死”因为网络抖动太常见了。如果 A 和 B 之间只是偶发丢包A 一票就能把 B 拉黑那集群就会在主节点之间频繁切换客户端路由表会被玩坏数据复制也会跟着一起遭殃。过半确认本质上是一种“见证机制”它要求多个独立节点都确认同一个结论才敢下达死亡判决。这个思想和后面要讲的选举机制是一脉相承的判定死亡需要多数派选出新主也需要多数派。2. 投票机制完整链路从主节点死亡到从节点转正2.1 选举的发起者和投票者分别是谁当持有槽位的主节点被确认进入 FAIL 状态后它的从节点会开始准备发起选举。选举的目的非常朴素队长挂了副队长们要选出一个转正继续服务原来那一批槽位。谁有投票权前面已经说过正常在线、持有槽位的其他主节点。从节点没有投票权已经进入 FAIL 状态的主节点也没有投票权。所以 3 主集群里如果其中一个 master 挂了剩下两个正常 master 就是完整的投票团。如果 5 主集群正常情况下也就是 3 个以上 master 才有资格投票。拉票过程是去中心化的。每个想晋升的从节点会把自己当前看到的 configuration epoch 加 1然后向集群里所有能投票的 master 广播 FAILOVER_AUTH_REQUEST。各 master 收到请求后如果同意就回复 FAILOVER_AUTH_ACK。从节点在超时时间内统计收到的 ACK 数只要拿到足够的票数就宣布自己当选。2.2 票数要求和“一票一纪元”规则多少票算赢严格说是超过当前可以投票的主节点数量的一半。3 主集群里需要 2 票5 主集群里需要 3 票。这本质上是因为 Redis 集群希望“新主”的出现必须得到绝大多数责任节点的认可而不是靠小圈子推举。这里有一个容易混淆的概念configuration epoch也就是配置纪元。每一轮选举都会产生一个更大的 epoch节点通过比较 epoch 大小来识别谁是“新王”。可以把它理解成朝代的年号老配置里的旧 epoch 对应的 master哪怕后来复活了也拿不回原来的地位因为它已经不是被大家认可的最新统治者了。每个投票者在同一个选举纪元里只能投一票。也就是说如果这个 master 已经给某个从节点回复过 ACK那这轮选举里它就不能再给其他人投票了。这一票一纪元的规则保证了即使多个从节点同时竞选也不会出现同一个投票者反复横跳的情况。如果从节点发起的这一轮选举因为票数不足失败它会递增 epoch 再发起新一轮直到成功或者集群状态变化放弃为止。2.3 从节点那么多凭什么是你一个 master 下面挂了多个从节点的时候到底谁有资格参选从节点发起选举时投票的 master 并不是纯粹看谁先来。它会参考两个关键信息一是节点配置里的 replica-priority数字越小优先级越高二是从节点的复制偏移量也就是它的数据距离原 master 有多近。数据完整性在这里比响应速度更重要。如果让一个数据明显落后的从节点上位那么原 master 上已经写入但还没来得及复制过去的数据就会全部丢失业务根本没法接受。所以 Redis 集群的投票逻辑会倾向于支持数据最完整的从节点。实际观察下来如果多个从节点同时拉票票数往往会集中到复制偏移量最大的那个从节点身上。竞选失败的从节点后续怎么办它们不会一直闹情绪而是会重新成为新主节点的从节点继续做数据备份。这也意味着故障转移结束之后集群的主从拓扑会自动重塑不再需要人为干预去把原来的从节点重新挂上去。3. 实测准备3主3从集群搭建与故障演练3.1 为什么拿3主3从当实验台做实测第一件事是选拓扑。我最后选了 3 主 3 从因为这是能完整演示“fail 状态判定 选举”的最小规模3 个 master 可以形成“2 票过半”的投票团每个主节点挂一个从节点能保证主节点死亡之后有候选人顶上。如果你只用 3 个 master、不带从节点的集群做测试你会发现主节点挂了之后只能看到 FAIL永远等不到自动恢复原因很简单没有从节点参选。这种测试适合观察 fail 状态本身但不适合观察投票机制。所以我在后面额外做了一个 3 主无从的实验用来对比“有候选人”和“没候选人”的差距。还有个细节集群创建之后master 和 slave 的分布是自动分配的。Redis 会尽量让主从节点落在不同的端口实例上。测试机资源够的话直接用 6 个独立端口的 redis-server 实例别图省事搞什么多实例混跑否则后续观察集群节点视图的时候会非常混乱。3.2 配置、启动与建集群每个实例需要一份独立配置端口从 7001 到 7006。建议单独建目录cluster-config-file 也要分开避免多个实例互相覆盖节点文件。核心配置如下以 7001 为例port 7001 daemonize yes cluster-enabled yes cluster-config-file nodes-7001.conf cluster-node-timeout 15000 appendonly yes dir /data/redis/7001cluster-node-timeout 默认是 15000 毫秒也就是 15 秒。测试的时候如果不想等太久可以改小比如 5000但生产环境不要轻易改小。这个时间直接影响节点故障判定和选举节奏我后面会细说。启动 6 个实例redis-server /data/redis/7001/redis.conf redis-server /data/redis/7002/redis.conf redis-server /data/redis/7003/redis.conf redis-server /data/redis/7004/redis.conf redis-server /data/redis/7005/redis.conf redis-server /data/redis/7006/redis.conf然后建立集群redis-cli --cluster create 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 --cluster-replicas 1交互过程中会问你是否接受分配方案输入 yes。看到[OK] All 16384 slots covered集群就建好了。这里必须提醒一句每个 redis-server 实例除了监听配置里的 client 端口还会自动在 client 端口加 10000 的位置开 cluster bus 端口。本机测试没什么感觉跨机器部署的时候防火墙和安全组必须把这两个端口都放行否则节点之间永远在打转集群根本建不起来。这是我见过的初学者最常见的坑。3.3 先写一批数据给故障后的验证做准备集群搭好之后别急着 kill。先写一批数据后面才有依据判断故障转移到底有没有把数据保住。我这边直接用一个循环往 7001 写了 10 万个 keyfor i in $(seq 1 100000); do redis-cli -p 7001 SET key:${i} value:${i} /dev/null done用循环写的好处是可控你知道哪些 key 落在哪个 master 上。写完检查一下集群状态redis-cli -p 7002 cluster nodes redis-cli -p 7002 cluster info正常情况下 cluster_state 是 okcluster_slots_pfail 和 cluster_slots_fail 都是 0。动手之前再确认一下 7001 的角色和槽位redis-cli -p 7001 cluster nodes | grep 7001如果 7001 是 master 且不是从节点它就可以当我们这轮测试的“目标炮灰”。4. 实测现场kill 主节点盯死投票全程4.1 开场确认角色后直接 kill -9准备就绪后找一个终端执行 kill。这里必须先说清楚模拟节点宕机有两种常用手段。kill -9 是直接杀死进程效果等同于断电kill -STOP 是把进程暂停进程还活着但不再响应网络适合模拟“假死”。建议先测 kill -9最直观。假设 7001 的 PID 是 12345执行kill -9 12345然后立刻用一个循环查看 7002 视角里 7001 和 7004 的状态while true; do redis-cli -p 7002 cluster nodes | grep -E 7001|7004; sleep 1; done为什么从 7002 看因为 7002 是另一个 master它的视角更接近投票团成员。执行 kill 的瞬间客户端往 7001 写入的命令会立刻报错这很正常。集群的反应不会立即出现需要先等节点超时判断跑完。4.2 状态流转时间线PFAIL → FAIL → 选举 → 转正在 cluster-node-timeout 保持默认 15 秒的条件下我的实测时间线大致是这样的0 秒kill 掉 7001。约 15 秒7002 和 7003 开始把 7001 标记为 PFAIL。约 20 到 30 秒PFAIL 通过 gossip 在集群内扩散7002 和 7003 都确认 7001 不可达7001 正式变成 FAIL。又过了几秒7004也就是 7001 的从节点发起选举广播拉票7002 和 7003 都投了赞成票7004 拿到 2 票超过可投票主节点数的一半升为新 master。槽位 0-5460 由 7004 接管cluster_state 从 fail 回到 ok。在连续执行 cluster nodes 时你会看到 7001 那一行的 flags 字段先变成master,fail然后过一会儿7004 那行从slave变成master。输出大致长这样# 故障前 6f4...7001 127.0.0.1:700117001 master - 0 0 1 connected 0-5460 6f4...7004 127.0.0.1:700417004 slave 6f4...7001 0 0 4 connected # 故障中 6f4...7001 127.0.0.1:700117001 master,fail - 0 0 1 disconnected 6f4...7004 127.0.0.1:700417004 slave 6f4...7001 0 0 4 connected # 故障转移完成 6f4...7001 127.0.0.1:700117001 master,fail - 0 0 1 disconnected 6f4...7004 127.0.0.1:700417004 master - 0 0 7 connected 0-5460期间如果你把日志打开7004 的日志里能看到类似下面的关键字Started a failover election for epoch 7 Failover election won with 2 votes不同版本日志格式略有差别但看到类似内容就能确认投票机制确实跑了一遍。4.3 用数据验证读得到老数据、写得了新数据状态变了不代表数据没问题。用集群模式连到 7002 去读之前写的 keyredis-cli -c -p 7002 GET key:50000正常情况下能读到 value:50000。再写一个全新 keyredis-cli -c -p 7002 SET new:after:failover 1同样能成功。这说明 7004 接管后原 7001 的数据没有丢新的写入也落到了新 master 上。再看一次 cluster infocluster_state:ok cluster_slots_assigned:16384 cluster_slots_ok:16384 cluster_slots_pfail:0 cluster_slots_fail:0 cluster_size:3虽然原 master 7001 还是 fail 状态但整个集群的 slot 覆盖已经完整cluster_state 恢复成 ok。这就是 Redis 集群最重要的自愈表现。如果此时把原来的 7001 进程重新启动kill -9 后直接再执行 redis-server你会发现它会自动以 slave 身份加入集群成为 7004 的从节点。原因是它的 slot 已经被 7004 接管重启后通过学习新的 configuration epoch 自动选择当新 master 的备机。这一步值得亲眼确认因为它解释了为什么故障演练之后不需要人工去清理旧节点。5. 实测延伸那些“踩雷”场景与关键参数5.1 没有从节点兜底只见 FAIL 不见恢复我额外做了一个 3 主无从的集群实验。把任意一个 master kill 掉cluster_state 立刻变成 fail而且不会自动恢复。原因很简单主节点 FAIL 之后没有任何从节点发起选举槽位就一直无人接管。配合 cluster-require-full-coverage 默认 yes整个集群会拒绝所有请求连和故障 slot 无关的 key 都读不了。这种场景并不罕见生产环境里经常有人图省事3 个节点裸奔或者从节点被运维人员无意间下线。一旦出现这种情况只能人工介入要么重启故障节点要么手工迁移 slot。故障窗口完全取决于人的反应速度Redis 的自动机制帮不上任何忙。所以我的建议很直接在条件允许的前提下一个 master 至少挂一个从节点。哪怕从节点配置不高至少能在主节点故障时先把服务顶起来再让运维慢慢决定怎么降级或扩容。没有候选人的集群所谓的高可用其实只是自欺欺人。5.2 网络分区场景投票机制如何拦住脑裂投票机制最能体现价值的地方其实是网络分区而不是单节点宕机。假设 3 主 3 从集群被切成了两个分区分区 A 有 2 个 master各自带 1 个从节点分区 B 只有 1 个 master 和它的从节点。如果 B 区的 master 在分区期间又不可达B 区的从节点能选出新主吗不能。B 区里能投票的 master 只有 1 个从节点就算发起选举最多也只能拿到 1 票而需要过半票数至少是 2 票选举必然失败。没有新主产生就不会出现“两个 master 同时声称自己拥有同一批 slot”的经典脑裂。不过这里要把话说完整投票机制保证的是“不让少数派分区选出新主”并不等于 Redis 集群在少数派分区里完全拒绝写入。如果把 cluster-require-full-coverage 设成 no旧 master 独立存活时仍可以继续服务自己名下的 slot分区恢复后可能产生数据不一致甚至丢更新。这种情况不能全怪投票机制而是去中心化集群在“既保证可用性又保证强一致”之间的天然取舍。5.3 cluster-require-full-coverage故障时全挂还是部分挂这个参数直接决定集群出现 slot 覆盖不全时的表现。默认值是 yes意思是只要存在任意一个 slot 没有可用节点负责整个集群对外统一报 CLUSTERDOWN。好处是绝对不会有半残服务坏处是故障影响被放大器一样传遍整个集群。设成 no 之后集群允许部分 slot 缺失的状态下继续服务其他正常 slot 照常读写。本质上是把故障范围缩小到故障槽位周边。它适合可用性优先级高于强一致性的业务但你必须清楚业务读到旧数据、写入暂时无法路由到故障 slot这些都是代价。实测时可以在运行中修改redis-cli -p 7002 CONFIG SET cluster-require-full-coverage no改成 no 之后再 kill 一个 master你会看到 cluster_state 虽然标记为 fail但访问其他健康 slot 的 key 仍然可以正常读写。这对理解“为什么 cluster_state 是 fail 但业务好像没挂”非常有帮助。6. 常见问题与排查技巧实录6.1 为什么一直 PFAIL迟迟不变成 FAILPFAIL 只是单个节点的视角要变成 FAIL 必须满足两个条件消息扩散出去并且足够多的可投票 master 都认可。常见原因是 cluster-node-timeout 设置过大、节点之间确实存在间歇性丢包、或者 gossip 消息被网络拥塞拖慢。排查的时候不要只看自己的主节点而要分别登录几台正常的 master各自执行 cluster nodes 查看同一个故障节点的状态。如果只有其中一台把它标成 PFAIL说明是局部网络问题如果多数节点都标了还是不变 FAIL那要怀疑超时确认周期还没走完或者 gossip 传播被阻塞。需要特别注意PFAIL 状态在节点恢复网络后会自动清除但没有官方命令可以直接把 PFAIL 强行改成 FAIL。比较实际的做法是恢复网络抖动或者把 cluster-node-timeout 临时调小再调回让超时判断重新跑一遍。6.2 主节点 FAIL 了但从节点就是不升主第一个要排查的是到底有没有从节点。如果没有换个角度排查第二个原因从节点数据落后太多。Redis 集群里的 cluster-replica-validity-factor 参数允许从节点的复制落后程度保持在一个合理范围如果落后太多连参选资格都会被取消。第三个原因是票数不够。比如网络被切成两部分从节点虽然发起了选举但它能联系到的 master 数量不足以凑够多数票。如果你在这个从节点的日志里看到类似 failover election 但结果不是 won 的输出基本可以往这个方向查。还有一种很低级但真实存在的情况你在同一个集群里反复做过手动 FAILOVER 或恢复操作configuration epoch 已经变得很乱。此时从节点看到的投票上下文可能被旧消息干扰。最简单的处理是保证每轮选举的 epoch 递增逻辑正常不要动不动就把节点 CLUSTER RESET 掉重新加群。6.3 手动 FAILOVER 和 TAKEOVER 到底啥区别很多人把 CLUSTER FAILOVER 当成“强制切换”其实它更像是“协商切换”。执行之后当前 master 会主动把 slot 平滑移交给指定的从节点整个过程中从节点会先追平复制偏移量再发起切换几乎不丢命令。它适合计划内维护比如升级主节点、更换机器。如果 master 已经彻底不可达用普通 FAILOVER 是没用的因为协商流程找不到原 master 确认。这时候可以用 CLUSTER FAILOVER TAKEOVER它会让从节点直接跳过协商和投票强制宣布自己成为新 master。这个方法少了很多安全护栏只能在原 master 确实死亡、并且你清楚自己在干什么的时候才用。我的经验是优先让集群自动 failover不要抢跑。只有自动机制确实失败了才考虑 TAKEOVER 兜底。自动 failover 走完了多数派确认和数据完整性检查两道关卡比人工硬切安全得多。6.4 监控别只盯 cluster_state很多监控面板会把 cluster_state 当成唯一的健康指标。但实测下来cluster_state 是全局概要它可能因为某个边缘节点 PFAIL 短时间闪红也可能在 cluster-require-full-coverageno 的情况下已经 fail 但大部分业务还能跑。单看它容易误判。我自己的监控策略是三个维度并列监控维度重点观察内容建议告警条件cluster_state是否处于 ok非 ok 立即告警但需结合其他维度判断cluster_slots_fail / pfail是否存在不可用槽位slots_fail 0 且持续超过设定时间master 节点数量当前存活且承载槽位的主节点数数量低于预期或变化异常时告警故障发生时重点看 slots_fail 有没有归零。如果变成 0 但 cluster_state 还是 fail很可能卡在少数派确认阶段。告警阈值也不要一刀切。核心集群任何 master 宕机都要告警边缘集群可以接受几十秒的自动 failover 窗口只要在 2 分钟内没有恢复再升级。这个阈值需要结合 cluster-node-timeout 来设计别让告警自己先炸了。最后再分享一点实测体会实测跑完一圈我自己最大的感受是投票机制不是 Redis 集群的附加功能它就是这个集群在故障时能不能自愈的地基。以前看文档总觉得概念很抽象真正拿着 kill 命令杀掉一个 master盯着 cluster nodes 的输出变化才理解 PFAIL、FAIL、选举窗口这些词到底在说什么。现在如果再接到凌晨的告警我看一眼 cluster nodes 和 cluster info基本就能判断出是网络分区、节点假死还是没有从节点兜底。最后补一个实践建议测试环境里可以放心把 cluster-node-timeout 调小来压缩时间但生产环境请先尊重默认的 15 秒——如果你把网络抖动也当成宕机来处理投票机制会帮你选出一堆新主然后你会在数据回滚里度过更多不眠之夜。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。