【Redis从入门到精通】第50篇:集群重新分片——不停服迁移槽位的黑魔法
上一篇【第49篇】MOVED和ASK——Cluster重定向机制详解下一篇【第51篇】Cluster复制与故障转移——节点挂了怎么办明日更新敬请期待好了集群搭起来了数据也分布好了。但好景不长——用户量激增内存告急老板说加两台机器。加机器容易但数据怎么搬过去停服迁数据老板说不可能7×24不能断这就是重新分片Resharding要解决的问题在不停止服务的情况下把槽从一个节点迁移到另一个节点。这不是简单的MOVE命令就能搞定的——槽里有成千上万个key每个都在被持续读写。今天我们就来拆解这个黑魔法。一、重新分片的使用场景┌──────────────────────────────────────────────────┐ │ 重新分片的常见场景 │ ├──────────────────────────────────────────────────┤ │ │ │ 场景1扩容 │ │ 3台机器 → 5台机器 │ │ 从每个旧节点移一些槽到新节点 │ │ │ │ 场景2缩容 │ │ 5台机器 → 3台机器 │ │ 把要下掉的节点的槽迁移到保留的节点 │ │ │ │ 场景3槽分布不均 │ │ 某节点槽太多/太大内存使用率98% │ │ 迁移部分槽到负载低的节点 │ │ │ │ 场景4流量倾斜 │ │ 热门key集中某几个槽对应节点CPU打满 │ │ 重新分配这些槽到多个节点分散压力 │ │ │ └──────────────────────────────────────────────────┘无论哪种场景核心操作都一样把N个槽从源节点迁移到目标节点。Redis Cluster的重新分片之所以是黑魔法是因为它做到了不中断服务迁移过程中对key的读写请求不被拒绝数据不丢失迁移过程原子化要么全成功要么失败回滚客户端无感知通过ASK重定向透明处理可观测通过命令实时监控迁移进度二、迁移一个槽的完整流程先看一个槽从头到尾是怎么搬走的。下图是单槽迁移的完整生命周期┌─ 源节点 (Node A) ───────┐ ┌─ 目标节点 (Node B) ───────┐ │ │ │ │ │ slots[slot] Node A │ │ slots[slot] ! Node B │ └──────────────────────────┘ └───────────────────────────┘ │ │ │ ① redis-cli向Node B发送 │ │ CLUSTER SETSLOT slot IMPORTING A │ → Node B记录: 这个槽的key正在从A迁进来 │─────────────────────────────────────────→ │ │ │ ② redis-cli向Node A发送 │ │ CLUSTER SETSLOT slot MIGRATING B │ → Node A记录: 这个槽的key正在迁到B │←────────────────────────────────────────│ │ │ │ ③ 获取槽中的所有key │ │ CLUSTER GETKEYSINSLOT slot count │←────────────────────────────────────────│ │ │ │ ④ 逐个迁移key │ │ MIGRATE B_ip B_port key 0 timeout │ → Node A: DUMP key → 序列化数据 │ → 发送序列化数据到Node B │ → Node B: RESTORE key │ → Node B返回OK │ → Node A: DEL key │─────────────────────────────────────────→ │ │ │ ⑤ 重复③④直到槽中key全迁移 │ │ │ │ ⑥ redis-cli向双方发送 │ │ CLUSTER SETSLOT slot NODE B_id │ → 正式宣布: slot归Node B了 │─────────────────────────────────────────→ │ │ │ ⑦ Gossip协议传播新槽分配 │ │ 全集群节点更新slots数组 │ │ │ 迁移完成 ┌──────────────────┐ ┌───────────────────┐ │ slots[slot]NULL │ │ slots[slot]Node B │ └──────────────────┘ └───────────────────┘这个流程中每一步都是可恢复的如果在①②之后失败 →CLUSTER SETSLOT slot STABLE回退如果在④中途失败 → key在源节点还在重试MIGRATE如果在⑥之前失败 →CLUSTER SETSLOT slot NODE A_id回退只有第⑥步执行后槽才算真正换了主人。三、MIGRATE命令一石三鸟的原子操作MIGRATE是迁移的核心武器它内部做了三件事MIGRATE target_ip target_port key destination-db timeout 内部执行流程: ┌─────────────────────────────────┐ │ 1. DUMP key │ │ 将key序列化为二进制数据 │ │ 格式: 类型码 CRC64 数据 │ ├─────────────────────────────────┤ │ 2. 发送序列化数据到目标节点 │ │ 通过TCP连接发送 │ │ 目标节点执行 RESTORE │ │ 带 REPLACE 参数可覆盖已有key │ ├─────────────────────────────────┤ │ 3. 在源节点上 DEL key │ │ 只有RESTORE成功才执行 │ │ 删除释放源节点内存 │ └─────────────────────────────────┘这是个伪原子操作——它不是传统数据库的事务原子性但通过先DUMP再RESTORE最后DEL的顺序确保了数据不会重复也不会丢失。具体保证DUMP成功但RESTORE失败 → key还在源节点数据没丢RESTORE成功但DEL失败 → key在两个节点各有一份重复但不丢——需要通过REPLACE标记处理RESTORE成功且DEL成功 → 完美迁移# 迁移单个keyMIGRATE192.168.1.1026380user:100105000# MIGRATE参数说明# 192.168.1.102 6380 → 目标节点# user:1001 → 要迁移的key# 0 → 目标数据库编号Redis Cluster始终为0# 5000 → 超时时间毫秒# 批量迁移一次请求迁移多个keyMIGRATE192.168.1.102638005000KEYS key1 key2 key3# 带COPY选项迁移但保留源key测试用MIGRATE192.168.1.1026380user:100105000COPY# 带REPLACE选项目标存在则覆盖MIGRATE192.168.1.1026380user:100105000REPLACEMIGRATE的底层实现// MIGRATE的内部实现src/cluster.c 简化版voidmigrateCommand(client*c){// 1. 判断是阻塞模式还是非阻塞模式// timeout0 → 非阻塞返回错误给客户端// timeout0 → 阻塞当前连接等待结果// 2. 创建到目标节点的Socket连接intfdanetTcpConnect(...);// 3. DUMP keyrioInitWithBuffer(payload,...);rdbSaveObject(payload,o);// 序列化// 4. 发送RESTORE命令到目标节点// RESTORE key ttl serialized_value [REPLACE]// TTL PTTL(key) 用于保留过期时间// 5. 读取目标节点的回复// 成功 → DEL key on source// 失败 → 保留key}踩坑提示MIGRATE是阻塞命令。在迁移大key时比如一个100MB的String或100万元素的SetMIGRATE会阻塞源节点的事件循环。虽然有MIGRATE内部异步I/O但DUMP序列化和数据复制都在主线程执行。迁移大key前一定要评估key的大小——超过10MB的key建议用业务迁移方案别用MIGRATE硬搬。四、redis-cli --cluster reshard 实操手动一步步执行MIGRATE太麻烦了。redis-cli --cluster reshard是官方提供的自动化重分片工具redis-cli--clusterreshard127.0.0.1:6379交互式流程 Performing Cluster Check (using node 127.0.0.1:6379) M: ... 127.0.0.1:6379 slots:[0-5460] (5461 slots) master M: ... 127.0.0.1:6380 slots:[5461-10922] (5462 slots) master M: ... 127.0.0.1:6381 slots:[10923-16383] (5461 slots) master [OK] All nodes agree about slots configuration. How many slots do you want to move (from 1 to 16384)?输入要迁移的槽数How many slots do you want to move (from 1 to 16384)? 1000 What is the receiving node ID?输入目标节点ID从上面的NODES输出中复制What is the receiving node ID? a1b2c3d4e5... # 新节点ID Please enter all the source node IDs. Type all to use all the nodes as source nodes for the hash slots. Type done once you entered all the source nodes IDs. Source node #1:这里可以指定从哪些节点迁出。输入all表示从所有现有主节点平均提取Source node #1: all Ready to move 1000 slots. Source nodes: M: ... 127.0.0.1:6379 slots:[0-5460] M: ... 127.0.0.1:6380 slots:[5461-10922] M: ... 127.0.0.1:6381 slots:[10923-16383] Destination node: M: ... 127.0.0.1:6384 slots: (0 slots) Resharding plan: Moving slot 0 from 127.0.0.1:6379 to 127.0.0.1:6384 Moving slot 1 from 127.0.0.1:6379 to 127.0.0.1:6384 Moving slot 2 from 127.0.0.1:6379 to 127.0.0.1:6384 ... Do you want to proceed with the proposed reshard plan (yes/no)?确认后开始迁移Do you want to proceed with the proposed reshard plan (yes/no)? yes Moving slot 0 from 127.0.0.1:6379 to 127.0.0.1:6384: ... Moving slot 1 from 127.0.0.1:6379 to 127.0.0.1:6384: . Moving slot 2 from 127.0.0.1:6379 to 127.0.0.1:6384: . ...每个.代表一个槽迁移成功。工具内部自动执行前面说的完整迁移流程。非交互模式# 自动分配一行命令搞定redis-cli--clusterreshard127.0.0.1:6379\--cluster-fromsource-node-id\--cluster-totarget-node-id\--cluster-slots1000\--cluster-yes# 从所有节点平均移出redis-cli--clusterreshard127.0.0.1:6379\--cluster-from all\--cluster-totarget-node-id\--cluster-slots1000\--cluster-yes检查迁移后的槽分布redis-cli--clustercheck127.0.0.1:6379# 查看新节点上的槽redis-cli-p6384CLUSTER NODES|grepmyself五、在线迁移过程中访问正在迁移的key在第49篇中我们详细讨论了MOVED和ASK的区别。在reshard过程中ASK机制确保数据持续可访问迁移中访问key的处理逻辑 ┌─ 源节点 (MIGRATING状态) ─┐ ┌─ 目标节点 (IMPORTING状态) ─┐ │ │ │ │ │ 收到 GET key │ │ 收到 GET key │ │ → key还在本地 │ │ → 有ASKING标记 │ │ 有 → 正常返回 │ │ 有 → 本地查找并返回 │ │ 无 → 返回ASK跳转 │ │ 无 → 返回MOVED给源节点 │ │ │ │ │ │ 注意MIGRATING状态下 │ │ 注意IMPORTING状态下 │ │ 90%的请求仍走本地 │ │ 只有被ASKING许可的请求 │ │ 只有已迁移的key才ASK │ │ 才会处理 │ └────────────────────────────┘ └────────────────────────────┘时间线视角Key: user:1001 所在槽 slot8416 时间点1: 槽正常归属于Node A GET user:1001 → Node A返回value ✓ 时间点2: CLUSTER SETSLOT 8416 IMPORTING/MIGRATING → 开始迁移 GET user:1001 → Node A还有这份数据 → 正常返回 ✓ 时间点3: MIGRATE user:1001 到 Node Bkey已迁走 GET user:1001 → Node A发现key不在 → 返回 ASK → 客户端转到Node B → 发ASKING → GET user:1001 → Node B返回value ✓ 时间点4: CLUSTER SETSLOT 8416 NODE B迁移完成 GET user:1001 → Node A返回 MOVED → 客户端更新缓存 → 转到Node B → Node B直接返回value ✓整个过程中只有MIGRATE执行的那一瞬间key不可用毫秒级其他时间都有明确的路由。六、大Key迁移与性能影响在reshard过程中大Keybig key是最让人头疼的问题。大Key的定义与影响┌───────────────────────────────────────────────┐ │ 大Key对迁移的影响 │ ├───────────────────────────────────────────────┤ │ │ │ String类型 10MB │ │ → DUMP序列化时瞬间分配大量内存 │ │ → 网络传输时间长可能超时 │ │ │ │ 集合类型 10000个元素 │ │ → 序列化和反序列化耗时随元素数线性增长 │ │ → DEL操作释放内存可能触发操作系统的内存回收 │ │ │ │ 总体影响 │ │ → MIGRATE阻塞源节点主线程 │ │ → 迁移过程中该key不可访问 │ │ → 引发客户端超时 │ │ │ └───────────────────────────────────────────────┘应对策略# 1. 迁移前扫描大Keyredis-cli--bigkeys-p6379# 2. 用MEMORY USAGE估算key大小redis-cli-p6379MEMORY USAGEbig_hash_key# (integer) 10485760 # 10MB!# 3. 评估该槽有多少大keyredis-cli-p6379CLUSTER COUNTKEYSINSLOT8416# (integer) 512# 4. 分批迁移避开大key的槽# 如果slot 8416有超大key可以# - 先迁移其他小key的槽# - 对大key所在槽业务低峰期单独迁移# - 或者把大key拆分成小key后再迁移踩坑提示MIGRATE的超时时间计算不是简单的传输时间。默认timeout参数是5000ms但对大key来说远远不够。如果1秒还没迁移完Redis内部会进行重试。每个重试周期都会重新DUMP key——如果key有10MB每秒重试一次就是每秒重新分配10MB内存。内存的频繁申请和释放会把源节点拖垮。MIGRATE内部的重试机制MIGRATE的timeout不是总超时时间而是单次传输的超时。如果key太大导致单次传输失败Redis会自动重试// MIGRATE的内部超时处理简化while(remaining_time0){// 发起连接和传输if(sendRestoreCommand(fd,key,payload)C_OK){// 成功了delKeyOnSource(key);return;}// 超时了但还有时间重试remaining_time-elapsed;}// 所有时间用完返回错误这就解释了为什么大key迁移可能卡住整个reshard流程——MIGRATE在一个大key上反复重试阻塞后续槽的迁移。业务侧迁移大key的方案对于超大key50MB建议不用MIGRATE改为业务迁移业务侧大key迁移方案 1. 向目标节点写入新key不同key名 如: big_key → big_key_new 2. 双写过渡期源目标都写 或者先全量复制再增量追 3. 验证数据一致性 4. 切换读取到目标节点 5. 删除源节点旧key迁移完成七、迁移过程的监控迁移是个漫长过程可能需要几个小时必须有完善的监控。实时监控命令# 查看集群状态关注slots_ok是否为16384redis-cli-p6379CLUSTER INFO|grep-Estate|slots_ok|slots_pfail# 查看迁移状态的节点redis-cli-p6379CLUSTER NODES|grep-Emigrating|importing# 输出示例# a1b2c3... 192.168.1.101:637916379 myself,master - 0 ... 1 connected 0-5460 [3999--a1b2c3...] [8416--d4e5f6...]# [3999--a1b2c3...] 表示 slot 3999 正在迁出到 a1b2c3# [3999--a1b2c3...] 表示 slot 3999 正在从 a1b2c3 迁入迁移进度估算# 估算剩余迁移时间redis-cli-p6379--clustercheck127.0.0.1:6379# 输出会显示# [OK] All 16384 slots covered.# Slot 0-5460 covered by node a1b2c3... (5461 slots)# ...# 对比迁移前后的槽分布变化一个简单的监控脚本#!/bin/bash# monitor_reshard.sh - 监控重分片进度HOST127.0.0.1PORT6379INTERVAL10# 每10秒检查一次echo Redis Cluster Reshard Monitor echoTime | State | Slots_OK | Migrating | Importingecho--------------------|-------|----------|-----------|----------whiletrue;doINFO$(redis-cli-h$HOST-p$PORT CLUSTER INFO2/dev/null)NODES$(redis-cli-h$HOST-p$PORT CLUSTER NODES2/dev/null)STATE$(echo$INFO|grepcluster_state|cut-d:-f2|tr-d\r)SLOTS_OK$(echo$INFO|grepcluster_slots_ok|cut-d:-f2|tr-d\r)# 统计迁移中的槽数MIGRATING$(echo$NODES|grep-o\--|wc-l)IMPORTING$(echo$NODES|grep-o-|wc-l)TIME$(date%Y-%m-%d %H:%M:%S)printf%-20s| %-6s| %-9s| %-10s| %-10s\n\$TIME$STATE$SLOTS_OK$MIGRATING$IMPORTINGsleep$INTERVALdone性能影响监控迁移期间务必关注这些指标监控指标正常阈值关注阈值报警阈值说明网络带宽500Mbps800Mbps900Mbps迁移会消耗源和目标的带宽源节点内存稳定持续下降-迁移后DEL释放内存应该下降目标节点内存稳定持续上升80%接收数据导致内存增长源节点CPU50%70%85%DUMP序列化消耗CPU目标节点CPU50%70%85%RESTORE反序列化消耗CPU源节点延迟1ms3ms5msMIGRATE阻塞可能影响其他请求重定向率1%3%10%MOVED/ASK比例异常升高客户端超时0偶发持续大key迁移导致的超时总结集群重新分片是Redis Cluster运维中的最核心技能。从CLUSTER SETSLOT设置迁移状态到MIGRATE的DUMPRESTOREDEL原子操作再到redis-cli --cluster reshard的自动化编排——整条链路设计精妙确保数据在迁移过程中持续可访问。关键要点IMPORTING/MIGRATING状态是迁移的安全网让客户端通过ASK找到正确数据MIGRATE是原子操作数据不丢不重大key是迁移的噩梦尽量拆分监控是迁移的眼睛带宽、CPU、内存、延迟一个不能少迁移完成后别忘了还需要为新节点配置从节点确保高可用。下一篇我们看Cluster内部如何实现自动故障转移——没有Sentinel节点挂了怎么自动恢复上一篇【第49篇】MOVED和ASK——Cluster重定向机制详解下一篇【第51篇】Cluster复制与故障转移——节点挂了怎么办明日更新敬请期待