实际工作中,Redis Cluster的数据迁移大概率出现在三种情形里:集群扩容想加节点分担压力、集群缩容要下线低负载节点、节点间数据倾斜时想把热点槽从高负载节点挪走。不管是哪种,迁移一旦做不好,轻则线上毛刺变多、命令超时,重则大范围访问失败。这篇文章不打算复述官方文档,而是把整个迁移动作拆开讲清楚:迁移的触发时机怎么判断、底层槽迁移是怎么回事、实操时每一步该敲什么命令、踩过的坑有哪些,以及迁移结果怎么验证才算真正安全。适合正在维护Redis Cluster、准备做扩缩容,或者已经被线上事故教训过的同学参考。
每次做迁移,我心里其实比任何时候都紧张,因为这段操作既要动数据又要动路由,还必须在业务不停摆的前提下完成。下面写的基本是我在真实环境里摸出来的流程和教训,照着做至少能避开大部分雷区。
1. 什么情况下必须做数据迁移:触发信号与前置检查
1.1 三类典型触发场景
第一类是节点内存告警。监控里最常看到的是 used_memory 持续逼近 maxmemory。Redis Cluster虽然把数据分散到了多个节点,但每个节点最后还是会撞到自己的内存上限。当节点内存水位超过85%,内存淘汰就开始频繁触发,热点key可能被提前淘汰,缓存命中率肉眼可见地往下掉。这时候与其一直给单节点扩容内存,不如增加节点,把一部分槽挪过去。
第二类是热点数据倾斜。有些业务用hash tag把一类key固定在同一个槽里,比如一个订单的所有子项都放在同一个tag下,结果这个槽成了热点。监控里能看到某个节点的CPU、网络入口带宽明显比其他节点高一个量级,命令延迟跟着涨。这种倾斜没法靠扩内存解决,正确的办法是把热点槽迁移到空闲节点,或者干脆再做一层拆分。
第三类是缩容。业务收缩、资源回收、把多套小集群合并,都会遇到要把某个节点从集群里摘掉的情况。缩容不是把redis进程停掉那么简单,所有槽都得先迁走,不然整个集群的槽覆盖就不完整了。
1.2 动手前必须确认的前置条件
我在迁移前有张固定检查清单,每一项都会在监控里核对一遍,缺一项都不敢动手。
版本一致性。源节点和目标节点的Redis主版本尽量保持一致,至少不应该出现一边是6.x一边是4.x还直接跨大版本迁移的情况。不同版本对数据结构有不同编码方式,迁移过去没问题,万一要回滚,旧版本可能读不了新版本的编码。实际上大多数线上扩缩容发生在同一集群内,版本基本都是统一管理的,但跨集群迁移时这个坑非常容易踩。
内存余量。迁移过程中数据会短暂双份存在——目标节点已经写入了一份,源节点还没删。目标节点必须预留足够内存,我一般要求目标节点在迁移窗口内的峰值内存不超过maxmemory的70%。如果目标节点自己都快满了,迁移过程中RESTORE命令会触发淘汰,刚迁过去的key可能马上被清掉。
从节点与复制健康度。迁移时源节点要处理额外的序列化和网络发送,主从复制缓冲区也可能因为命令积压而增大。如果从节点本来就严重落后,迁移操作会让主从差距雪上加霜。动手之前先看CLUSTER NODES,确认每个主节点都有健康的从节点,主从复制偏移量差距在可控范围内。
网络带宽。这是最容易被忽略的一项。MIGRATE是同步操作,每个key的迁移都涉及源、目标节点之间的一次网络往返。批量迁移时瞬时带宽消耗很可观。迁移前我会看当前节点的带宽使用率,如果已经超过网卡上限的60%,要么错峰,要么降低迁移并发。
1.3 迁移窗口和QPS水位怎么定
迁移窗口的选择直接决定线上影响程度。我的经验是看两个指标:QPS峰值和客户端超时阈值。
一般选QPS最低的时间段,比如凌晨1点到4点。但光看绝对QPS还不够,还要看此刻是否有批量任务在跑。很多公司凌晨有定时任务、报表统计、数据回刷,这些任务跑起来Redis压力比白天还猛。最稳妥的办法是看历史7天的QPS曲线,找连续低水位持续2小时以上的窗口。
另一个指标是客户端的超时阈值。如果客户端配置的Redis命令超时是200ms,迁移期间源节点阻塞超过这个时间,线上就会开始报错。所以迁移速率要反过来用超时阈值推算,这点在后面实操部分细说。
2. 迁移的最小单位是槽:分片规则和MIGRATE命令的底层行为
2.1 从CRC16到16384个槽:数据与节点的映射关系
理解迁移为什么要以"槽"为单位,得先理解Redis Cluster的分片规则。Redis Cluster把整个键空间固定分成16384个槽,每个key通过 CRC16(key) % 16384 计算出它属于哪个槽。槽再被分配到各个主节点上。
很多人问,为什么迁移不直接以节点为单位?因为槽才是数据分布的粒度。一个节点的负载是由它承载的槽决定的,槽的分布是可以细调的。比如新加一个节点,不需要把整个旧节点的数据都复制过来,只需要从多个节点各取一部分槽就能均衡负载。反过来,缩容时也只把要下线节点的槽平均分给其他节点,而不是把整块数据搬过去。
集群里每个节点都保存了完整的槽映射表,也就是从"槽编号→节点ID"的全部映射关系。这个映射表是最终一致的,在日常状态每个节点的视角基本一致,只有在迁移过程中才会出现临时差异,这就是后面要说的MOVED和ASK状态。
2.2 MIGRATE执行一次key迁移时发生了什么
Redis Cluster把槽从一个节点迁到另一个节点,不是一条命令搞定的,而是分成两个阶段:先搬运这个槽里的所有key,再修改槽的归属。
搬运key靠的是MIGRATE命令。它的核心行为是:把源节点的key序列化成Redis在磁盘上持久化用的RDB格式,直接通过网络发送给目标节点,目标节点收到后执行RESTORE命令写入,成功后源节点删除本地这个key,整个过程是原子的、同步的。
这里有个容易误解的点:MIGRATE虽然名字里带"迁移",但它执行期间整个源节点实例是阻塞的,因为Redis是单线程事件循环。一个几KB的小key迁移,加上网络往返可能只要1-2毫秒,但一个几MB的大key迁移就要几百毫秒乃至更久,这期间源节点上所有其他命令全部排队等待。这也是整个数据迁移里风险最高的窗口。批量迁移多个key时,MIGRATE支持KEYS参数,可以在一次命令里携带多个key,大幅减少网络往返次数,这也是控制迁移延迟的核心手段之一。
2.3 读写请求在迁移期间的MOVED/ASK路由变化
槽的迁移不是瞬间完成的。在某个槽的key还没全部搬完时,这个槽对源节点来说处于"正在迁出"状态,对目标节点来说处于"正在导入"状态。
这个状态由两条命令设置:
- 源节点:CLUSTER SETSLOT MIGRATING <target_node_id>
- 目标节点:CLUSTER SETSLOT IMPORTING <source_node_id>
设置了MIGRATING状态后,如果客户端向源节点请求这个槽的key,源节点会先查key是否还在本地。在的话直接返回,不在的话返回ASK错误,提示客户端去目标节点读。目标节点收到ASK后,请求里还需要带ASKING标记,否则目标节点会认为槽还不归自己管而拒绝。
当这个槽的所有key都迁移完成,集群会广播一条CLUSTER SETSLOT NODE <target_node_id>,槽的归属正式变更。之后客户端再访问源节点,会收到MOVED错误,MOVED告诉客户端槽的新归属,客户端需要更新本地路由缓存并转向新节点。
大多数语言客户端都内置了ASK和MOVED的重定向处理逻辑,但这个逻辑在某些封装不完善的客户端里是有问题的。我见过有的客户端MOVED处理不更新路由缓存,导致每次请求都先访问旧节点再被重定向,延迟翻倍。迁移前最好在压测环境用真实客户端跑一轮,确认重定向处理正常。
3. 在线扩缩容实操:从添加节点到槽位重平衡的完整命令流
3.1 新节点加入集群与主从关系落地
扩容第一步是把新节点加进集群。假设新节点已经装好Redis、监听在6379端口,执行:
redis-cli -c -h 任意集群节点 -p 6379 CLUSTER MEET 新节点IP 新节点端口CLUSTER MEET执行后,新节点会通过gossip协议和集群内其他节点交换信息,逐步学习到完整的拓扑。几秒钟后CLUSTER NODES里就能看到它,只是它还处于无槽、无主从归属的状态。
如果只是想增加副本数,直接让新节点做某个主节点的从节点:
redis-cli -h 新节点IP -p 6379 CLUSTER REPLICATE 目标主节点ID它会开始全量同步,同步完成就变为从节点。
但如果是扩容分担压力,最终目标是让新节点成为主节点并分到槽。此时新节点如果没有潜在的从节点,可以直接做主节点;如果新集群里每个主节点都应该带从节点,更好的做法是让新节点先作为某个主节点的从节点,数据同步完成后用CLUSTER FAILOVER将其手动提升为主节点,那么原主节点反过来可以做新节点(现在的新主)的从节点,或者原主节点再挂到其他副本下。这个过程本质是把主节点身份整体迁移到新机器上,适合数据量很大的集群快速完成扩容,缺点是没有真正打散数据分布。
3.2 redis-cli --cluster reshard的槽数拆分与分批策略
真正让数据均匀分散到扩容后的节点上,要用reshard重新分配槽。官方客户端redis-cli提供了子命令:
redis-cli --cluster reshard 集群任意节点IP:端口 \ --cluster-from 源节点ID \ --cluster-to 目标节点ID \ --cluster-slots 需要迁移的槽数量 \ --cluster-yes不指定--cluster-from时它会让你交互式选择,也可以指定多个源节点ID,用逗号分隔,工具会自动按比例从每个源节点取槽。
槽数量怎么定,这非常关键。我推荐分批策略:每批次迁移的槽数量不超过目标节点总槽数的10%。比如集群里有3个主节点各5461个槽,单批次迁500个槽以内,迁完观察内存、QPS、慢查询再决定是否继续。
为什么不能一上来迁两千个槽?因为--cluster slots的实现是连续执行每个槽的key搬运。迁移过程中源节点会持续被MIGRATE命令阻塞,虽然key级别阻塞时间很短,但几千个槽下来队列积压会非常严重。更科学的控制方式是把大批任务拆小,每次跑完都留出一段观察窗口,让延迟指标恢复基线再继续。
迁移过程中可以用CLUSTER SLOTS实时查看槽的分布,也可以中途Ctrl+C中断,已经迁移完成的槽不会回滚,未迁移的槽保留原状。这种机制让我可以在发现指标异常时立刻刹车。
3.3 缩容下线的正确顺序:先迁槽再下线进程
缩容和扩容顺序正好反过来,容易出事的都是想把节点先停掉再处理数据。正确的顺序是:先把要下线节点上的所有槽迁走,确认它变成无槽节点,再把它从集群里移除,最后才停进程。
用官方工具迁移所有槽:
redis-cli --cluster reshard 集群节点IP:端口 \ --cluster-from 待下线节点ID \ --cluster-to 接收节点ID \ --cluster-slots 待下线节点的槽数 \ --cluster-yes槽全部迁走之后,有一个细节不能漏:这个节点如果还挂着从节点,要把从节点重新分配到其他主节点,或者也一并下线。否则它自己的从节点会因为没有主节点而进入异常状态。
然后从集群中移除节点,注意不是只在一两个节点上执行,而要在集群内所有存活节点上执行:
redis-cli -c -h 集群任一节点 -p 端口 CLUSTER FORGET 待下线节点ID官方客户端也提供了简洁方式:
redis-cli --cluster del-node 集群节点IP:端口 待下线节点IDdel-node内部会先检查该节点是否有槽,有槽直接拒绝删除。只有全部槽迁移完之后才能删。
全部节点执行完CLUSTER FORGET之后,再停掉待下线节点的Redis进程。这个顺序能保证集群在任何时刻的槽映射都是完整可用的,即使中途某个操作失败,集群仍然对外服务。
4. 迁移结果怎么验证:校验方法、残留清理与回滚兜底
4.1 集群层面检查:槽覆盖完整性、节点健康与内存水位
迁移完成不代表万事大吉,还要验证。第一层是集群拓扑层面:
redis-cli --cluster check 集群节点IP:端口 redis-cli --cluster info 集群节点IP:端口check会逐个节点扫描,检查16384个槽是否全部被覆盖、主节点是否有从节点、从节点同步是否正常。info会汇总所有节点的槽数量、内存使用、从节点数量。
这两个命令过了,只说明集群结构没问题,不代表数据内容正确。尤其跨集群迁移、迁移过程中业务还在写入的场景,内容校验必不可少。
4.2 数据内容抽样校验:SCAN采样与TTL对比
内容校验我的做法是抽样+重点验证结合。
先抽样:迁移前后各跑一轮SCAN,按同样的前缀遍历规则采样一定数量key,比对源节点和目标节点上这些key的value是否一致、TTL是否一致、类型是否一致。SCAN虽然不阻塞,但每次都读很多key还是会增加负载,建议采样的key数量控制在百万分之一量级,或者按关键业务前缀抽。
注意一个坑:MIGRATE不会把TTL原样带过去,它会保留相对TTL而不是绝对TTL。具体来说,迁移完成那一刻,key的TTL会少掉迁移所花的时间。两个节点分别读TTL时,由于读取时间不同,数字略有差异是正常的,比较时要用绝对过期时间戳(当前时间+TTL)而不是直接比较剩余秒数。
重点业务前缀要做全量key存在性校验。比如订单缓存前缀order:*有多少个key,源节点SCAN一遍得到数量,迁移后目标节点SCAN一遍得到数量,两边要严格相等。如果数量差了几个,就要逐个定位差异key,大部分情况是迁移过程中业务写入的新key,需要结合业务逻辑判断是否要补迁。
4.3 回滚策略:RDB兜底、反向reshard与业务双写
迁移前必须想好回滚方案。我的兜底做法是迁移前对源节点执行BGSAVE,把RDB文件留存。RDB不是用来做实时回滚的,它是最后一条防线:万一迁移过程中出现不可恢复的数据错乱,可以停掉业务、把源节点用RDB恢复,损失掉RDB时间点之后的增量写入。
常规的回滚其实没那么复杂,就是反向执行一次reshard,把槽从目标节点迁回源节点。前提是数据没有在源节点和目标节点上产生冲突。迁移后如果业务已经正常读写目标节点,源节点上的老key是残留,反向迁移时这些旧key和源节点上已经存在的数据可能都是脏的,需要先用前面提到的校验手段判断回滚是否会覆盖新数据。
最稳妥的回滚路径其实是业务双写。对于跨集群迁移这类大动作,我宁可在迁移窗口内让业务同时写新旧两个集群,迁移完成后再通过数据对比确认一致,最后把读流量切到新集群。这个方案对业务改造要求高,但回滚非常干净,切回旧集群只需把流量切回来,数据不会分裂。
当然,纯在线扩缩容场景不需要双写,反向reshard已经足够。但要记住:反向迁移也需要同样的前置检查和分批策略,很多人回滚时手忙脚乱,反而把原本正常的集群迁坏了。
5. 真实环境踩坑实录:大key阻塞、批量超时与内存双份
5.1 大key迁移导致的全集群抖动:一次完整排查链路
那次线上事故我印象很深。下午在给一个节点做扩容,迁移刚开始十几分钟,监控平台突然爆出一片延迟告警,所有访问某个节点的客户端命令延迟从2ms飙到2秒以上,Redis日志里全是慢查询告警。
当时的排查思路是:先看慢查询,发现大量MIGRATE命令耗时在300ms以上,再往上游看,发现有几十个几百KB甚至几MB的key正在被迁移。因为MIGRATE是同步命令,这些大key连续迁移时,每个key都阻塞源节点几百毫秒,排队请求越积越多,整个节点的服务质量直接崩了。
最终的处理是立即中断迁移,等待慢查询恢复基线,然后修改迁移策略,把大key先单独处理,再做整体迁移。
这次事故教会我一个规矩:迁移前必须先跑一遍大key扫描。redis-cli --bigkeys可以找出大key,但真正操作时我把阈值设得更严,凡是超过256KB的key都单独拉出来处理。大key要么拆分成小key再迁移,要么调整业务的读写方式,把网络传输成本摊薄。
5.2 迁移速率与慢查询的量化关系:10万key一个slot的现实
另一个坑是槽内key数量分布极度不均。Redis的槽分配是按key的hash值均匀分散的,但业务key的分布完全取决于实际数据,有的槽可能有几千个key,有的槽可能有几十万key。
我们曾经遇到过某个槽里有50万个小key,用官方工具整个迁移这个槽,跑了四十多分钟才完成。单看每个key迁移只要1ms,但50万次连续MIGRATE,期间源节点被阻塞的时间累积起来非常可观,加上socket建立释放的损耗,实际耗时远大于理论值。这四十分钟里源节点的服务能力其实是在持续衰减的。
解决思路是不要用官方工具默认的逐key迁移。可以自己写脚本,从源节点用CLUSTER GETKEYSINSLOT获取一批key,然后用MIGRATE的KEYS参数一次迁移多个key,每次携带100个key。这能把源节点的阻塞时间从N次网络往返压缩成N/100次往返,总耗时大幅下降。脚本里还可以加入睡眠控制,每迁一批让延迟指标回归正常再继续。
5.3 内存不降反升的原因与目标节点容量预估
最后说一个反直觉的观测:迁移过程中源节点内存不降反升。
按理说key迁移过去源节点就删了,内存应该下降。但实际操作里,迁移期间业务还在正常写,新写入的key会根据当前槽映射直接落到目标节点,源节点的老key也在逐步删除,双方内存都会动态变化。如果目标节点的槽还没完成归属切换,客户端路由还是旧的,那么写请求仍然会打到源节点,源节点内存可能不降。更关键的是,MIGRATE删除源节点key的行为不是立即释放内存的,Redis复用了内存分配器,DEL之后内存并不会马上交给操作系统,所以INFO memory看到的used_memory下降幅度远小于key数量的减少量。
这提醒我们,目标节点容量预估不能按"迁移的槽占它总槽数的比例"简单测算,要按实际迁入的key对象大小总和,再加上业务增长的buffer来预留。我给目标节点定的安全线是:迁移后稳定内存不超过maxmemory的70%,如果在迁移前就已经超过50%,那就要考虑多批次小批量迁移,或者在迁移窗口内暂停部分非核心业务。
迁移后的内存清理也值得注意:源节点上被腾出来的槽虽然key删光了,但内存碎片可能很高。缩容下线时直接停进程无所谓,如果是扩容后源节点还要继续服务,可以在低峰期开启碎片整理或者重启节点整理内存,否则used_memory会一直虚高。
6. 工具选型与跨集群迁移的进阶思路
6.1 官方工具和第三方迁移工具的优缺点对比
日常扩缩容,官方redis-cli的--cluster reshard和--cluster cp基本够用。reshard基于槽迁移,语义清晰,能保证槽覆盖完整;cp适合跨集群把部分key复制过去,源端数据不删除,适合做数据副本和灰度验证。
但官方工具的缺点也很明显:速率控制粒度粗,遇到大key和key数量不均匀的槽容易翻车;迁移过程不提供断点续传和进度报表;对目标集群的容量压力没有预判能力。
第三方开源迁移工具在处理跨集群迁移时更灵活,常见的有基于RDB解析的全量同步方案和基于命令回放的增量同步方案。这类工具的优势是可以处理超大集群、异构版本、甚至不同中间件之间的迁移,通常还支持限速、断点续传和数据校验。代价是部署和运维复杂度高,而且增量同步的实时性需要额外保障。
我的取舍原则很简单:同一集群内的扩缩容,用官方工具;跨集群、跨版本、对业务连续性要求高的,选第三方工具或自己写脚本。自己写脚本听上去工作量更大,但胜在每一步都可控,配合监控平台能做到失败即停,而不用猜工具内部在做什么。
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| redis-cli --cluster reshard | 集群内扩缩容 | 操作简单、槽覆盖安全 | 速率控制粗、大key风险高 |
| redis-cli --cluster cp | 跨集群复制部分key | 源端数据不动 | 不适合全量、无自动校验 |
| 自定义脚本+批量MIGRATE | 大数据量、精细化控制 | 可控性强、可断点续传 | 需要开发与测试 |
| 第三方迁移工具 | 跨集群、跨版本大迁移 | 功能全、自动校验 | 运维复杂、依赖业务场景适配 |
6.2 跨集群迁移的三种路线:RDB导入、协议回放与双写切流
如果要把一个独立的Redis Cluster整体迁移到另一个集群,比如机房搬迁、换硬件、升级大版本,前面说的槽迁移就不够用了,因为两个集群互不相识。
路线一:RDB离线导入。源集群每个节点BGSAVE出RDB文件,把文件传输给目标集群对应节点,启动时加载。这种方案最快,但需要停写或做严格的数据比对,因为RDB文件保存的是一个时间点的快照,之后增量写入容易丢。适合迁移窗口业务可停的场景。
路线二:协议回放。用一个中间层把源集群的所有写命令实时转发到目标集群,源集群作为主读,目标集群作为备,等数据追平后再切换读流量。这个方案能做到近乎无缝迁移,但中间层的性能和稳定性是瓶颈,命令顺序也必须保持严格一致,否则slot内的数据会错乱。
路线三:双写切流。业务层同时写两个集群,迁移前先把历史数据全量导入目标集群,然后保持双写,定期做数据对账,最后灰度切读流量。这是三种方案里对业务和运维要求最高但是最安全的一种,尤其适合多机房容灾架构的迁移。
我个人在大规模跨集群迁移时倾向双写切流,虽然改造量最大,但每一步都可以回滚,不用背"迁移失败全盘覆没"的包袱。日常扩缩容则老老实实做好前置检查和分批迁移就够了。
最后说一个我坚持的习惯。无论这次迁移多"常规",我都会在操作前把CLUSTER NODES快照和每个节点的INFO memory结果保存一份,迁移过程中每迁一批就记录槽数量、QPS、延迟和内存数据,迁移完成后把CLUSTER SLOTS输出和校验结果一起存档。这些数据将来排查问题、做历史对比时非常管用。
数据迁移本身技术在文档里都有,真正拉开差距的是对执行过程的掌控。每次迁移前我都问自己三句话:数据会不会丢、客户端会不会超时、出问题怎么回滚。把这三句话想清楚,再动手也不迟。迁移这种事,平稳落地靠的从来不是运气,而是把每条命令、每个批次、每次校验都考虑到位。