1. 从一次凌晨告警说起:Redis Cluster 故障转移到底在做什么
凌晨两点十七分,手机开始疯狂震动。监控面板上,某个业务集群的写入延迟从 2ms 飙到 800ms,紧接着一连串CLUSTERDOWN报错刷屏。我爬起来连上跳板机,cluster info一看——三个主节点里有一个已经标记为fail,但从节点迟迟没有接管,整个哈希槽区间处于不可用状态。那次事故让我彻底意识到:Redis Cluster 的故障转移不是"配好了就自动生效"的魔法,它背后有一整套投票、选举、纪元推进的机制,任何一个环节配置不当,都会让高可用变成纸面文章。
这篇文章我想把 Redis Cluster 故障转移这件事从头到尾讲透。它是什么?简单说,就是当集群中某个主节点宕机后,它的从节点通过一套分布式协商机制自动升级为主节点,接管原来负责的哈希槽,让整个集群继续对外提供服务。能解决什么问题?解决单点故障导致的写入中断、数据不可用。适合谁看?正在用或准备用 Redis Cluster 做缓存/存储的运维、后端开发,以及被"集群为什么没自动切换"折磨过的同行。
我不会只贴几行配置了事。下面会拆解故障转移的完整链路:主观下线怎么判定、客观下线怎么达成、从节点怎么拉票、主节点怎么投票、新主怎么接管槽位、旧主回来怎么办。每一环我都会说清楚"为什么这么设计"以及"实际踩过什么坑"。看完你应该能自己排查"为什么我的集群没切换"这类问题,而不是干等着它自己好。
2. 故障转移的底层逻辑:先搞懂 Cluster 的"权力结构"
2.1 哈希槽、节点角色与集群总线
Redis Cluster 把整个键空间划分成 16384 个哈希槽(slot),每个主节点负责其中一部分槽。键通过CRC16(key) mod 16384定位到具体槽,再找到负责该槽的节点。从节点默认只做数据复制,不参与读写(除非开启READONLY做只读从库读)。
节点之间靠Cluster Bus通信,这是一个独立的二进制协议端口,默认是客户端端口 + 10000。比如客户端端口 7000,总线端口就是 17000。这个细节非常关键——很多"集群状态异常但节点都活着"的问题,根源就是总线端口被防火墙拦了。总线负责 gossip 消息传播、故障检测、配置纪元(config epoch)同步、投票选举。可以说,故障转移的全部协商都跑在这条总线上。
提示:部署时务必确认
port + 10000的连通性,云环境安全组、容器网络、K8s NetworkPolicy 都要放行。我见过太多案例是客户端端口通、总线端口不通,结果集群永远处于fail状态却不自知。
2.2 为什么是"去中心化投票"而不是"选主哨兵"
很多人第一反应是:Redis 不是有 Sentinel 吗,为什么 Cluster 不用 Sentinel 那套?原因是架构目标不同。Sentinel 是独立进程,监控的是普通主从;而 Cluster 本身就是多主多从的分布式结构,节点数量动态变化,如果再加一层外部哨兵,部署复杂度和一致性风险都会上升。
Cluster 选择让主节点自己充当"投票人",从节点充当"候选人",通过 gossip 协议在集群内部完成选举。这套机制的好处是没有外部依赖,坏处是——它依赖"多数主节点存活"这个前提。一旦超过半数的主节点挂掉,集群就无法完成任何选举,直接进入不可用。这就是为什么 Cluster 的规模规划里,主节点数量最好是奇数,且要保证故障域分散。
2.3 故障转移的完整链路概览
在深入细节前,先用一张表把整个流程的六个阶段串起来,后面每一节都会展开:
| 阶段 | 触发条件 | 关键动作 | 涉及参数 |
|---|---|---|---|
| 主观下线 PFAIL | 单节点心跳超时 | 标记对方为疑似故障 | cluster-node-timeout |
| 客观下线 FAIL | 多数主节点确认 | 广播 FAIL 消息 | 需多数主节点达成一致 |
| 从节点发起选举 | 主节点 FAIL 且从节点数据足够新 | 递增 currentEpoch 拉票 | cluster-replica-validity-factor |
| 主节点投票 | 收到拉票请求 | 每个纪元只投一票 | 需获得多数主节点票 |
| 新主接管槽位 | 得票过半 | 提升自己为主,接管槽 | configEpoch 更新 |
| 旧主回归 | 原主恢复 | 降级为从节点 | 自动跟随新主 |
这张表建议收藏,排查问题时按阶段对号入座,能快速定位卡在哪一步。
3. 主观下线与客观下线:故障判定不是"一断就切"
3.1 心跳机制与 cluster-node-timeout 的真实含义
每个节点会定期向其他节点发送PING,收到PONG就算活着。如果超过cluster-node-timeout(默认 15000ms)没收到有效响应,就标记对方为PFAIL(主观下线)。注意"主观"两个字——这只是单个节点的判断,不代表集群共识。
这里有个常见误解:很多人以为把cluster-node-timeout调小就能"更快切换"。理论上是的,但代价是误判率飙升。网络抖动、GC 停顿、慢查询阻塞都可能导致短暂无响应。timeout 设成 1 秒,一次 Full GC 就可能触发一轮不必要的选举,反而制造抖动。
我的经验值:内网环境 5~10 秒,跨机房或云环境 15~30 秒。判断依据是"你的正常网络 P99 延迟 + 最大 GC 停顿"要明显小于 timeout。如果业务对切换时间极度敏感,与其压 timeout,不如优化从节点数量和网络质量。
3.2 从 PFAIL 到 FAIL:多数派确认的数学
单个节点标记 PFAIL 后,会通过 gossip 把"我认为 X 挂了"这个消息传播出去。当负责处理槽位的主节点中,超过半数都认为 X 处于 PFAIL 或 FAIL 状态时,某个节点就会把 X 标记为FAIL(客观下线),并广播一条 FAIL 消息,强制所有节点接受。
这里的"多数"是主节点总数的多数,不是所有节点的多数。假设有 3 个主节点,需要 2 个确认;5 个主节点需要 3 个;如果有 6 个主节点,需要 4 个(超过半数)。这就引出一个致命约束:
注意:如果挂掉的主节点数量达到或超过半数,剩余主节点无法凑齐多数票,故障转移永远不会触发。比如 3 主 3 从的集群,挂掉 2 个主,集群直接不可用,从节点再多也没用。
所以集群规划时,主节点数量决定了容错上限。3 主最多容忍 1 个主挂,5 主最多容忍 2 个。想要更高容错,就得加主节点,而不是加从节点。
3.3 一个容易忽略的细节:FAIL 消息的传播与纪元
FAIL 消息带有configEpoch信息。节点收到 FAIL 消息后,如果消息里的 epoch 比自己记录的新,就更新本地状态。这套机制保证了故障判定在集群内的一致性——不会出现"A 认为 X 挂了,B 还认为 X 活着"的长期分裂。
但这里有个坑:如果网络分区导致集群被切成两半,少数派那一侧的主节点会被多数派标记为 FAIL,然后它们的从节点会尝试发起选举。如果少数派侧的主节点还活着,它会拒绝投票(因为它认为自己的主还在),选举就会失败。这正是 Cluster 保证一致性的方式——宁可少数派不可用,也不产生脑裂双主。
4. 从节点选举:一场需要多数票的"竞选"
4.1 选举资格:数据够新才有资格参选
主节点被标记 FAIL 后,它的从节点不会立刻全部冲上去抢。每个从节点会先做一次资格自检,核心是判断自己的数据是否"足够新"。判断依据是:
(now - master_last_io_seconds_ago) > cluster-node-timeout * cluster-replica-validity-factor如果从节点与主节点最后一次数据交互距今太久(超过 timeout 乘以 validity-factor),就认为数据太旧,放弃参选。cluster-replica-validity-factor默认是 10,意味着从节点最多容忍 10 倍 timeout 的数据延迟。
这个参数的设计意图是防止数据严重滞后的从节点被选为新主,造成数据大量丢失。但实际运维中,默认值 10 往往太宽松。我一般会把它调到2~5,配合合理的 timeout,让数据最新的从节点优先当选。如果设成 0,则表示不做数据新鲜度检查,任何从节点都能参选——除非你完全不在乎数据一致性,否则不建议。
4.2 拉票过程:currentEpoch 递增与投票规则
有资格的从节点会发起选举,流程是这样的:
- 从节点把自己的
currentEpoch加一,进入新的选举纪元。 - 向所有主节点广播
FAILOVER_AUTH_REQUEST请求投票。 - 主节点收到请求后,检查:这个纪元我投过票没有?请求的 epoch 是否比我记录的新?发起者是不是我负责的槽对应的从节点?
- 满足条件就回
FAILOVER_AUTH_ACK,并记录"本纪元已投票"。 - 从节点统计票数,拿到多数主节点的票就当选。
关键规则:每个主节点在每个 epoch 只能投一票。这防止了同一纪元内多个从节点同时当选。如果两个从节点同时拉票,先到的拿到票,后到的只能等下一个 epoch 重试。
4.3 为什么会有"选举延迟"和"重试"
从节点发起选举不是立即的,而是有一个延迟窗口,计算方式是:
DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms其中SLAVE_RANK是数据新鲜度排名,数据最新的 rank 为 0。这个设计非常巧妙:数据最新的从节点延迟最短,最先发起选举,最可能当选。随机部分则避免了多个同 rank 从节点同时拉票导致的分票。
如果第一轮选举失败(比如票数不够),从节点会等待一个随机时间后重试,直到成功或主节点恢复。这就是为什么有时候你会看到"主挂了十几秒才切换"——可能前几轮选举因为各种原因失败了。
实操心得:如果发现切换特别慢,先看
cluster-node-timeout,再看从节点数量。从节点太多(比如一个主挂 5 个从),会加剧分票和重试,反而拖慢切换。一般一个主配 1~2 个从就够,追求极致可用可以配 2 个。
5. 新主接管与旧主回归:槽位迁移的收尾工作
5.1 接管槽位:configEpoch 的更新与广播
从节点当选后,会做几件事:把自己提升为主节点,接管原主负责的所有哈希槽,更新自己的configEpoch为当前最大 epoch + 1,然后通过 gossip 广播"我现在是这些槽的主人了"。
其他节点收到广播后,更新本地的槽位映射表。客户端如果缓存了旧的路由信息,会收到MOVED重定向,被迫刷新。这个过程通常在毫秒级完成,但如果客户端没有正确处理 MOVED,就会持续报错。这是应用层最常见的坑之一,后面会专门讲。
5.2 旧主回归:自动降级为从节点
原主节点恢复后,会发现自己的 configEpoch 比新主低,于是自动降级为从节点,跟随新主复制数据。这里有个重要细节:旧主上如果有新主没有的写入(故障期间产生的),这些数据会被丢弃,因为复制是单向的、以新主为准。
这就是 Redis Cluster 故障转移的异步复制本质——它保证的是可用性和最终一致,不是强一致。故障瞬间未同步的写入会丢失。如果你的业务不能接受任何丢失,需要在应用层做补偿,或者用WAIT命令做同步复制(但会牺牲性能)。
5.3 手动故障转移:CLUSTER FAILOVER 的正确用法
除了自动转移,Redis 还提供了手动命令CLUSTER FAILOVER,在从节点上执行。它有三个模式:
CLUSTER FAILOVER:默认模式,需要主节点同意,会先做数据同步再切换,最安全。CLUSTER FAILOVER FORCE:主节点不可达时强制切换,可能丢数据。CLUSTER FAILOVER TAKEOVER:完全绕过协商,直接接管,最危险,仅用于灾难恢复。
日常做计划内维护(比如升级主节点),我强烈建议用默认模式,它能保证切换前数据同步完成,几乎零丢失。TAKEOVER只在集群已经不可用、需要强行恢复时用。
6. 实战排查:为什么我的集群没有自动切换
6.1 排查思路:按阶段逐层定位
遇到"主挂了但没切换",别慌,按下面的顺序查:
- 确认主节点是否真的被标记 FAIL:
cluster nodes看状态,是fail?还是fail。 - 确认从节点是否在线且数据够新:看从节点的
master_link_status和 offset 差距。 - 确认多数主节点是否存活:如果挂的主超过半数,永远切不了。
- 确认总线端口连通性:
telnet或nc测port+10000。 - 看日志:从节点日志里搜
Start of election、failover auth,能看到选举卡在哪。
6.2 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| 主挂了不切换 | 存活主节点不足半数 | cluster nodes数主节点 | 增加主节点或恢复故障节点 |
| 切换后数据丢失 | 异步复制未同步 | 对比 offset | 应用层补偿或调小 timeout |
| 切换特别慢 | timeout 过大或从节点过多 | 看选举日志时间戳 | 调小 timeout,减少从节点 |
| 集群一直 fail | 总线端口不通 | 测 port+10000 | 放行防火墙/安全组 |
| 从节点不参选 | 数据太旧 | 看 validity-factor 计算 | 调大 factor 或修复复制 |
| 反复切换 | 网络抖动误判 | 看 PFAIL 频率 | 调大 timeout |
6.3 独家避坑技巧
技巧一:用CLUSTER COUNT-FAILURE-REPORTS看故障报告数。这个命令能告诉你某个节点收到了多少份 PFAIL 报告,是判断"是否接近客观下线"的利器。如果报告数在涨但没到多数,说明网络在抖动。
技巧二:监控cluster_state和cluster_slots_pfail指标。前者是集群整体状态,后者是处于 PFAIL 的槽数。这两个指标能提前预警,比等到业务报错再查强得多。
技巧三:定期演练故障转移。别等真出事才验证。用CLUSTER FAILOVER或直接 kill 一个主节点,观察切换时间和数据一致性。演练过的集群,真出事时你心里有底。
技巧四:客户端必须处理 MOVED 和 ASK。很多"切换后业务报错"其实是客户端没刷新路由。用支持 Cluster 的客户端(如 Jedis Cluster、Lettuce、redis-py-cluster),并确保开启了拓扑刷新。
7. 参数调优与容量规划:让故障转移真正可靠
7.1 核心参数对照与推荐值
| 参数 | 默认值 | 作用 | 推荐值 | 说明 |
|---|---|---|---|---|
| cluster-node-timeout | 15000 | 判定 PFAIL 的超时 | 5000~10000 | 内网可小,跨机房调大 |
| cluster-replica-validity-factor | 10 | 从节点数据新鲜度容忍倍数 | 2~5 | 越小越优先新数据 |
| cluster-migration-barrier | 1 | 主节点最少从节点数 | 1~2 | 影响从节点自动迁移 |
| cluster-require-full-coverage | yes | 槽未全覆盖是否停止服务 | 视业务 | no 可提升可用性 |
| cluster-replica-no-failover | no | 从节点是否禁止转移 | no | yes 用于特殊维护 |
cluster-require-full-coverage这个参数值得单独说。默认yes时,只要有任何一个槽不可用,整个集群拒绝所有请求。改成no后,只有受影响槽的请求失败,其他槽正常服务。对可用性要求高的业务,建议设为 no,代价是部分数据可能暂时不可访问。
7.2 主从数量与故障域的规划
前面反复强调"多数主节点"这个约束。规划时记住:
- 主节点数量建议 3、5、7 这样的奇数,容错数分别是 1、2、3。
- 每个主节点配 1~2 个从节点,且从节点要分布在不同物理机/机架/可用区。
- 避免所有主节点在同一台物理机,否则机器一挂全完。
我见过一个反面案例:6 个主节点全在同一批虚拟机上,结果宿主机故障,6 个主同时挂,集群彻底不可用。故障域分散不是口号,是保命线。
7.3 监控指标清单
想让故障转移可靠,这些指标必须监控:
cluster_state:集群状态,ok 才正常。cluster_slots_assigned:已分配槽数,应等于 16384。cluster_known_nodes:已知节点数,异常下降说明有节点失联。cluster_size:主节点数量,变化说明发生了转移。- 各节点
master_link_status:从节点复制状态,up 才健康。 - 各节点
master_last_io_seconds_ago:复制延迟,过大影响参选资格。
把这些指标接入告警,配合日志里的failover关键字,基本能做到故障转移"发生即感知"。
8. 写在最后:几个我踩过的真实教训
关于 Redis Cluster 故障转移,我最后再分享几个用血换来的经验。
第一,别迷信"自动"。自动故障转移能处理大部分场景,但网络分区、多数派失效这类情况它无能为力。关键业务要有降级预案,比如本地缓存兜底、限流保护。
第二,切换时间不是越短越好。我早期把 timeout 调到 3 秒,结果一次网络抖动触发切换,切换过程中又抖动,来回切了三次,业务抖动比不切还严重。后来调到 8 秒,稳定多了。稳定比快更重要。
第三,演练要常态化。我们后来定了个规矩:每季度做一次故障转移演练,随机 kill 一个主节点,记录切换时间和数据丢失量。坚持一年后,真出事时团队反应速度完全不一样。
第四,客户端配置和集群配置一样重要。再好的集群,客户端不刷新路由、不重试、不处理 MOVED,照样报错。上线前一定要压测客户端的故障恢复能力。
这套东西说起来不复杂,但每个细节都可能在关键时刻掉链子。希望这篇能帮你把 Redis Cluster 的故障转移从"黑盒"变成"透明盒",下次告警响起时,你能胸有成竹地定位问题,而不是像我第一次那样手忙脚乱。