Redis Cluster reshard 迁槽实战与卡顿治理
reshard(迁槽)是 Cluster 扩缩容的核心动作,也是最容易把线上打抖的操作:迁移期间业务请求要走 ASK 重定向,迁移大 key 时主线程会被阻塞,客户端实现不佳还会直接报错。本文基于 Redis 7,先讲命令用法,再下探到协议层的迁槽原语,最后给出卡顿的定位与治理手段。
一、redis-cli 的两种 reshard 用法
交互式适合人工操作,命令会依次询问迁移槽数、目标节点、源节点:
$ redis-cli --cluster reshard 127.0.0.1:6379
>>> How many slots do you want to move (from 1 to 16384)? 1000
>>> What is the receiving node ID? <目标节点 id>
>>> Source node #1: all
>>> Do you want to proceed with the proposed reshard plan (yes/no)? yes
自动化脚本里推荐非交互式参数,一次指定全部要素:
redis-cli --cluster reshard 127.0.0.1:6379 \
--cluster-from <源节点 id> \
--cluster-to <目标节点 id> \
--cluster-slots 1000 \
--cluster-pipeline 100 \
--cluster-timeout 60000 \
--cluster-yes
两个参数直接影响迁移速度与卡顿:--cluster-pipeline 是每次 MIGRATE 命令批量携带的 key 数(默认 10),调大可以减少网络往返;--cluster-timeout 是单次 MIGRATE 的超时,默认 60000 毫秒。执行过程中 redis-cli 每迁移一批 key 就打印一个进度点,配合终端输出可以大致观察迁移速率。
迁移进度还有个量化办法:把每个节点的 offset 与 key 数在迁移前后各采一次,差值就是实际搬走的量。执行期间也可以直接查槽的归属状态:
redis-cli -c -h 10.0.0.11 -p 6379 cluster nodes
# 10.0.0.11:6379@16379 myself,master - 0 1761350402000 1 connected 0-5460
# 10.0.0.12:6379@16379 master - 0 1761350401000 2 connected 5461-10922 [1234->-<目标 id>]
# 10.0.0.13:6379@16379 master - 0 1761350403000 3 connected 10923-16383 [1234-<-<源 id>]
方括号里的 [1234->-<id>] 是 migrating 标记,[1234-<-<id>] 是 importing 标记。这一行输出是判断迁移是否卡住最直接的证据:如果某个槽的标记持续数分钟不变,说明这个槽里的 key 搬不动,通常是有大 key 或者 MIGRATE 超时后反复重试。
二、迁槽在协议层面做了什么
redis-cli 只是编排者,真正的迁槽由一组集群命令完成。以把槽 1234 从节点 A 迁到节点 B 为例:
# 1. 目标节点标记导入状态(必须先做,保证能接收 ASK)
redis-cli -h <B> cluster setslot 1234 importing <A 的 id>
# 2. 源节点标记迁出状态
redis-cli -h <A> cluster setslot 1234 migrating <B 的 id>
# 3. 从源节点分批取 key
redis-cli -h <A> cluster getkeysinslot 1234 100
# 4. 批量迁移(空 key 占位 + KEYS 列表)
redis-cli -h <A> migrate <B 的 IP> 6379 "" 0 60000 KEYS key1 key2
# 5. 迁完后在目标、源节点上确认归属,顺序不能反
redis-cli -h <B> cluster setslot 1234 node <B 的 id>
redis-cli -h <A> cluster setslot 1234 node <B 的 id>
理解这段流程就理解了迁移期的业务表现:槽处于 migrating 状态时,源节点对已存在的 key 正常处理,对不存在的 key 返回 ASK 重定向;客户端向 B 发送 ASKING 后重试该条命令。多 key 命令如果部分 key 已迁走、部分还在源节点,源节点会返回 TRYAGAIN,客户端需要稍后重试。
注意:第 5 步必须先通知目标节点再通知源节点。反过来时若目标节点中途宕机,槽会处于无主状态,即使触发故障转移也无法自动恢复归属。
三、迁移期卡顿的四个来源
第一,MIGRATE 本身是阻塞命令。源节点要序列化 key、走网络传输、等目标节点反序列化并恢复到内存,整个期间源节点主线程不处理其他命令,目标节点同样被占用。单次 MIGRATE 的耗时约等于一个 key 的序列化与传输时间,key 越大阻塞越久。
第二,大 key 是元凶。一个几百 MB 的 hash 经 MIGRATE 迁移时,源节点会秒级无响应,这通常就是「迁移动画走着走着业务就卡了」的直接原因。
第三,ASK 重定向放大延迟。迁移中的槽,不在本节点的 key 都要多一次往返;客户端若不支持 ASK(自研或较老的 SDK),表现为直接抛错。
第四,批量太小导致 RTT 累积。--cluster-pipeline 默认 10,跨机房或网络抖动环境下,迁 10 万个 key 就是上万次往返,迁移窗口被拉长,业务暴露在 ASK 状态下的时间随之变长。
四个来源里,MIGRATE 的阻塞机制最值得下探一层,因为它决定了所有治理手段的上限。MIGRATE 命令在源节点上的执行路径是:查 key 是否存在、调用 dumpCommand 把值序列化成 DUMP 格式、在同一个命令上下文里建立到目标节点的同步连接、发送 RESTORE-ASKING 与负载、等待目标返回、删除本地 key。整条链都在主线程里同步完成,中间没有让出执行权的机会。所以单次 MIGRATE 的耗时等于"序列化 + 一次完整 RTT + 目标节点反序列化",三者相加全部计入源节点的阻塞时间。
这也解释了为什么 --cluster-pipeline 的收益有天花板。它把 N 个 key 合并进一次 MIGRATE,RTT 部分从 N 次降为 1 次,但序列化与反序列化的总工作量不变,而且单个大 key 造成的阻塞时间也不会因为批量而缩短。同机房 RTT 约 0.2 ms 时,把批量从 10 调到 100 能让 RTT 开销下降九成;跨机房 RTT 达到 5 ms 时,同样的调整收益更显著,但是一旦某个 key 有 50 MB,序列化耗时就远超 RTT,调参的作用被彻底掩盖。这就是"先拆大 key,再调参数"的顺序依据。
四、卡顿治理清单
治理的总体思路是:把单次阻塞压短,把迁移窗口收窄。生产上建议按顺序执行:先扫描并拆解迁移范围内的大 key(bigkeys 或 cluster getkeysinslot 逐槽检查);把 reshard 安排在业务低峰,并一次性迁几百个槽就停下来观察 slowlog 与客户端错误,而不是一口气迁完;网络往返多时调大 --cluster-pipeline;迁移期间暂停全库扫描类的运维任务。若迁移中途中断,槽可能残留 importing/migrating 状态,用 redis-cli --cluster fix 127.0.0.1:6379 修复后再继续。
常见坑 / 最佳实践
- 先拆大 key 再迁槽。 MIGRATE 对单个 key 是原子阻塞的,几百 MB 的 key 会造成秒级无响应。先治理大 key,或者把含大 key 的槽单独安排在最低峰迁移。
- 分批迁移,边迁边看。 一次 reshard 上千个槽的窗口太长。按几百个槽一批,每批结束后检查
SLOWLOG GET、LATENCY 事件与业务 P99,确认无异常再继续。 - 确认客户端支持 ASK 重定向。 迁移上线前在测试环境跑一轮小规模 reshard,验证所用 SDK(版本)在 ASK 场景下不报错、不丢请求。
--cluster-pipeline按网络质量调。 同机房可调到 100 以上;跨机房迁槽数据量又大时,先评估带宽,避免 RDB 流量与迁移流量互相挤压。- 中断的迁移必须收尾。 残留的 importing/migrating 状态会让该槽持续返回 TRYAGAIN 与 ASK,用
--cluster fix或手工 SETSLOT STABLE 清理。
小结
- reshard 的本质是 IMPORTING/MIGRATING 标记、GETKEYSINSLOT 取 key、MIGRATE 批量搬运、SETSLOT 确认归属这四步原语的循环。
- 迁移期源节点对不存在的 key 返回 ASK,多 key 命令出现部分迁移时返回 TRYAGAIN,客户端要能处理这两种错误。
- 卡顿的四个来源是 MIGRATE 阻塞、大 key、ASK 往返与批量过小,对应解法分别是低峰、拆 key、验证客户端、调 pipeline。
- 迁槽顺序有讲究:目标节点先 IMPORTING,归属确认时目标节点先于源节点 SETSLOT。
- 分批、观察、再继续是生产迁槽的节奏,中断残留状态要用 fix 收尾。
- 点赞
- 收藏
- 关注作者
评论(0)