Redis 集群这块东西,我最早接触是在一个缓存命中率死活上不去、单节点内存天天告警的项目里。那时候团队的第一反应是“加机器、上主从”,结果主从是配上了,写压力还是全压在 master 上,扩容也只是读扩展,写瓶颈一点没动。后来才真正把 Cluster 这套分片机制吃透,才发现很多人对它的理解停留在“把数据切开存到多个节点”这一层,而真正决定它好不好用的,是哈希槽怎么分、请求怎么路由、故障怎么转移、以及客户端到底懂不懂重定向。这篇就把我这些年踩过的坑、调过的参数、排过的故障,按一个能直接抄作业的思路完整讲一遍。
1. 先搞清楚 Cluster 到底解决什么问题
1.1 单机与主从的天花板在哪
很多人上手 Redis 的第一套高可用方案就是主从加哨兵。这套东西解决的是“读扩展”和“故障自动切换”,但它有一个绕不过去的硬伤:所有写请求最终都落到同一个 master 上。你加再多从节点,写的吞吐和单机内存上限还是被那一台机器卡死。我见过一个场景,业务方把几千万个会话 key 全塞进一个实例,内存冲到 60 多 G,主从复制延迟一度飙到十几秒,哨兵切换的时候直接丢了一批写请求。
Cluster 的思路完全不同。它把整个数据集按16384 个哈希槽(hash slot)切开,每个节点负责一部分槽位,写请求根据 key 落到对应槽的节点上。这样一来,写压力被分散到多个 master,内存上限也随节点数线性增长。你可以理解为:主从是“一个老板带几个助理”,助理只能帮忙看文件不能签字;Cluster 是“几个老板各管一摊业务”,每个人都能独立收单。
1.2 哈希槽机制的核心逻辑
Cluster 没有用一致性哈希,而是用了固定 16384 个槽的取模方案。计算方式是:
slot = CRC16(key) mod 16384为什么是 16384 而不是 65536?这是官方权衡后的结果。槽位数量直接决定集群元数据的大小,每个节点都要维护一份“哪个槽属于哪个节点”的映射表。16384 个槽用 bitmap 表示大约 2KB,节点间心跳包能塞得下;如果换成 65536,心跳包体积会明显变大,网络开销和 gossip 传播成本都上去了。这个数字不是随便定的,是吞吐和元数据开销之间的平衡点。
槽的分配是集群初始化时决定的。三个 master 的典型分法是 0-5460、5461-10922、10923-16383,均分 16384。你可以手动调整,比如把热点槽集中到性能更好的机器上,但要注意每个节点至少要有一个槽,否则它不参与集群投票。
1.3 什么场景该上 Cluster,什么场景别碰
Cluster 不是银弹。我总结下来,数据量大、写并发高、能接受一定运维复杂度的场景适合上;而事务多、多 key 操作频繁、对延迟极度敏感的场景要慎重。
具体来说,如果你的业务里有大量MGET、MSET、SUNION这类跨 key 操作,Cluster 会因为 key 不在同一个槽而直接报CROSSSLOT错误。解决办法是用hash tag,比如user:{1001}:name和user:{1001}:age,大括号里的内容相同,CRC16 计算时就只算大括号内的部分,保证它们落在同一个槽。但这又带来一个新问题:如果大量 key 用了同一个 tag,它们会全挤到一个节点上,分片就失去意义了。所以 hash tag 要克制使用,只给真正需要原子操作的 key 组加。
2. 集群搭建:从零到可用的完整流程
2.1 节点规划与端口约定
Cluster 模式下每个节点要开两个端口:一个是给客户端用的服务端口(比如 7000),另一个是集群总线端口(服务端口 + 10000,即 17000),用于节点间 gossip 通信、故障检测、配置传播。这个 10000 的偏移量是硬约定,防火墙规则千万别只放服务端口,否则节点之间握手都握不上,集群永远处于fail状态。
我一般建议至少 3 主 3 从,也就是 6 个节点起步。为什么是 3 主?因为集群的故障转移需要多数派投票,3 个 master 里挂 1 个还能选出新主,挂 2 个整个集群就不可用了。从节点数量看你的读扩展需求,但每个主至少配 1 个从,否则主挂了没有候选者接管。
节点规划表大致长这样:
| 节点角色 | 服务端口 | 总线端口 | 说明 |
|---|---|---|---|
| master-1 | 7000 | 17000 | 负责槽 0-5460 |
| master-2 | 7001 | 17001 | 负责槽 5461-10922 |
| master-3 | 7002 | 17002 | 负责槽 10923-16383 |
| slave-1 | 7003 | 17003 | 复制 master-1 |
| slave-2 | 7004 | 17004 | 复制 master-2 |
| slave-3 | 7005 | 17005 | 复制 master-3 |
2.2 配置文件的关键参数
每个节点的配置文件里,Cluster 相关的参数必须显式打开,否则节点启动后是普通单机模式。核心配置如下:
port 7000 cluster-enabled yes cluster-config-file nodes-7000.conf cluster-node-timeout 15000 appendonly yes这里有几个参数值得展开说。cluster-config-file是节点自己维护的集群状态文件,不要手动改它,节点重启时会读这个文件恢复集群拓扑,手改容易把集群搞乱。cluster-node-timeout是故障判定超时,默认 15000 毫秒,意思是节点间心跳超过这个时间没响应就标记为疑似下线(PFAIL)。这个值调太小会误判,网络抖一下就触发故障转移;调太大故障恢复慢。内网环境 15000 比较稳,跨机房部署要适当加大。
appendonly yes不是 Cluster 专属,但在集群里我更推荐开 AOF。因为故障转移时从节点提升为主,如果只靠 RDB,最后一次快照之后的数据就丢了。AOF 的 everysec 策略最多丢 1 秒数据,对大多数业务可接受。
2.3 用 redis-cli 组建集群
配置写好、节点都启动之后,用redis-cli --cluster create一条命令就能组集群:
redis-cli --cluster create \ 127.0.0.1:7000 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 \ --cluster-replicas 1--cluster-replicas 1表示每个主配一个从。命令执行后,redis-cli 会自动分配槽位、指定主从关系,然后打印一份分配方案让你确认。确认后集群就建好了。
注意:这条命令里的节点顺序有讲究。前三个会被优先选为 master,后三个作为 slave 分配。如果你把顺序打乱,可能出现主从交叉分配不符合预期的情况。建议按“先所有 master,再所有 slave”的顺序写。
组好之后用redis-cli --cluster check 127.0.0.1:7000验证,输出里会显示每个节点的槽位范围、主从关系和集群状态。看到[OK] All 16384 slots covered才算真正可用。
2.4 动态扩缩容:加节点与迁槽
业务增长要加节点时,流程分两步:先把新节点加入集群,再迁移槽位过去。
# 加入一个空 master redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 # 从已有节点迁 1000 个槽到新节点 redis-cli --cluster reshard 127.0.0.1:7000 \ --cluster-from <source-node-id> \ --cluster-to <new-node-id> \ --cluster-slots 1000 \ --cluster-yesreshard 的过程是在线迁移,槽里的 key 会逐个搬到目标节点。迁移期间客户端访问正在迁移的槽,如果 key 还在源节点,源节点会返回ASK重定向;如果已经搬走,返回MOVED。这个过程对业务基本透明,但迁移大批量 key 时会占用网络和 CPU,建议放在低峰期做。
缩容就是反过来:先把要下线节点的槽全部迁走,再用del-node移除。
redis-cli --cluster del-node 127.0.0.1:7000 <node-id>实操心得:迁槽之前一定要确认目标节点内存够用。我踩过一次坑,迁了 2000 个槽过去,结果目标节点内存直接打满触发 OOM,key 被淘汰了一批。迁槽前用
redis-cli --cluster check看下各节点的 key 分布和内存占用,心里有数再动手。
3. 请求路由与重定向:客户端到底经历了什么
3.1 MOVED 与 ASK 的区别
这是 Cluster 里最容易混淆的一对概念,但理解它们对排查问题至关重要。
MOVED表示“这个槽永久归另一个节点管,你以后直接找它”。客户端收到 MOVED 后应该更新本地的槽位映射表,下次同样的 key 直接发到正确节点。
ASK表示“这个槽正在迁移中,这个 key 现在在目标节点,但槽的归属还没变”。客户端收到 ASK 后,这次请求要转发到目标节点,但不能更新本地映射表,因为迁移完成后槽的归属才正式变更。
打个比方:MOVED 像是“我搬家了,以后寄这个地址”;ASK 像是“这个东西暂时放在邻居家,你去他那拿,但我还没正式搬”。
3.2 智能客户端 vs 普通客户端
普通客户端(比如没开集群模式的 Jedis 单机版)每次遇到重定向都要多一次网络往返,性能很差。智能客户端会在本地缓存一份槽位映射表,直接算好 key 该去哪个节点,只有映射表过期时才触发重定向更新。
主流客户端的集群支持情况:
| 客户端 | 集群支持 | 本地缓存 | 备注 |
|---|---|---|---|
| Jedis | 支持 | 有 | 需用 JedisCluster |
| Lettuce | 支持 | 有 | 基于 Netty,异步性能好 |
| redis-py | 支持 | 有 | 用 RedisCluster 类 |
| go-redis | 支持 | 有 | 生产环境常用 |
选客户端的核心看两点:是否维护本地槽位缓存、是否支持拓扑自动刷新。没有本地缓存的客户端,每次请求都可能多一跳,QPS 高的时候延迟会明显放大。
3.3 重定向风暴的成因与规避
重定向风暴是指大量请求同时收到 MOVED,客户端频繁刷新映射表,导致请求在节点间来回弹。常见诱因有两个:一是集群刚做完 reshard,槽位映射大面积变更;二是客户端缓存刷新策略太激进,一收到 MOVED 就全量拉取拓扑。
规避办法:客户端配置里把拓扑刷新间隔设一个合理值,比如 30 秒到 1 分钟,不要每次 MOVED 都全量刷新。另外 reshard 尽量小批量、分多次做,别一次性迁几千个槽。
4. 故障转移与一致性取舍
4.1 故障检测的完整链路
一个 master 挂掉到新 master 接管,中间经历了好几个阶段。首先是节点间心跳超时,其他节点把它标记为PFAIL(疑似下线)。当超过半数 master 都标记它为 PFAIL,就升级为FAIL(确认下线)。这时它的从节点发起选举,请求其他 master 投票,拿到多数票后提升为新 master,接管原主的槽位,并广播新的拓扑。
整个过程受cluster-node-timeout控制,默认配置下大概十几秒完成。想加快可以调小超时,但要权衡误判风险。
4.2 脑裂与数据丢失的边界
Cluster 用的是异步复制,主节点写完就返回成功,不等从节点确认。这意味着主挂掉的瞬间,还没同步到从节点的写请求就丢了。这是 Cluster 为了性能做的取舍,无法完全避免。
更麻烦的是脑裂:网络分区导致一部分节点以为主挂了、选出新主,另一部分节点还认为老主活着。等网络恢复,两个主可能都写过数据,合并时就会冲突。Cluster 的应对是少数派分区停止服务:如果某个分区的 master 数量不过半,它拒绝写请求,避免产生分叉数据。这个机制叫cluster-require-full-coverage相关的一致性保护,理解它有助于你在网络抖动时判断集群行为是否符合预期。
4.3 手动故障转移与运维操作
有时候需要主动把主切到从,比如给主节点做维护。用CLUSTER FAILOVER命令在从节点上执行:
# 在从节点上执行,强制提升为主 redis-cli -p 7003 cluster failover默认是强制模式,会直接切换,可能丢少量数据。加TAKEOVER参数更激进,连主节点是否同意都不问。生产环境做计划内维护,建议用默认模式,让主从先做一次数据同步再切,减少丢失。
注意事项:手动 failover 之前,确认从节点的复制偏移量和主节点差距不大。用
INFO replication看master_repl_offset和slave_repl_offset,差距越小切换越安全。
5. 常见故障排查速查
5.1 集群状态异常排查表
| 现象 | 可能原因 | 排查命令 | 解决方向 |
|---|---|---|---|
| cluster_state:fail | 有槽未分配 | cluster info | 检查槽覆盖,重新分配 |
| 节点一直握手失败 | 总线端口不通 | telnet 总线端口 | 放通防火墙 |
| 大量 MOVED | 槽位映射过期 | cluster nodes | 客户端刷新拓扑 |
| CROSSSLOT 错误 | 多 key 不同槽 | 检查 key 命名 | 用 hash tag |
| 主从频繁切换 | 网络抖动 | 看日志 PFAIL | 调大 node-timeout |
5.2 几个我踩过的真实坑
第一个坑是总线端口没放通。当时服务端口都通了,节点也能启动,但cluster info一直显示fail,折腾半天才发现是 17000 系列端口被防火墙拦了。集群节点之间握不上手,永远组不成集群。
第二个坑是hash tag 用过头。有个业务为了图省事,把所有 key 都加了同一个 tag,结果全落在一个节点上,分片形同虚设,那个节点内存直接爆掉。hash tag 是给需要原子操作的小 key 组用的,不是全局方案。
第三个坑是客户端没开集群模式。用单机客户端连集群,遇到 MOVED 不会自动跳转,直接报错。这个错误信息其实很明确,但新手容易忽略,以为是集群本身有问题。
5.3 性能调优的几个抓手
Cluster 的性能瓶颈通常不在分片本身,而在跨节点操作和大 key。几个调优方向:
- 避免跨槽的多 key 命令,能用 pipeline 就用 pipeline 减少往返
- 大 key 拆分,一个几 MB 的 hash 拆成多个小 hash,避免单节点阻塞
- 合理设置
cluster-node-timeout,内网 15 秒,跨机房适当加大 - 监控每个节点的内存和 QPS,避免槽位分配不均导致热点
6. 我个人的一些实操体会
Cluster 这套东西,配置本身不难,难的是理解它的边界。它不是把单机 Redis 简单复制几份,而是引入了一套分布式的槽位管理和故障转移机制,代价就是跨槽操作受限、事务支持弱化、运维复杂度上升。
我的建议是:如果你的数据量在单机内存能扛住的范围内,写 QPS 也没到瓶颈,别急着上 Cluster,主从加哨兵足够用,运维简单得多。真到了单机扛不住的时候再上,而且要提前把客户端选型、hash tag 规范、监控告警这些配套做好,否则集群建起来容易,稳定跑起来难。
最后分享一个小技巧:集群上线前,用redis-cli --cluster check和--cluster info各跑一遍,把槽位分布、主从关系、内存占用都截图存档。后面出问题的时候,有这份基线数据对比,排查效率会高很多。这个习惯我保持了几年,救过好几次场。