拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Redis集群为何会整体不可用?五大陷阱与救火指南

Redis集群为何会整体不可用?五大陷阱与救火指南

先给结论:Redis 集群并不是“只要还有主节点活着就能用”。在默认配置下,只要有一个槽位失去所有者,整个集群就会直接进入CLUSTERDOWN状态,所有读写全部被拒绝——一个分片的主从全挂,能拖垮整个集群。这不是玄学,而是集群设计从一开始就写死的“要么完整,要么全挂”的一致性偏好。这篇文章结合我多年维护和救火 Redis Cluster 的实际经验,把会让集群整体不可用的场景一个个列出来,每个场景配套对应的日志、报错特征和恢复办法,适合正在用或者准备上线 Redis 集群的同学收藏。

1. 先搞清楚集群的“可用”是怎么定义的

1.1 槽位、复制、投票三件事

Redis Cluster 的高可用不是靠“哪个节点活着”来判断的,它依赖三个底层机制:

  1. 槽位(hash slot):整个集群有 16384 个槽位,按 key 的 CRC16 哈希值映射,每个主节点负责一段槽位区间。只要有一个槽位没有被任何节点负责,集群状态就是不一致的。
  2. 复制(replication):每个主节点可以有多个从节点,从节点实时复制主节点数据,在主节点挂掉后可以接管它负责的槽位。
  3. 投票(voting):主节点之间通过 gossip 协议互相发心跳,默认cluster-node-timeout是 15000ms。一个节点超过这个时间没收到另一个节点的心跳,会先把它标记为PFAIL(主观下线);当集群内多数主节点都认为它是PFAIL,才升级为FAIL(客观下线)。

从PFAIL到FAIL这个设计非常关键:它保证了“判定死亡”不是单个节点拍脑袋,而是需要集群多数节点共同确认,从机制上防脑裂。

当某个主节点确认失败后,它的从节点会发起FAILOVER_AUTH_REQUEST,其他主节点投票,谁的数据偏移量最新谁接替。但这个选举有个硬前提:必须拿到多数主节点的投票。票凑不齐,从节点就永远升不了主。

1.2 “整个集群不可用”其实分两个层次

很多人理解的不可用是“节点全挂了”,但 Redis 集群最常见、也最坑的一种不可用是:节点明明活着,集群却整体拒绝服务。此时你连一个热 key 都读不出来,返回的全是:

(error) CLUSTERDOWN The cluster is down

这就是cluster_state: fail的状态。触发它的条件有两个:

  • 存在没有节点负责的“孤儿槽位”;
  • 节点认为自己和集群多数节点已失联,无法形成有效决策。

这里有个核心参数cluster-require-full-coverage,默认是yes。它的含义就是:只要有任何一个槽位失去归属,集群直接进入 fail 状态,所有客户端请求全部拒绝。等于说整个集群的可用性被最差的那个分片拽着走。

明白了这层逻辑,下面讲的所有“集群整体不可用”场景,都会归根到这两个机制上。

2. 什么情况会让集群全盘拒绝服务

2.1 主节点失联超过半数,投票选不出新主

这是最直观的场景:假设集群有 5 个主节点,某次交换机故障导致 3 个主节点同时失联。剩下的 2 个主节点无法凑齐“多数票”,所有失联主节点的从节点都发起不了有效的 failover。于是这 3 个主节点负责的槽位全部变成孤儿槽位,默认配置下整个集群CLUSTERDOWN。

这里容易有个误解:很多人以为“挂了 3 个主,还剩 2 个主能服务”,但实际上 Redis 集群的可用性不是按“剩余节点数”算的,而是按“槽位覆盖完整性”和“选举多数”算的。主库大面积死亡后:

  • 没有多数票,从库无法自动接管;
  • 即使部分槽位还有从库活着,槽位依旧处于“没有有效主节点”的状态;
  • cluster-require-full-coverage yes下,所有读写被一刀切。

换句话说,“主节点故障超过半数”不是唯一的触发条件,任何让槽位失去主从覆盖的场景,都能触发全局不可用。

2.2 某一组槽位的主从全部挂掉,槽变成“孤儿”

这是生产环境最容易被低估的风险。只要一次物理机宕机,恰好把某个分片的主节点和它所有从节点一起带走,哪怕其他分片全部健康,整个集群也会因这一个分片的孤儿槽位而进入 fail 状态。

我用实际案例说明:之前有一个 3 主 3 从的集群,为了省机器把其中一组主从放在了同一台物理机上。某天那台机器磁盘损坏,主和从一起没了。存活的两个分片运行正常,Redis 却对全集群所有读写返回CLUSTERDOWN。业务方反馈“整个缓存突然全挂了”,实际上只挂了一台物理机。

这就是require-full-coverage的残酷性:它把 16384 个槽位看成整体,一个分片的完整性等于整个集群的完整性。少一个槽位,等于全挂。

