
实际工作中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实时查看槽的分布也可以中途CtrlC中断已经迁移完成的槽不会回滚未迁移的槽保留原状。这种机制让我可以在发现指标异常时立刻刹车。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输出和校验结果一起存档。这些数据将来排查问题、做历史对比时非常管用。数据迁移本身技术在文档里都有真正拉开差距的是对执行过程的掌控。每次迁移前我都问自己三句话数据会不会丢、客户端会不会超时、出问题怎么回滚。把这三句话想清楚再动手也不迟。迁移这种事平稳落地靠的从来不是运气而是把每条命令、每个批次、每次校验都考虑到位。