单机Redis用到了25G内存的时候,全量RDB每次要压十几分钟,慢查询和内存淘汰在高峰期同时爆发,值班电话凌晨两点被打爆。那段时间我几乎把社区里所有和Redis扩容相关的方案都翻了一遍,最后老老实实把集群这块从原理到落地啃了一遍。这篇文章不是Redis Cluster的官方文档翻译,是我自己从单机到集群、从踩坑到稳定运行之后沉淀下来的理解,尽量把每个机制背后的"为什么"讲清楚。
如果你正准备上集群、在准备Redis面试,或者集群偶尔出点奇怪问题不知道怎么排查,这篇应该能帮你省不少时间。我不打算只罗列"怎么搭集群"的步骤,重点放在原理:数据怎么切片、节点怎么通信、主从怎么切换、客户端怎么找到key、脑裂为什么会发生。把这些串起来之后,再去看配置和命令,基本就是水到渠成的事。
1. 集群到底解决了什么:先分清"扩容"和"高可用"两码事
1.1 单机撑不住的四种表现
很多人一听到"Redis集群",第一反应就是"内存不够了,要多搞几台机器存数据"。这个理解不算错,但太粗了。我在生产环境里总结下来,单机Redis撑不住通常有四种表现,它们对应的解法其实并不完全一样。
第一种是内存容量触顶。数据量到几十个G之后,成本先不提,持久化的压力会非常明显。RDB fork子进程写快照,内存越大,fork瞬间的卡顿越明显,如果开启了AOF,重写时的磁盘IO和CPU消耗同样吓人。第二种是单线程CPU饱和。Redis 6之前核心命令是单线程处理的,就算你的机器是128核,一个实例也只能吃满一个核。热点key集中在某个高QPS业务上时,CPU先到瓶颈。第三种是写请求的并发量超出单机上限,比如某个活动场景下每秒几万次写,单机不管怎么调优都有天花板。第四种是单一故障点,机器挂了整个缓存层就塌了。
这四种问题,前三种本质上是"算力不够、容量不够",要靠分片来横向扩展;第四种是"可用性不够",要靠副本加自动切换来解决。Redis Cluster的设计目标,就是用一套方案同时覆盖这两类需求。
1.2 哨兵加主从为何走了弯路
在没有Cluster之前,大家最常用的高可用方案是"主从复制 + Sentinel哨兵"。主从复制解决的是数据冗余和读扩展,Sentinel解决的是主节点故障时的自动切换。这套方案在小规模场景下非常成熟,我早期好几个项目都是这么跑的。
但它最大的问题在于:数据还是只在主节点上写。从节点只是备份和分担读流量,并没有真正把写容量拆开。一旦数据总量超出单机上限,或者写QPS打满单核CPU,Sentinel再强也救不了你,因为所有写请求还是落在同一个单点上。
所以后来的演进路线很自然:既然单机写有瓶颈,那就把数据拆成多份,分别放在不同的机器上,每份数据由一个独立的主节点负责。这就是分片(sharding)的思路。而Redis Cluster做的事情,是在分片之上又加了主从复制和自动故障转移,让每个分片都不是单点。
1.3 集群与哨兵架构的分工边界
现在经常能看到有人纠结"到底用Sentinel还是Cluster"。我比较认可的分工方式是:如果你的数据量单机装得下,只是想要高可用,Sentinel够用;如果数据量已经逼近单机上限,或者写QPS已经压满单核,直接上Cluster,不要再叠Sentinel。
为什么不建议"集群套哨兵"?因为Cluster内部已经内置了故障转移机制,再套一层Sentinel反而让架构变复杂,主从关系切换的决策者变多了,容易出现两个"主节点"同时在线的风险。Cluster模式下的主从切换是由多个主节点投票决定的,和Sentinel的quorum机制原理类似,但它在集群内部完成,管理成本更低。
我实际迁移过的一个项目,一开始是"3主3从用Sentinel管",后来数据涨到单实例30G,写QPS也一直在涨,最后迁移到了Cluster。整个迁移过程最大的感受是:Sentinel管的是"谁是老大",Cluster管的则是"数据放在哪一片 + 每一片谁说了算"。两者解决的问题域不同,但Cluster的设计是向下兼容的"每一片内部仍然是主从复制"。
2. 数据分片与slot设计:集群最核心的数学
2.1 从哈希取模到slot的演进逻辑
Redis Cluster分片的核心不是"一致性哈希",而是slot(槽)。这个概念很多人初听觉得绕,其实背后的演进逻辑很清晰。
早期搞客户端分片,比如Twemproxy或者Jedis Sharded,最常用的方式是哈希取模:hash(key) % N,N是节点数。这个方案实现简单,但有一个致命伤:节点数一变,N变了,几乎所有key的映射位置都会变化,这意味着大量的key需要重新分布,缓存会在扩容瞬间大面积失效,对线上是个灾难。
后来大家开始用一致性哈希,它把哈希值空间首尾相接形成一个环,每个节点映射到环上,key沿环顺时针找到最近的节点。好处是增减节点时只需要迁移少量key,坏处是实现和运维复杂度上来了,而且节点少时数据容易倾斜,一种宽松的做法是加一层虚拟节点。Redis设计集群时没有走这条老路,而是引入了一个中间层:把整个哈希空间划分为固定数量的slot。
slot这个中间层最大的价值,是把"key到节点"的映射拆成了两步:key -> slot是固定不变的,slot -> 节点是可以动态调整的。扩容缩容时只需要在节点之间搬运slot,不用关心每个key单独是谁的。这个抽象非常像操作系统里的虚拟内存:应用面对的是连续的虚拟地址空间,物理内存页怎么分配是内核说了算。
2.2 CRC16与16384,这两个数字为什么重要
在Redis Cluster里,一个key最终落到哪个slot,由下面这行计算决定:
HASH_SLOT = CRC16(key) % 16384CRC16是循环冗余校验算法,输入一个字符串,输出一个16位的整数,范围是0到65535。再对16384取模,就得到了0到16383的slot编号。
这里有同学会问:为什么用CRC16而不是MD5或者SHA?因为CRC16计算速度快、输出长度短,作为key的散列函数足够均匀。Redis作者选的是CRC16的一个特定多项式实现,社区里有人验证过,对大量常见key的分布接近均匀。你不需要记住多项式细节,只需要知道:任意一个key都能被快速算出一个0到16383之间的slot编号,这个编号就是它在集群里的"地址"。
如果key里包含{},规则会变:HASH_SLOT = CRC16({}内部内容) % 16384。比如{user:1001}.name和{user:1001}.age会落到同一个slot。这个特性叫hash tag,是做批量操作的关键工具,后面专门讲。
2.3 为什么要设为16384个槽而不是更多
很多人背过"Redis集群有16384个slot",但很少人想过:为什么偏偏是16384,而不是65536,或者干脆用2^32个槽?这个数字背后是有工程考量的。
第一,心跳消息的大小限制。节点之间通过Gossip协议互相传播信息,消息里要携带当前节点的slot分布信息。Redis用一种非常紧凑的方式表示:bitmap位图,一个bit代表一个slot是否由本节点负责。16384个slot只需要16384 / 8 = 2048字节,也就是2KB。如果slot数量翻4倍变成65536,位图就变成8KB。Gossip消息是在节点之间周期性传递的,消息越大,网络带宽消耗越大,尤其集群规模上来之后,这个开销会非常可观。
第二,数据分布粒度的成本。slot越细,每个slot包含的key越少,迁移时越灵活,但迁移和管理的元数据开销也会增加。16384这个数量级对于Redis官方预期的千节点以内的集群规模来说,既能保证key分布相对均匀,又不会让slot管理和迁移成本失控。Redis官方在设计说明里也提到过,节点数量一般不会超过1000,16384个slot在这个规模下每个节点平均能分到16个以上,足够用了。
第三,还有一个不太起眼的点:slot数量必须是2的幂,这样取模操作可以用位运算优化。16384是2的14次方,CRC16(key) & 16383和% 16384是等价的,但前者的计算开销更低。
2.4 扩容和缩容时slot是怎么迁移的
理解了slot之后,扩容缩容就很好解释了。给集群加一个节点,本质上就是:新节点先以空master身份加入,然后用reshard命令从老节点那边匀一部分slot过来。移动slot的过程并不是一个原子操作,而是逐个slot、逐个key地迁移。
这个过程中Redis做了几个精巧的设计来保证客户端无感。迁移一个slot时,源节点和老节点都会保留这个slot的路由信息。客户端访问到源节点时,如果key还没迁移走,正常处理;如果key已经被迁走了,源节点会返回一个ASK重定向错误,告诉客户端"你去目标节点问"。关于MOVED和ASK的细节,我后面单独用一章展开。
缩容的操作逻辑刚好反过来:先把这个节点负责的所有slot迁移给其他节点,等它变成空master之后,再执行del-node把它从集群中移除。千万不要直接kill掉一个还持有slot的节点,那会让集群负载不完整,轻则部分key不可访问,重则整个集群拒绝服务(取决于cluster-require-full-coverage配置)。
3. 节点通信机制:Gossip协议如何维持集群认知
3.1 两个端口背后的设计意图
和单机Redis不同,集群模式下的每个节点会同时监听两个端口。一个是客户端端口,默认6379;另一个是集群总线端口,默认是客户端端口加10000,也就是16379。当初第一次部署时我还忽略了这个细节,结果单机多实例部署时把两个节点都配成6379,直接冲突启动失败。
为什么要单独开一条总线?因为客户端访问的流量和节点间内部通信的流量,它们的优先级和特性不一样。客户端命令需要低延迟、高吞吐,而节点间心跳、数据同步、故障广播,走的是独立的连接,这样不会因为大量客户端请求把心跳阻塞了,导致节点被误判为下线。集群总线端口承载的是二进制的Gossip协议,不是普通RESP协议。
3.2 心跳消息里的节点信息交换
Gossip协议是分布式系统里久经考验的通信方式,Redis Cluster的节点之间每隔100ms就会互相交换一次心跳消息。这里"互相交换"不是全连接广播,而是每个节点会随机挑选一部分节点发送PING,收到PING的一方回PONG。消息里携带什么内容?除了发送者自己的状态信息,还有它从别人那里听到的多个节点的状态信息,比如某个节点是否疑似下线、它的config epoch是多少、它负责哪些slot。
这样设计的好处是:信息传播不依赖中心节点,任何一个节点挂了或者出现网络分区,其余节点能通过多条路径逐渐达成一致的认知。代价是信息传播有延迟,节点状态在全集群收敛需要一定时间。这就是为什么集群对故障的反应不会像单机那么"瞬时",而是以cluster-node-timeout为基准的秒级、十几秒级。
Gossip在集群规模小的时候看起来有点"浪费"——明明只有3个节点,每次心跳消息还要捎带那么多样本。但节点数涨到几十上百之后,这种去中心化的设计优势就出来了:每个节点不需要维护到所有其他节点的长连接,也能在若干跳之内把关键状态扩散到全部节点。
3.3 从PFAIL到FAIL:故障状态如何确认
这里要区分两个状态:PFAIL(probable fail,疑似下线)和FAIL(确定下线)。一个节点超过cluster-node-timeout(默认15秒)没有响应心跳,首先会被标记为PFAIL。注意,这只是"我看到它可能挂了",不代表整个集群都这么认为。
PFAIL状态会通过Gossip消息扩散给其他节点。当集群里持有slot的主节点中,超过一半都认为这个节点是PFAIL时,它就会被打上FAIL标记。FAIL标记才是真正"官方认定"的故障,会广播给所有节点,触发后续的从节点选举。
这个两阶段确认机制的用意很明显:避免单节点因为一次网络抖动就误判整个集群状态。比如某个节点只是GC停顿了20秒,它与其他节点的连接全部超时,如果单节点就能判它死刑,那么正常的节点很可能被误切换。PFAIL到FAIL的"过半确认"机制,本质上是对网络抖动的一种容忍。
3.4 已经是集群了,为什么还需要主从复制
很多第一次接触Cluster的人会问:节点之间都用Gossip通信了,slot也分了,那"主从"还有存在的必要吗?答案是:分片管的是数据的水平拆分,主从复制管的是一片的稳定可用。
每个slot最终只由一个master负责处理写请求,但这个master如果宕机了,slot的数据不能跟着消失。所以每个master都会挂一个或多个replica(从节点)。从节点通过异步复制实时同步主节点的数据,平时不承担slot的写请求,只负责备份和读取扩展。当master被标记为FAIL之后,副本中才会通过选举产生一个新的master,接管整个slot集合。
从这个角度看,Redis Cluster其实是"分片 + 主从复制 + 自动故障转移"三者的结合体。任何一个master挂掉,只要它下面还有存活的从节点,整个集群就还能正常服务。这也是为什么生产环境的Redis集群至少会做成3主3从,而非纯3主。
4. 主从切换与自动故障转移:一次failover的完整时间线
4.1 从节点何时发起选举
节点被标记为FAIL之后,接下来就要看从节点怎么上位了。并不是FAIL一发生,从节点就立刻抢着当老大。从节点在发起切换之前,必须先确认自己有条件接替。
具体来说,从节点会检查几个条件:它的master是否已经处于FAIL状态;它自己与master断开连接的时间是否超过了cluster-node-timeout乘以一个系数(防止主从之间短暂网络抖动就触发切换);它的数据是否足够新。最常用的判断是复制偏移量,如果从节点落后主节点太多,即便切换上去,也会丢失大量数据,这种情况下它大概率不会抢着上位,而是等数据集补上来之后再做决定。
从节点发起选举的方式,是把自己当前的config epoch加1,然后向所有持有slot的主节点发送投票请求。config epoch可以理解成这个节点对集群配置的"版本号",版本号大的配置会覆盖版本号小的,这在后面脑裂场景里非常关键。
4.2 选举投票的规则与"过半"的意义
投票规则上,Redis集群用的也是"过半"思路:每个持有slot的主节点在同一轮选举里只能投一张票,从节点拿到超过半数主节点的投票,就能当选新的master。
举个例子,一个3主3从的集群,三个master分别是A、B、C,A宕了,A的从节点A1发起选举。B和C都有投票权,A1只需要拿到2张票(超过3的一半)就获胜。如果一个master下面有多个从节点同时发起选举,那么第一个取得过半票数的胜出,其他从节点就保持从属状态。
这个"过半"设计能保证同一轮选举中不会同时出现两个"新master"。因为两个候选者不可能同时拿到过半票数,这在统计学和逻辑上都不成立。分布式的世界里不怕慢,就怕多个决策者同时认为自己是老大,所以任何需要用投票决出的位置,过半都是最常见的安全阀。
4.3 一次真实故障转移的节点视角时间线
我刚开始学习时,老是搞不清楚"故障转移到底花了多长时间",后来画了一条时间线才彻底明白。以默认cluster-node-timeout为15秒为例:
- 0s:主节点A突然宕机,或是网络断开。
- 0s~15s:其他节点持续向A发心跳,没有收到PONG,A被标记为PFAIL。
- 15s之后:A的PFAIL状态开始通过Gossip扩散,其他主节点收到这个消息,纷纷确认自己也无法联系A。当超过半数的持有slot的主节点达成一致,A被标记为FAIL,FAIL消息广播到全部节点。
- FAIL之后:A的从节点A1检测到master状态为FAIL,发起选举,请求其他主节点投票。
- 选举完成后:A1收到过半票数,执行
SLAVEOF NO ONE把自己升为master,接管A原本负责的所有slot,并广播新的配置信息。 - 后续:如果A后来又恢复了,重新加入集群,它会发现自己现有的config epoch比对方小,于是作为新master的从节点存在,重新同步数据。
整体看下来,一次干净利落的故障转移大概需要16~20秒左右。这个延迟主要是由cluster-node-timeout决定的。如果你觉得太慢,可以调小这个参数,比如10秒甚至5秒,但要考虑网络抖动带来的误判风险。我一般建议线上先保持15秒,等对网络质量有充分把握再考虑调小。
4.4 网络分区场景:客户端会被坑在哪里
如果只是单个节点宕机,上面的时间线足够清晰了。真正容易出问题的场景是网络分区:集群中的一部分节点与另一部分节点断开了连接,但它们各自其实都还活着。
举个例子,5个主节点的集群,其中2个主节点和它们的从节点在同一个机架,这个机架的网络交换机挂了,导致这2个主节点与其他3个主节点断连。Cluster的故障判定机制依然会工作:其他3个主节点联系不上这2个,会把这2个标记为PFAIL,最终打成FAIL;这2个主节点自己也联系不上其他3个,但它们这边的某个从节点可能升级成功,重新自成一个"小集群"。
问题在于:网络分区期间,两个分区内的节点都以为自己是合法集群的一部分。如果客户端连接的是"少数派分区"里的主节点,可能仍然能正常写入,但这段写入数据在分区恢复后会被覆盖或丢弃。这种事在任何一个分布式存储系统里都算得上"经典难题",Redis Cluster同样没法完全避免。我能给的实践建议是:部署时尽量避免让"主节点+从节点"都集中在同一个故障域里,跨机架、跨可用区分散部署,能显著降低这种极端情况的发生概率。
5. 客户端视角:MOVED、ASK与smart client的配合
5.1 没有哈希环,客户端怎么知道key在哪
分布式系统里,客户端要读一个key,首先得知道它去哪台机器。早期的一致性哈希方案需要客户端内置哈希环逻辑,每个客户端库都要实现一遍,版本一多就乱。Redis Cluster则换了个思路:客户端不需要自己算key对应的slot再找节点,而是先随便连上一个节点,由服务端告诉它key的归属。
当你向集群中某个节点发送命令时,节点会先计算CRC16(key) % 16384,得到slot编号,再查自己是不是负责这个slot。如果是,自己处理;如果不是,返回一个MOVED错误,错误信息里包含目标节点的IP和端口。这一点我非常喜欢:服务端持有全量的slot路由表,客户端不需要单独同步一份集群拓扑,只做错误响应处理就够了。
最简单的客户端就是redis-cli -c,它的-c参数会开启集群模式,自动跟随MOVED重定向去正确的节点重发命令。如果没加-c,你会直接看到一堆MOVED 1234 192.168.1.5:6379的报错,这不是集群坏了,是客户端不支持集群协议。
5.2 MOVED和ASK的分工:一个是"永久搬家"一个是"临时借调"
MOVED和ASK长得像,实际含义完全不同。面试里经常考这个点。
MOVED表示目标key已经确定不归当前节点管了,当前节点返回"你去另一个节点找吧"。这个信息是永久性的,客户端收到MOVED后应该更新自己缓存的路由表(如果它是smart client),后续同类slot的请求直接发到新节点。
ASK出现在slot迁移的过渡期。迁移过程中,目标slot的一部分key还在源节点(老节点)上,一部分已经被搬到了目标节点(新节点)。当源节点收到一个key请求时,它发现这个key属于一个正在迁移中的slot,但那个key恰好已经被迁走了,于是返回一个ASK错误,指引客户端去目标节点。
关键区别在于:MOVED是"以后永远都去新节点",ASK是"只有这一次请求去新节点,之后你还是当它归老节点管"。所以smart client收到ASK时,不能更新路由表,只能重发这一次请求。收到MOVED则一定要更新路由表,否则每次都要多走一跳。Redis把这个写得很严谨,理解了这个细节,再去看redis-cli --cluster reshard过程中出现的日志,就不慌了。
5.3 hash tag:把多个key绑在同一个slot里的唯一办法
集群模式对单key操作没有影响,但对多key操作有严格限制。MGET、事务、Lua脚本等一次涉及多个key的操作,要求所有key必须落在同一个slot里。如果你同时操作foo和bar,它们的slot大概率不同,Redis会直接返回CROSSSLOT错误。
这时候就要用hash tag。只要key里出现{},Redis就只对大括号内的内容做CRC16计算。比如:
{user:1001}.profile {user:1001}.cart {user:1001}.orders它们的{}里都是user:1001,slot必然相同,就能放进同一个事务、同一个Lua脚本或同一条MGET命令里。hash tag是用一部分散列均匀性换来了多key操作的可行性。使用时要注意:别把粒度设得太粗,比如所有key都是{all}:xxx,那所有key都挤进同一个slot,分片彻底失效,集群退化成了单机。我见过有人图省事这么干,最后热点全部压在一个节点上,和没上集群一样。
6. 集群不是银弹:脑裂、数据丢失与一致性边界
6.1 脑裂是怎么发生的,cluster和sentinel都有此风险
"脑裂"这个词听起来吓人,其实就是分布式系统里多个节点同时认为自己是"主"的状态。在Redis Cluster里,典型场景是网络分区:旧主节点与它的从节点被隔开了,但旧主节点自己还活着,还能接收客户端请求。
分区之后,从节点因为联系不上主节点,发起选举并成功提升为新主节点。与此同时,旧主节点所在的少数派分区里,客户端仍然能连上旧主节点继续写数据。此时集群里就同时存在两个"master":一个在少数派分区里继续写旧数据,一个在多数派分区里接管slot并同步新数据。
等网络分区恢复,两个主节点重新建立连接,集群会通过config epoch来做"版本对账"。原理不复杂:谁的数据版本新,谁说了算。多数派分区这边选举产生的master通常配置版本号更高,旧主节点发现对方版本号比自己高,就甘愿降级为从节点。但降级之后,它要重新从新主节点全量同步数据,这意味着分区期间旧主节点上新增的写入数据会被直接覆盖掉。
6.2 导致数据丢失的两个关键窗口
这段是面试最爱考的:Redis集群在主从切换时,数据丢失有两个天然窗口。
第一个窗口是主节点异步复制带来的延迟窗口。Redis主从复制默认是异步的,主节点收到写命令后,先自己执行并返回给客户端,再通过复制流把命令发给从节点。如果主节点刚好在"已经回复客户端、还没把数据同步给从节点"的时候宕机,从节点升为主后自然没有这部分数据,丢了就丢了。
第二个窗口是脑裂导致的过期写窗口。刚才说的网络分区场景里,旧主节点在分区期间继续接收写入,这些写入在旧主节点降级后被全量同步覆盖,同样全都丢了。
所以Redis官方文档对Cluster一致性的描述非常坦诚:Redis Cluster不保证强一致性,在特定故障场景下会丢失写入。如果你对数据丢失零容忍,就不应该把Redis当成唯一的数据源,正确姿势是把它当缓存,底层数据库保留最终数据,或者对写请求做双写。设计系统时能接受多大的数据丢失,决定了你该怎么用集群,这是架构层面的一笔账。
6.3 理解Redis Cluster的最终一致性和取舍
用CAP理论来看,Redis Cluster在发生网络分区时,选择的是优先可用性:它是AP系统,分区发生时一边继续提供服务,一边在后台努力收敛状态,最后通过网络分区恢复后的"对账"达成最终一致。这个取舍对缓存场景非常合理:如果请求打到Redis,Redis因为分区拒绝服务,对业务影响反而更大;丢失一小段缓存数据,顶多重查数据库回填。
我自己的经验是:不要拿Redis集群和数据库的强一致集群做类比。数据库挂了要立刻停写保护一致性,Redis挂了要尽量继续提供缓存服务,两者设计哲学就是反着的。想清楚这一点,你就能理解为什么Cluster的故障转移并不追求零延迟,为什么异步复制下从节点升级不检查数据是否绝对最新,为什么网络分区后旧主数据会被覆盖。这套"不完美"的设计,恰恰是在复杂网络环境里长期稳定运行的关键。
7. 部署与调优中容易被忽略的细节
7.1 最小6节点和实例规划的讲究
网上总有人说"Redis集群最少要6个节点",这个说法背后是有逻辑的。集群要保证分片数据不丢失,至少需要3个master,每个master至少有1个从节点,3加3就是6个。如果你只想要水平拆分、不关心高可用,那3个master确实就够;但只要想在master宕机后自动切换,从节点就必不可少。
实际生产规划时还要考虑几个因素:每个master的内存不要超过物理内存的一半,因为从节点全量同步时会fork子进程,内存开销翻倍;从节点不应该和它对应的主节点放在同一台物理机上,否则机器挂了主从一起没;如果追求更高可用性,可以给某个热点master配2个从节点,或者利用cluster-migration-barrier参数让集群自动从从节点冗余的master迁移一个从节点给没有任何从节点的master兜底。
7.2 配置要点:从cluster-enabled到cluster-require-full-coverage
开启Redis集群之前,先把配置项捋清楚。我用的是最常用的几个:
# 开启集群模式 cluster-enabled yes # 每个节点独立的集群状态文件,千万不能共享 cluster-config-file nodes-6379.conf # 节点间通信超时时间,默认15000毫秒 cluster-node-timeout 15000 # 从节点优先级,数字越小越优先,0表示不参与选举 replica-priority 100 # slot没有全部被覆盖时是否拒绝服务 cluster-require-full-coverage no重点说最后一个cluster-require-full-coverage。旧版本默认是yes,意思是只要任何一个slot没有被任何master接管(比如某个master挂掉了,且它的从节点也没能成功切换),整个集群就会拒绝所有读写请求。这个设计的初衷是防止读到不完整的数据,但实测中代价太大:一个节点出问题,业务全挂。Redis 7.0之后默认改成了no,也就是允许部分slot失效,其他slot继续提供服务。建议生产环境显式配置成no,并接受"部分key暂时不可访问"的代价,至少别让整个集群停摆。
另外再提一句cluster-config-file,这个文件由Redis自己写,记录节点ID、角色、slot分配等元数据。每个节点必须用不同的文件名,否则多实例共享同一份文件会互相污染元数据。我踩过这个坑,两个实例在同一个目录下启动,互相把对方的信息覆盖掉,最后只能重置集群。
7.3 单机多实例、Docker和NAT场景下的端口坑
搭建测试环境时,很多人喜欢在一台机器上起多个Redis实例模拟集群。这时候要记住:每个实例不仅有客户端端口,还有集群总线端口。默认情况下总线端口是客户端端口加10000,比如7001对应17001。如果启动了防火墙,或者Docker做端口映射时只映射了客户端端口而忘了映射总线端口,节点之间会一直握手失败,cluster nodes里看到所有节点都是disconnected状态。
在Docker或跨主机部署时,还要额外配置公告地址参数。因为容器内部和宿主机外部的网络视角不同,节点在Gossip消息里宣告的IP和端口,必须是其他节点能够访问的。这时候要用:
cluster-announce-ip 10.0.0.5 cluster-announce-port 6379 cluster-announce-bus-port 16379我见过最典型的报错是:容器正常启动,cluster info显示集群状态正常,但客户端从外部连接时老是被重定向到容器内网IP,连不上。十有八九就是忘了配cluster-announce-ip。
7.4 集群运维常用命令与故障定位思路
最后分享几个我平时定位集群故障最高频的命令。先把命令记熟,再遇到问题就有底气了:
# 查看集群节点状态,包括角色、slot、连接状态 redis-cli -h 127.0.0.1 -p 6379 cluster nodes # 查看集群整体信息,包括集群状态、slot分配数、节点数 redis-cli -h 127.0.0.1 -p 6379 cluster info # 查看slot与节点的对应关系 redis-cli -h 127.0.0.1 -p 6379 cluster slots # 重新分片(迁移slot) redis-cli --cluster reshard 127.0.0.1:6379 # 检查集群健康状态 redis-cli --cluster check 127.0.0.1:6379故障定位的核心思路,我总结成一句口诀:先看cluster info的cluster_state,再看cluster nodes里有没有fail状态的节点,最后用cluster slots确认路由是否完整。
如果cluster_state:ok但某些key访问报错,优先怀疑客户端是否开启集群模式;如果cluster_state:fail,大概率是某个slot没有master接管,先看有没有节点处于fail或handshake状态;如果一切看起来正常但性能上不去,看看是不是热点key都挤在了同一个slot,检查那个slot的访问量是否远高于其他slot。
我在实际操作中还有一个体会:升级Redis版本前一定要先在测试环境把集群完整跑一轮故障注入,包括kill主节点、断网、重启从节点、缩容扩容,确认版本的故障转移行为符合预期再上生产。这个习惯帮我避掉过好几次升级后偶发切换异常的坑。另外,给从节点分配读流量前,记得先发一条READONLY命令告诉节点"我要在从库读",否则就算连接到了从节点,它也会按集群规则把写请求和未本slot的命令重定向走,别到时候以为集群坏了,其实只是协议没搞对。