2.3 网络分区把集群切成两半

集群最怕的不是单点故障,而是网络分区,尤其是把节点切成了“大小两个区”。

假设 6 主 6 从跨两个机房部署,骨干网抖动把两个机房切断了。A 机房有 4 个主节点,B 机房有 2 个主节点:

  • A 机房侧因为拥有多数节点,可以正常形成决策,B 机房的主节点会被 majority 标记为 FAIL,A 机房内的从库可以发起接管;
  • B 机房侧只有 2 个主节点,永远凑不齐 3 票,无法进行任何选举,几个主节点一旦需要 failover,对应的槽位就迅速变成孤儿槽位。

结果往往是:A 机房正常服务,B 机房因为 sees 多数不可达,加上孤儿槽位,直接进入 fail 状态。如果客户端路由策略不当,业务请求在 B 机房一侧全部报CLUSTERDOWN,表现为“整个集群不可用”。

这个场景没有完美的解药。Redis 集群的多数派机制决定了一件事:它宁可让一部分节点不可用,也不允许脑裂后两边同时产生新主写入数据。对很多团队而言,“分区期间一边挂掉”是可以接受的,真正不能接受的是两边同时写、数据分叉。

2.4 级联失败才是最常见的“整集群事故”

真正导致 Redis 集群整体瘫痪的,往往不是一次“精准打击”,而是级联失败。最典型的是下面这条链:

  1. 某主节点发生长耗时 GC 停顿或磁盘 IO 抖动,心跳超过cluster-node-timeout;
  2. 其他节点把它标记为 PFAIL,随后多数确认 FAIL;
  3. 多个从节点同时对它发起 failover,其中一个升主;
  4. 新主节点负载飙升,同时触发持久化和复制积压,又出现心跳超时;
  5. 新一轮 FAIL 继续扩散,多个分片进入 failover 循环。

我在一次事故里看到过,cluster-node-timeout被人从默认的 15000 调到了 3000,理由是“加快故障切换”。结果一次全集群 GC 停顿 4 秒,所有节点互相标记为 PFAIL,一堆从库同时尝试 failover,集群在十几秒内彻底 fail。把超时调得太小,等于把误判成本放到最大。

2.5 配置型事故:timeout 太小、bind 错、时钟漂移

除了真实的故障,还有一类“人祸”也会让整个集群不可用,而且这类问题排查起来特别费劲:

  • cluster-node-timeout设置过小:任何一次突发延迟都可能引发全集群误判,多个分片同时 failover;
  • cluster-announce-ip/ bind 配置错误:在容器或虚拟机环境里,节点间通过错误的 IP 互相发现,心跳断断续续,集群长时间处于 PFAIL/FAIL 边缘,状态极其不稳定;
  • 服务器时钟漂移:Redis 集群虽然不强制要求 NTP,但时钟不同步会直接影响心跳超时判断和选举纪元,A 节点认为 B 已经超时,B 却认为自己一切正常,vote 混乱;
  • 误删nodes.conf:这个文件记录了节点 ID 和集群拓扑,重启时如果文件被误删,节点会以为自己是个新节点,拿不到原来的槽位信息,集群关系直接错乱。

配置型事故里,nodes.conf被误删是我见过最容易造成“全网瘫痪”的操作:直接导致槽位归属信息丢失,集群无论如何都无法恢复,只能重建或从备份恢复。

3. 日常最容易踩的坑和配置取舍

3.1 require-full-coverage:一个参数决定全局生死

这个参数是“整个集群不可用”话题里绕不开的开关,必须单独说。

  • yes(默认):任何槽位裸奔,全集群拒绝服务;
  • no:槽位裸奔时,访问到裸奔槽位的请求报CLUSTERDOWN,其他槽位照常服务。

很多团队会把no当作保命配置,我理解这个选择,但也想提醒它的问题:设为no之后,你必须要接受“部分请求报错”这个常态。如果监控只盯“集群节点存活”,不盯“具体哪些槽不可用”,很容易出现大量请求失败却没人发现的情况。

我的建议是:

  • 线上核心链路,能承受短时全停的,保持默认yes,配合完善的告警;
  • 不能承受全停、宁可部分降级的,设no,但必须对cluster_state:fail和具体孤儿槽位做告警;
  • 无论哪种,都应该在架构上保证“每个分片的主从不在同一台物理机/同一个可用区”,否则require-full-coverage怎么配都救不了。

提示:cluster-require-full-coverage是 rolling 配置,修改后逐节点生效,但要在所有节点上保持一致,否则节点间互相判定集群状态不一致。

3.2 持久化 fork 阻塞触发“心跳失联”

