做 Redis Cluster 扩容这件事,很多人第一反应是“加一台机器跑个 reshard 不就完了”,但真正上手后会发现,从节点加入到槽位迁移,再到客户端感知,中间每一步都有值得掰开揉碎讲的细节。这篇就围绕 Redis Cluster 集群扩容过程做一次完整拆解:扩容前要看哪些指标、迁移量怎么算、reshard 背后到底发生了什么、中途会遇到哪些坑,以及我个人的一些排查经验和操作习惯。适合正在维护集群的运维和开发同学参考,也适合准备把集群横向扩展但还没动过手的团队。
1. 扩容前先把原理吃透:为什么加台机器不等于扩容
1.1 哈希槽:数据分布的基本单位
Redis Cluster 不像很多分布式系统那样用一致性哈希来做数据分片,而是引入了一个固定数量的哈希槽概念。整个集群固定有 16384 个槽,每条 key 通过CRC16(key) % 16384计算出它属于哪个槽,然后槽再映射到具体的 master 节点上。
这个设计有意思的地方在于,槽是“租户”,节点是“房东”。扩容、缩容、替换节点,本质上都是在调整槽和节点的归属关系,而不是让所有 key 重新计算位置。假如用一致性哈希,加一台机器往往要涉及大量 key 的重新分布,而且迁移边界是模糊的;哈希槽则把数据搬移的粒度固定下来,一次迁一个槽,迁移目标非常明确,工具也好实现。
为什么偏偏是 16384 而不是 65536 或者其他数字?主要是为了兼顾元数据大小和网络开销。每个节点在心跳包里都要携带自己负责的槽位信息,用一个 bitmap 来表达 16384 个槽只需 2KB 空间,放在 gossip 消息里代价很小。如果槽数量再翻几倍,虽然也能跑但通信开销会更重,对集群规模来说没必要。
扩容时你新增一个节点,最初它手里一个槽都没有。此时集群对外依然可以正常读写,只是所有请求还压在老节点上。真正的扩容动作,是后续把一部分槽从老节点迁移到新节点,让数据流量跟着槽走。所以理解扩容,先理解槽,这是最重要的地基。
1.2 节点之间怎么知道“你在迁槽”:gossip 与元数据同步
Redis Cluster 是去中心化架构,没有专门的协调者节点。每个节点都维护一份完整的集群视图,通过 gossip 协议互相通信:节点每 100ms 左右发送一次 ping/pong 消息,携带自己知道的节点状态、槽位归属、fail 标记等信息,逐步同步到整个集群。
新节点加入集群,并不需要你跑到所有老节点上去注册。你只要让新节点和集群里任意一个已存在的节点通过CLUSTER MEET建立联系,后续老节点会通过 gossip 把这个新面孔传播给其他节点。这个过程在实际操作里表现为:执行redis-cli --cluster add-node后,稍等片刻,CLUSTER NODES里就能看到所有节点都认识新来的了。
但这个机制的代价也要心里有数:节点越多,gossip 消息体越大,收敛时间越长。尤其是你一口气往集群里加好多台机器时,集群会有一段时间处于“元数据正在传播中”的状态,此时如果碰上节点抖动,误判 fail 的概率会比平时高。扩容不是一个瞬时动作,它有一个集群视图慢慢收敛的过程,过程分析里这块很容易被忽略。
1.3 “过程分析”到底要分析什么
我理解的扩容过程分析,核心是四个层面:节点加入、槽位迁移、元数据收敛、客户端感知。很多人只关注第二步“槽位迁移”,但真正影响业务的是第一步和第四步。
节点加入如果配置不对,比如cluster-enabled没开、端口被防火墙挡住、集群配置文件和别的节点冲突,连 MEET 都过不去。槽位迁移如果数据量估算不准,可能把新节点内存打爆,也可能压垮网络带宽。元数据收敛的问题在前一步说过,扩容越频繁,收敛窗口越容易被放大。客户端感知则更微妙:槽位归属变了,客户端缓存的路由表不会自动更新,除非它收到 MOVED 重定向,这中间会出现一段时间的重定向风暴和延迟抖动。
所以这篇文章后面讲的每一步,都是围绕这四个层面展开的。单纯会敲 reshard 命令不难,难的是知道每一条命令在集群里引发了什么连锁反应。
2. 扩容前的容量评估与风险检查
2.1 什么时候需要扩容:先看这 4 个指标
扩容不是拍脑袋的决定,也不是等报警了才动手。我一般会盯四个指标,任何一个持续触及警戒线,就开始筹备扩容。
第一个是内存水位。Redis 是内存数据库,内存水位是最硬的约束。单节点 used_memory 已经超过 maxmemory 的 70%,并且平滑曲线没有下降趋势,说明当前分片压力很大。第二个是 QPS 和 CPU。如果节点单核 CPU 已经打到 80% 以上,尤其是热点指令集中的场景,拆槽分流比单纯加副本更有意义。第三个是热点倾斜。某个节点的 keyspace 命中数明显高于其他节点,单纯增加 master 数量但迁移槽位时没有刻意绕开热点,扩完还是歪的。第四个是带宽与连接数。很多团队只看内存和 CPU,忽略了客户端连接数打满、网卡流量逼近上限的情况,这种情况下扩容同样能缓解网络争用。
需要注意的是,这四类问题不一定都能靠扩容解决。如果是大 key 造成的内存不均,扩容只是暂时缓解,根治还得拆 key;如果是慢查询导致的 CPU 打满,先把慢查询理清楚再说扩容的事。扩容解决的是“总量不够”和“分片粒度不够细”的问题,不是万能的。
2.2 迁移量怎么算:从一个三主集群说起
计算迁移量,很多人的直觉是“新节点平均该分多少槽就算多少”,但实际操作里要同时考虑槽数、内存、带宽三个变量。槽数和内存并不是严格线性对应的,因为不同槽位上的 key 大小可能差很多,所以更稳妥的做法是结合 used_memory 来估算。
举个例子,假设一个三主集群,16384 个槽平均分到三个节点上,因为 16384 不能被 3 整除,通常是一个节点 5461、一个 5461、一个 5462。现在扩到四主,理想目标是每个节点 4096 个槽,也就是从旧节点各迁出大约 1365 个槽给新节点。
但这里要问一句:这 1365 个槽上到底有多少数据?如果旧节点内存已经 12GB,按三节点来算总数据量约 36GB,扩到四节点后每节点应该 9GB,那么每个旧节点需要迁出约 3GB 数据。迁移期间新节点会被逐渐写入,峰值可能是 9GB 加上正在迁入的临时数据,所以新节点的 maxmemory 至少要留出 25% 的余量。旧节点迁出 3GB 之后内存会降下来,但迁移过程中旧节点本身的内存不会立刻释放,因为 key 是一批批迁的,前面迁完的内存空间可能被新写入占用,所以旧节点同样要留出缓冲。
带宽和时间也可以粗算。假设键平均大小 1KB,3GB 大概需要迁移三百万个 key。同机房内网 RTT 在 1ms 以内时,MIGRATE 的迁移速度能到每秒数万 key,三百万个 key 理论上只要几分钟到十几分钟;如果跨机房或者启用了 AOF 重写、副本全量同步,时间会成倍拉长。所以迁移时间一定要按最坏情况估,并且安排在业务低峰。
2.3 扩容前 10 分钟检查单
动手指敲命令之前,我强烈建议花十分钟过一遍检查单:
- 所有节点的 Redis 版本保持一致。混合版本集群虽然能跑,但不同版本的槽迁移机制细节有差异,出了问题不好排查。
- 备份必须存在。扩容虽说是在线操作,但万一某个环节出错,有备份心里不慌。至少对要迁出数据的源节点做一次 BGSAVE,或者确认从节点有完整数据。
- 确认业务低峰窗口。迁移会带来额外的网络 IO 和短暂的延迟抖动,不要在促销时段做。
- 确认新节点是干净的。
flushall后别忘了cluster reset,否则 nodes.conf 里残留旧身份,加入时会报节点 ID 冲突。 - 检查
cluster-require-full-coverage配置。默认是 yes,意味着只要集群里有任何槽没有被节点管理,整个集群会拒绝读写。扩容过程中如果操作不当,可能短暂触发这个保护机制,造成全局不可用。如果业务可以容忍部分 key 失败,考虑设为 no,但要在团队里明确其风险。 - 记录每个节点的 node-id 和当前槽位分布,作为回滚和校验的基准。
这些检查看起来琐碎,但每一条都在真实环境里坑过人。尤其最后一条,很多人扩容完发现数据没丢、集群也 ok,但有几个槽的归属和预期不一样,就是因为操作前没记录基准。
3. 在线扩容实操记录
3.1 新节点准备与加入
新节点要准备的事情不多,但每一样都别漏。配置文件里必须打开cluster-enabled yes,指定cluster-config-file路径,最好也设置cluster-node-timeout,比如默认的 15000ms 在大多数场景下够用。端口按规划设定,AOF 和 RDB 策略按集群内其他节点保持一致,避免新老节点持久化行为不一致。
启动后用redis-cli -p <port> cluster info会看到这个节点还处于cluster_state:fail状态,因为它的 nodes.conf 里只有自己,这正常。接下来执行加入操作:
redis-cli --cluster add-node <新节点ip:新端口> <任意老节点ip:端口>这里第二个参数是集群里任意一个存活节点,工具会通过它向新节点发送 MEET 消息。命令执行成功后,在任意节点上执行cluster nodes,能看到新节点已经变成 master,但是没有slots标记,也就是俗称的空 master。
如果新节点准备作为某个 master 的副本加入,可以带上参数:
redis-cli --cluster add-node <新节点ip:新端口> <任意老节点ip:端口> --cluster-slave --cluster-master-id <目标master的node-id>不过作为从节点加入后,它不参与槽位迁移,数据会通过主从复制自动同步。实际扩容场景里,我更推荐先把新节点作为空 master 加进来,完成槽位迁移后再按需要给它配副本,这样每台机器的角色边界更清晰。
3.2 槽位迁移的执行与观察
槽位迁移最省事的办法是直接用 reshard 子命令,但它有几个参数值得逐一说清楚。假设我们要从三个旧节点各迁一部分槽到新节点,先确认新节点的 node-id,然后执行:
redis-cli --cluster reshard <任意节点ip:端口> \ --cluster-from <旧节点1-id>,<旧节点2-id>,<旧节点3-id> \ --cluster-to <新节点-id> \ --cluster-slots 4096 \ --cluster-yes--cluster-from控制从哪些节点迁出,可以传多个 node-id 用逗号分隔,也可以传all表示从所有 master 迁出;--cluster-to是迁入目标;--cluster-slots是迁移的总槽数;--cluster-yes跳过交互确认。工具会尽量把槽平均地从各个源节点抽取,但如果槽上的 key 数量差异很大,这种“平均抽槽”可能导致内存分布不均,所以我更倾向于分批指定来源迁移,而不是一把 all。
迁移过程中最重要的观测窗口是cluster info里的几个指标:
redis-cli -p <任意节点端口> cluster info密切关注cluster_state:ok是否一直保持,cluster_slots_assigned是否始终是 16384,cluster_slots_migrating是否在逐步减少。另外用cluster nodes看新节点的槽列表是否在增长。
如果迁移的数据量很大,不要幻想着一条命令跑完就完事。我习惯拆成多次执行,每次迁移几百到一千个槽,跑完一轮观察一下内存、带宽、慢查询,一切平稳再继续下一轮。这样即使某个环节出问题,回滚范围也小。
3.3 迁移后的验证与收尾
迁移完成不等于扩容结束,验证和收尾同样关键。
第一件事是全局看槽位分布,执行:
redis-cli --cluster info <任意节点ip:端口>它会打印每个 master 持有的槽数,正常情况下应该是四个节点都接近 4096。如果某些节点多出几百个槽也不要慌,说明历史分布就不均匀,可以在后续用 rebalance 微调。
第二件事是逐节点看内存。分别登到每个节点执行info memory,对比 used_memory 是否和预估一致。如果新节点内存涨幅远超出迁移量,要警惕迁移过程中有应用写入倾斜,把大量新 key 直接写到了新节点上。
第三件事是验证关键数据。虽然槽迁移是原子的,但工具出 bug 或者中途强杀进程的情况也不是没有。我习惯用CLUSTER COUNTKEYSINSLOT对迁移前后的槽做一次 key 数量采样对比,或者直接跑一段业务探针脚本,验证核心读路径正常。
收尾时把本次扩容的节点、迁移槽位数、迁移前后内存对比记录到文档里,方便以后做容量规划时回溯。
4. 扩容过程的关键机制细节
4.1 MIGRATE 命令与逐 key 迁移
reshard 工具看起来是“迁移一个槽”,底层其实是把槽里的 key 一个个搬过去。核心命令是MIGRATE,它做的事是:从源节点把指定的 key(或一批 key)序列化后发送给目标节点,目标节点加载成功后,源节点再把本地副本删掉。整个过程对源节点来说是一个同步阻塞操作,迁移单个 key 期间,源节点处理该 key 的请求会短暂等待,但正常环境下这个时间是毫秒级,业务基本无感。
当工具要迁移一个槽时,会先通过CLUSTER GETKEYSINSLOT <slot> <count>从源节点拉取一批 key,然后用 MIGRATE 把这批 key 搬到目标节点。redis-cli --cluster reshard提供了--cluster-pipeline参数控制每次搬多少个 key,默认值在多数场景下够用,但如果你发现迁移速度上不去,适当调大这个值能减少命令往返次数。
这里有个容易忽略的性能细节:超大 key 会拖死整个迁移节奏。一个 10MB 的 key,MIGRATE 传输它时需要完整序列化并发送,期间源节点和网络都被这个单 key 占住,后面排队的 key 全要等。碰到这种场景,要么先把大 key 拆小,要么至少不要在高峰期做迁移,否则一条命令的阻塞可能带来秒级抖动。
4.2 MIGRATING/IMPORTING 与客户端重定向
槽迁移过程中,源节点和目标节点会分别进入特殊状态。源节点执行CLUSTER SETSLOT <slot> MIGRATING <目标节点-id>,意思是“这个槽正在往那边搬”;目标节点执行CLUSTER SETSLOT <slot> IMPORTING <源节点-id>,意思是“这个槽我正在接收”。
这两个状态决定了客户端请求的去向。对于源节点来说,如果请求的 key 还在本地,那就正常处理;如果 key 已经不在了,说明已经搬走或者本来就不存在,源节点会返回 MOVED,告诉客户端去找目标节点。对于目标节点来说,只有带 ASK 标记的请求才会被立即处理,否则它也会返回 MOVED,把客户端指回源节点。
这就产生了一个很有意思的中间态:某个 key 刚被搬走但整个槽还没完成切换时,客户端如果路由缓存没更新,就可能出现“源说去目标,目标说回源”的来回重定向,反映在业务上就是偶发的延迟升高和MOVED错误增多。这不是 bug,是集群在迁移过程中的正常现象,但你需要知道它存在,并且提前和业务团队打好招呼。支持 cluster 的智能客户端一般会通过 MOVED 更新本地槽映射,一段时间之后就稳定了,真正危险的是那些老旧的客户端,它们可能不仅不更新缓存,还会把 MOVED 当作异常直接抛错。
4.3 槽位归属切换:SETSLOT 的同步艺术
当一个槽下的 key 全部搬完之后,还有最后一步:把槽的所有权正式从源节点切换给目标节点。这一步靠CLUSTER SETSLOT <slot> NODE <目标节点-id>完成,而且源节点和目标节点都要执行一遍。因为 Redis Cluster 没有中心化存储,每个节点维护自己的视图,只有相关节点都更新了槽归属,集群才不至于出现“源以为自己还有这个槽、目标以为这个槽归自己”的元数据分裂。
reshard 工具会自动执行这两次 SETSLOT,但如果你在某次迁移中断后想手动恢复,一定要记住命令顺序。常见做法是先在目标节点执行SETSLOT <slot> NODE <target-id>,让目标节点把槽正式接管,然后回到源节点执行同样的命令,让源节点放弃槽归属。顺序不能反,否则源节点已经放弃槽、目标还没接管,中间会留下一个没有任何节点认领的槽,在cluster-require-full-coverage yes的环境下,整个集群会瞬间拒绝服务。
这也是为什么我前面建议记录操作前的节点和槽位分布。真到了需要手动修复的阶段,有记录能少走很多弯路。
5. 扩容中的常见问题与避坑实录
5.1 问题速查表
| 症状 | 可能原因 | 处理思路 |
|---|---|---|
| 新节点加入时报 node-id 冲突 | nodes.conf 里的旧身份没清掉 | 执行cluster reset再重新加入 |
| 迁移过程中业务出现大量 MOVED | 客户端缓存未及时刷新 | 确认客户端支持 cluster 并正确实现路由更新 |
| 集群突然全部不可写 | 某个槽临时无归属,且cluster-require-full-coverage yes | 检查cluster info,手工补 SETSLOT;考虑修改配置 |
| 迁移非常慢,但 key 数量不多 | 存在超大 key | 定位大 key,先拆分或调大 MIGRATE 超时参数 |
| 新节点内存上涨远超预期 | 应用新写入大量落在新节点 | 观察写入分布,必要时调整槽位配比 |
| 扩容过程中出现 failover | gossip 收敛压力大或节点抖动 | 检查网络和cluster-node-timeout,避免迁移期切换 |
| 迁移完成后槽数不均衡 | 历史分布本来就不均匀 | 用redis-cli --cluster rebalance做二次均衡 |
5.2 三个容易翻车的场景
场景一:大 key 拖死迁移。有一次我在一个测试集群里迁移一个包含几十万元素的 hash,单个 key 就有几十 MB,MIGRATE 搬它的时候源节点几乎卡住,后面的小 key 全部排队。排查的时候看latency和网络流量才定位到问题。后来遇到大 key,我都是先在前置检查里扫一遍DEBUG OBJECT或者用采样工具找出 key 大小异常的对象,该拆的拆,该提前处理的提前处理,而不是指望迁移时运气好。
场景二:cluster-require-full-coverage 引发的雪崩。某个团队把集群配成默认的 yes,然后缩容操作时先删了一个节点的槽,但没在目标节点完成 SETSLOT,导致集群短暂出现“无主槽”。结果整个集群的读写全部被拒,比单点故障还可怕。这件事给我的教训是:生产集群如果业务不能接受全局不可用,这个参数必须经过充分评估再决定是否改成 no,同时所有扩容缩容操作都要有可回滚的步骤,而不是依赖工具不出错。
场景三:迁移期间误触发 failover。集群扩容时节点数量变化,gossip 消息变大,如果网络本身有抖动,节点之间的心跳超时概率会上升,容易触发从节点自动提升。迁移过程中如果发生 failover,槽位归属和复制关系同时变化,排查起来非常痛苦。我在实际运维中遇到这种情况,会适当调大cluster-node-timeout或者在迁移期间暂停自动故障转移流程,等集群完全稳定再恢复,相当于给集群加了一层保险。
5.3 我的几点经验原则
多次扩容做下来,我总结了几条个人经验,不一定适用于所有团队,但可以当参考。
扩容一定安排在低峰期,哪怕是在线迁移,也要给可能出现的抖动留缓冲。迁移尽量分批,每次只迁一部分槽,观察完内存、带宽、客户端错误率再继续。宁可慢,不要一把梭。新节点的内存规格不要卡着预算来,留出 20% 以上的余量,因为你很难准确预测迁移期间的新增写入。迁移完成后不要立刻投入大流量,先观察 10 到 15 分钟,再用cluster info确认没有残留的 migrating 或 importing 状态。
还有一点我想专门提醒:扩容后如果发现槽数均衡但各节点内存差异很大,不要硬靠 rebalance 去追求内存均衡,先找数据倾斜的根源,比如热点 key 或 hash tag 的使用。否则你今天把内存调均衡了,明天流量一大又会歪掉。
我在实际扩容中还有一个比较受益的习惯:每次完成后,把节点 down 机或替换的演练也一起做一遍。因为扩容改变了槽位分布,之前某个节点挂了由哪个从节点顶上、客户端路由什么时候恢复,这些默认行为都值得重新验证一遍。扩容不是终点,扩完之后集群的容灾边界已经变了,这才是“过程分析”最后应该落到的地方。