Redis 执行BGSAVE或 AOF rewrite 时要 fork 子进程。如果实例内存很大,fork 瞬间会阻塞主进程,可能长达几秒甚至十几秒。所有节点同时在做定时持久化的话,很可能集体错过心跳窗口,互相标记 PFAIL/FAIL。

之前有个客户集群,内存 80GB 实例,把save配置设成了每 5 分钟执行一次,结果高负载时段 fork 阻塞 8~10 秒,恰好大于cluster-node-timeout,整集群进入 failover 风暴。

应对办法很简单:

  • 把cluster-node-timeout调到 15~30 秒之间,给 GC 和 fork 留出缓冲;
  • 错峰持久化,别让所有节点同一时刻 BGSAVE;
  • 监控 fork 耗时和redis-cli info stats里的latest_fork_usec,超过阈值就要排查内存和内存碎片化问题。

3.3 迁移/缩容中途宕机

做 resharding 或者缩容时,如果中途有节点宕机,事情会非常难看。槽位迁移依赖MIGRATE命令,数据迁移到一半时,源节点和目标节点处于“migrating/importing”的中间状态。这个状态下如果源主节点挂了,而它的从节点没有同步到最新的迁移状态,槽位可能处于一种“既有人认领又没人真正负责”的模糊状态。

这种情况最容易踩的坑是:不看 cluster info 就开始操作集群变更。我的习惯是操作前先跑一遍redis-cli --cluster check,确认cluster_slots_ok == 16384且没有 PFAIL/FAIL 节点,再动拓扑。变更过程中,严格避免同时进行另一项故障操作。

3.4 热 key 把单点打满引发的连锁

Redis 集群没有全局负载均衡,key 落在哪个分片是哈希决定的。某个热 key 所在的分片被打满 CPU 或内存后,这个节点的响应变慢,心跳开始超时,被标记为 FAIL,从库接管后继续被打满,再次超时……

这种“热 key 导致单点故障,单点故障触发全局 fail”的链路,本质上和 2.4 的级联失败是同一种模式。唯一的解法是在业务层做热点 key 拆分,或者在架构上把热点 key 的访问分散到多个 key 上,减少单分片压力。集群本身的故障转移机制救不了“被打满”的节点,因为打满它的流量还在。

4. 哨兵方案和集群方案的边界

4.1 哨兵模式什么时候全挂

很多团队没上 Cluster,用的是主从加哨兵。哨兵模式下整集群不可用的触发条件,和 Cluster 不太一样:

  • 哨兵多数派消失:假设部署 3 个哨兵,任何一个哨兵宕机都不影响,但如果 2 个哨兵同时宕机,剩下 1 个哨兵即使发现主库挂了,也无法完成 failover(需要多数派确认和选举 leader)。此时主库宕机,写入直接中断,服务不可用。
  • down-after-milliseconds设得太短:哨兵对主库的误判也会导致频繁 failover,甚至主库还在等服务却已经切换走,写入全部被拒。
  • 从库被误删或者复制断裂:哨兵能发现主库挂掉,但从库数据延迟过大,failover 后接手的从库缺少大量数据,业务读取到的是很久以前的数据,这种“伪可用”比不可用更可怕。

哨兵方案的核心保护参数是min-replicas-to-write(旧版本叫min-slaves-to-write)。设置为主库至少有 N 个健康从库时才接受写入,可以在极端情况下保护数据一致性,代价是可用性下降。生产上到底设多少,本质上是业务写多还是读多、能容忍丢失多少数据。

4.2 两种方案“不可用”的条件对比

我整理了一张对比表,方便你直接根据自身架构判断风险:

场景Redis Cluster哨兵 + 主从
单主节点宕机从库自动接管,槽位仍覆盖哨兵多数派存活时自动切换
某一分片主从全挂槽位失控,默认全集群 CLUSTERDOWN该主从链路不可用,其他链路不受影响
超过半数管理节点故障无法选举,槽位多个失控,全局失败哨兵无法构成多数,无法切换,写入中断
网络分区多数派一侧可继续,少数派一侧 fail哨兵分区,主库可能长期不切换或双主风险
脑裂保护选举投票机制天然防脑裂靠 quorum + min-replicas 约束,风险更高

一句话总结:Cluster 用“全局槽位完整性”换来了强一致和高分片能力,但也因此把单分片故障的影响面放大到全局;哨兵方案各主从链路相对独立,反而没那么容易被“一粒老鼠屎”拖垮整个集群。这也是为什么很多中小团队到现在还坚持用哨兵而不是 Cluster。

5. 出事后怎么定位和恢复

5.1 三行命令先看现场

集群进入未知状态时,第一件事千万别重启节点,先做现场采集。我救火的一贯顺序是:

redis-cli -h <ip> -p <port> cluster info redis-cli -h <ip> -p <port> cluster nodes redis-cli -h <ip> -p <port> cluster slots

cluster info里的关键指标:

  • cluster_state:ok还是fail;
  • cluster_slots_assigned/cluster_slots_ok:两者不等说明有孤儿槽位;
  • cluster_known_nodes:确认节点总数是否和预期一致。

cluster nodes输出里每一行代表一个节点,重点看中间 flags 列:

  • myself:当前节点自己;
  • master/slave:角色;
  • fail?:PFAIL,代表某个节点认为它有主观故障嫌疑;
  • fail:FAIL,已经多数确认客观下线;
  • disconnected:该节点无法和集群内其他节点正常通信。

这三个命令能快速回答两个问题:集群是怎么挂的(孤儿槽位还是多数派失联),以及哪些节点处于什么状态。

5.2 恢复顺序和止血操作

确认了故障类型后,按下面的顺序处理:

  1. 先救孤儿槽位。优先启动宕机的主节点或从节点,让槽位恢复归属。节点启动后会自动根据nodes.conf里的拓扑信息重新加入集群,不需要手动CLUSTER MEET。
  2. 凑不齐多数时用 FORCE 接管。如果超过半数主节点失联导致无法自动选举,可以在存活的从节点上执行:
redis-cli -h <replica-ip> -p <replica-port> cluster failover force

FORCE可以绕过多数票机制,让从库直接接管。注意这属于紧急操作,只有在你确认原主节点短期无法回来时才建议用,否则会出现双主风险。

  1. 节点恢复但数据不对时不要直接上线。原主节点重新启动后,它可能会带着一份“旧数据”回到集群。cluster-replica-validity-factor默认值是 10,意思是主节点失联超过 10 个cluster-node-timeout后,从库不再自动让它变回主节点,避免陈旧数据污染集群。如果非要恢复,优先把它作为从库挂回集群,让它先同步新主数据再切换角色。

  2. 配置层面止血。紧急情况下可以先把cluster-require-full-coverage临时改为no,让集群恢复部分可用,把大面积故障的影响面收缩到受害分片。但这个操作只能救“孤儿槽位”场景,救不了“选举多数派丢失”场景。

注意:cluster failover force是高危操作。执行前必须确认要接管的从库具备最新的复制偏移量,否则你恢复的是一个数据严重落后的“假主节点”。

5.3 集群可用性排障速查

下面这张表是我自己压箱底的排障速查,遇到问题直接对照:

报错信息典型场景恢复方向
CLUSTERDOWN The cluster is down存在孤儿槽位或多数派失联启动死节点、检查槽位覆盖、必要时 force failover
CLUSTERDOWN Hash slot not served某个槽位无主可依(多半是 require-full-coverage no 状态下的局部不可用)恢复该分片主从
MOVED <slot> <ip:port>客户端用了旧路由信息使用支持自动路由的客户端,或刷新集群拓扑
ASK <slot> <ip:port>槽位正在迁移的中间状态客户端处理 ASK-REDIRECT,或等待迁移完成
MASTERDOWN Link with MASTER is down哨兵模式下主库挂掉且从库拒绝旧数据读取触发或手动执行哨兵 failover
LOADING Redis is loading the dataset节点重启加载 RDB/AOF 中等待加载完成,检查持久化文件是否过大
OOM command not allowed when used memory节点达到 maxmemory,写入被拒扩容、清理 key、调整淘汰策略

5.4 关于告警和演练的小建议

还有一个值得单独拿出来的经验:集群不可用的恢复不一定靠“对”的操作,更多时候靠“快”的响应。如果你没有提前做故障演练,等到线上真的出现CLUSTERDOWN,再去看日志、查节点状态,业务早就超时到爆炸了。

我们团队内部会定期做三类演练:

  • 随机杀主节点,验证从库能否在预期时间内接管;
  • 模拟网络分区,观察两端分区的 failover 行为是否符合预期;
  • 演练过程中把cluster-node-timeout、require-full-coverage等关键参数逐个调整,验证监控告警是否真的能触发。

不做演练的集群,配置再漂亮也只是纸面高可用。

6. 最后说点运维层面的体会

这几年的经验里,我最深的感受是:Redis 集群的整体不可用,几乎都不是“设计缺陷”,而是“配置和部署方式没跟上它的预期”。它默认认为每个分片的主从物理隔离、心跳网络干净、节点时钟一致、节点数不会频繁变动。你在哪个环节破了它,它就在哪个环节拉爆全局。

cluster-node-timeout别调到 5 秒以下,require-full-coverage别在没搞清数据一致性代价时随意改,节点变更前记得先看cluster info,跨机房部署一定要确认多数派在一个区内。这些都不是新知识,但每一条都是从真实的线上事故里摔出来的。

如果你现在正在规划集群方案,我的建议是:先把“容忍哪部分不可用”想清楚,再决定用什么方案、调什么参数。高可用从来没有银弹,只有你愿意为哪一种故障买单的选择。

返回列表