Redis集群添加节点之后数据重新均匀分配
# redis 5 cluster集群增加节点动态扩容的步骤
已有集群为6个节点3组主从节点
版本说明
本文写于 2022-03。redis-cli --cluster reshard/rebalance 的用法与 slot 迁移机制未变,经 2026-07 复核仍适用。
<PUBLIC_IP>:9002
<PUBLIC_IP>:9003
<PUBLIC_IP>:9002
<PUBLIC_IP>:9003
<PUBLIC_IP>:9001
<PUBLIC_IP>:9001
2
3
4
5
6
现要增加第4组主从节点
<PUBLIC_IP>:9005
<PUBLIC_IP>:9004
2
新增节点
创建一个新的主节点
<PUBLIC_IP>:9004命令里需要指定一个已有节点以便于获取集群信息,(新增节点在前,已有节点在后)该命令添加的节点默认一个主节点,但是没有分配slot,也没有key./data/redis/redis9001/bin/redis-cli -h <PUBLIC_IP> -p 9001 -a <PASSWORD> --cluster add-node <PUBLIC_IP>:9004 <PUBLIC_IP>:90011将
<PUBLIC_IP>:9005创建为<PUBLIC_IP>:9004的从节点。因为,集群会自动为从节点数最少的主节点创建从节点,所以这里不需要指定主节点。/data/redis/redis9001/bin/redis-cli -a '<PASSWORD>' --cluster add-node <PUBLIC_IP>:9005 <PUBLIC_IP>:9001 --cluster-slave1
# Slot 数据迁移到新节点(一定要在业务低峰期操作)
三个主节点的slot分布:主节点1: [0-5460],主节点2: [5461-10922],主节点3: [10923-16383]
预计分配之后的slot: 主节点1[0-4095],节点2:[6144-10922] 节点3:[12288-16383],主节点4:[4096-6144] +[10922-12288]
<PUBLIC_IP>:9002 slots:[0-5460] (5461 slots) master
<PUBLIC_IP>:9001 slots:[5461-10922] (5462 slots) master
<PUBLIC_IP>:9001 slots:[10923-16383] (5461 slots) master
2
3
快速迁移 cluster reshard(业务繁忙时不建议使用)
# 指定迁移1365个slot迁移到新节点
/data/redis/redis9001/bin/redis-cli -a <PASSWORD> --cluster reshard <PUBLIC_IP>:9001 --cluster-from 6f7c25fc85f5e66484ea6679d5220dcaed5f3672 --cluster-to 26319656b03d9238839187ea2a074cf54e239ecf --cluster-slots 1365 --cluster-yes
2
#!/bin/sh
# Writed by yijian on 2020/8/10
# 迁移 slot 工具,多个slot迁移
#
# 使用时,需要指定如下几个参数:
# 1)参数1:必选参数,用于指定源节点(格式为:ip:port)
# 2)参数2:必选参数,用于指定目标节点(格式为:ip:port)
# 3)参数3:可选参数,用于指定访问 redis 的密码
# 4)参数4:开始slot
# 5)参数5:结束slot
#
# 使用示例(将2020从<PUBLIC_IP>:1383迁移到<PUBLIC_IP>:1386):
# move_redis_slot.sh 2020 <PUBLIC_IP>:1383 <PUBLIC_IP>:1386
#
# 执行本脚本时,有两个“确认”,
# 第一个“确认”是提示参数是否正确,
# 第二个“确认”是提示是否迁移已有的keys,
# 如果输入非yes则只迁移slot,不迁移已有keys。
# 确保redis-cli可用
REDIS_CLI=${REDIS_CLI:-redis-cli}
#REDIS_CLI="/usr/local/bin/redis-cli"
which "$REDIS_CLI" >/dev/null 2>&1
if test $? -ne 0; then
echo "\`redis-cli\` not exists or not executable"
exit 1
fi
# 参数检查
if test $# -ne 4 -a $# -ne 5; then
echo -e "Usage: $(basename $0) source_node destition_node redis_password \033[1;33mbegin end\033[m"
echo -e "Example1: $(basename $0) <PUBLIC_IP>:6379 <PUBLIC_IP>:6380 \033[1;33m450 460\033[m"
echo -e "Example2: $(basename $0) <PUBLIC_IP>:6379 <PUBLIC_IP>:6380 YourPassword \033[1;33m450 460\033[m"
exit 1
fi
# SLOT=$1
SRC_NODE="$1"
DEST_NODE="$2"
REDIS_PASSOWRD="$3"
BEGIN_SLOT="$4"
END_SLOT="$5"
# 得到指定节点的 nodeid
function get_node_id() {
node="$1"
node_ip="$(echo $node | cut -d':' -f1)"
node_port=$(echo $node | cut -d':' -f2)
# 得到对应的 nodeid
$REDIS_CLI --raw --no-auth-warning -a "$REDIS_PASSOWRD" \
-h $node_ip -p $node_port \
CLUSTER NODES | awk -v node=$node -F'[ @]' '{if ($2==node) printf("%s",$1);}'
}
SRC_NODE_ID="$(get_node_id $SRC_NODE)"
SRC_NODE_IP="$(echo $SRC_NODE | cut -d':' -f1)"
SRC_NODE_PORT=$(echo $SRC_NODE | cut -d':' -f2)
DEST_NODE_ID="$(get_node_id $DEST_NODE)"
DEST_NODE_IP="$(echo $DEST_NODE | cut -d':' -f1)"
DEST_NODE_PORT=$(echo $DEST_NODE | cut -d':' -f2)
echo -e "\033[1;33mSource\033[m node: $SRC_NODE_IP:$SRC_NODE_PORT($SRC_NODE_ID)"
echo -e "\033[1;33mDestition\033[m node: $DEST_NODE_IP:$DEST_NODE_PORT($DEST_NODE_ID)"
if test -z "$SRC_NODE_ID"; then
echo -e "Can not get the source node ID of \033[1;33m$SRC_NODE\033[m"
exit 1
fi
if test -z "$DEST_NODE_ID"; then
echo -e "Can not get the destition node ID of \033[1;33m$DEST_NODE\033[m"
exit 1
fi
echo -en "Confirm to continue? [\033[1;33myes\033[m/\033[1;33mno\033[m]"
read -r -p " " input
if test "$input" != "yes"; then
exit 1
fi
echo "........."
# 目标节点上执行 IMPORTING 操作
# 如果 $SLOT 已在目标节点,则执行时报错“ERR I'm already the owner of hash slot 1987”
echo -e "\033[1;33mImporting\033[m $BEGIN_SLOT $END_SLOT from $SRC_NODE($SRC_NODE_ID) to $DEST_NODE($DEST_NODE_ID) ..."
for ((SLOT = ${BEGIN_SLOT}; SLOT <= ${END_SLOT}; SLOT++)); do
err=$($REDIS_CLI --raw --no-auth-warning -a "$REDIS_PASSOWRD" \
-h $DEST_NODE_IP -p $DEST_NODE_PORT \
CLUSTER SETSLOT $SLOT IMPORTING $SRC_NODE_ID)
echo "${SLOT}"
if test "X$err" != "XOK"; then
echo "[destition://$DEST_NODE_IP:$DEST_NODE_PORT] $err"
exit 1
fi
done
# 源节点上执行 MIGRATING 操作
# 如果 $SLOT 并不在源节点上,则执行时报错“ERR I'm not the owner of hash slot 1987”
echo -e "\033[1;33mMigrating\033[m $BEGIN_SLOT $END_SLOT from $SRC_NODE($SRC_NODE_ID) to $DEST_NODE($DEST_NODE_ID) ..."
for ((SLOT = ${BEGIN_SLOT}; SLOT <= ${END_SLOT}; SLOT++)); do
err=$($REDIS_CLI --raw --no-auth-warning -a "$REDIS_PASSOWRD" \
-h $SRC_NODE_IP -p $SRC_NODE_PORT \
CLUSTER SETSLOT $SLOT MIGRATING $DEST_NODE_ID)
echo "${SLOT}"
if test "X$err" != "XOK"; then
echo "[source://$SRC_NODE_IP:$SRC_NODE_PORT] $err"
exit 1
fi
done
# 是否迁移已有的keys?
echo -en "Migrate keys in slot://$BEGIN_SLOT $END_SLOT? [\033[1;33myes\033[m/\033[1;33mno\033[m]"
read -r -p " " input
if test "$input" = "yes"; then
first=1 # 是否第一轮keys迁移操作
batch=1000 # 一次批量迁移的keys数
timeout_ms=60000 # 超时时长(单位:毫秒)
destination_db=0 # 对于redis集群,取值总是为0
num_keys=0
echo "........."
echo -e "Migrating keys in slot://$BEGIN_SLOT $END_SLOT ..."
while true; do
# 在源节点上执行:
# 借助命令“CLUSTER GETKEYSINSLOT”和命令“MIGRATE”迁移已有的keys
for ((SLOT = ${BEGIN_SLOT}; SLOT <= ${END_SLOT}; SLOT++)); do
keys="$($REDIS_CLI --raw --no-auth-warning -a "$REDIS_PASSOWRD" \
-h $SRC_NODE_IP -p $SRC_NODE_PORT \
CLUSTER GETKEYSINSLOT $SLOT $batch | tr '\n' ' ' | xargs)"
if test -z "$keys"; then
if test $first -eq 1; then
echo -e "No any keys to migrate in slot://$SLOT"
else
echo -e "Finished migrating all keys (\033[1;33m$num_keys\033[m) in slot://$SLOT"
fi
break 2
fi
first=0
n=$(echo "$keys" | tr -cd ' ' | wc -c)
num_keys=$(($num_keys + $n))
#echo -e "(\033[1;33m$n\033[m)$keys"
# 在源节点上执行命令“MIGRATE”迁移到目标节点
# MIGRATE returns OK on success,
# or NOKEY if no keys were found in the source instance
if test -z "$REDIS_PASSOWRD"; then
err=$($REDIS_CLI --raw \
-h $SRC_NODE_IP -p $SRC_NODE_PORT \
MIGRATE $DEST_NODE_IP $DEST_NODE_PORT "" $destination_db $timeout_ms \
REPLACE KEYS $keys)
else
err=$($REDIS_CLI --raw --no-auth-warning -a "$REDIS_PASSOWRD" \
-h $SRC_NODE_IP -p $SRC_NODE_PORT \
MIGRATE $DEST_NODE_IP $DEST_NODE_PORT "" $destination_db $timeout_ms \
REPLACE AUTH "$REDIS_PASSOWRD" KEYS $keys)
fi
if test "X$err" = "XNOKEY"; then
break
fi
echo -e "\033[1;33m$n\033[m keys migratd"
done
done
fi
# 取得所有 master 节点
nodes=($($REDIS_CLI --raw --no-auth-warning -a "$REDIS_PASSOWRD" -h $DEST_NODE_IP -p $DEST_NODE_PORT \
CLUSTER NODES | awk '{if (match($3,"master")) printf("%s\n",$2);}'))
# 在所有 master 节点上执行 NODE 操作
# 实际上,只可对源节点和目标节点执行 NODE 操作,
# 新的配置会自动在集群中传播。
echo "........."
for node in ${nodes[*]}; do
node_ip="$(echo $node | cut -d':' -f1)"
node_port=$(echo $node | cut -d':' -f2)
echo -en "NODE: $node_ip:$node_port "
for ((SLOT = ${BEGIN_SLOT}; SLOT <= ${END_SLOT}; SLOT++)); do
err=$($REDIS_CLI --raw --no-auth-warning -a "$REDIS_PASSOWRD" \
-h $node_ip -p $node_port \
CLUSTER SETSLOT $SLOT NODE $DEST_NODE_ID)
echo -e "\033[1;33m$err\033[m"
done
done
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#从<PUBLIC_IP>:9002迁移[4095到5460共1365]个slots到新的主节点<PUBLIC_IP>:9004
#最好多次少量迁移 [4095 4595],[4596 5095],[5096 5460]
sudo sh /tmp/redis_slot_move_more.sh <PUBLIC_IP>:9002 <PUBLIC_IP>:9004 YourPassword 4096 5460
#从<PUBLIC_IP>:9001迁移[5461到6826共1365]个slots到新的主节点<PUBLIC_IP>:9004
sudo sh /tmp/redis_slot_move_more.sh <PUBLIC_IP>:9001 <PUBLIC_IP>:9004 YourPassword 5461 6144
#从<PUBLIC_IP>:9001迁移[10923到12288共1365]个slots到新的主节点<PUBLIC_IP>:9004
sudo sh /tmp/redis_slot_move_more.sh <PUBLIC_IP>:9001 <PUBLIC_IP>:9004 YourPassword 10922 12288
2
3
4
5
6
7
若出现警告,则手动执行即可 cluster setslot 11763 stable
Check for open slots...
[WARNING] Node <PUBLIC_IP>:8001 has slots in importing state 11763.
[WARNING] The following slots are open: 11763.
#解决:
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
<PUBLIC_IP>:8001> cluster setslot 11763 stable
OK
再执行 check
2
3
4
5
6
7
8
# 为什么 reshard 必须业务低峰期执行
Redis Cluster 的槽位迁移是渐进式的,不是原子操作。迁移单个槽位的过程:
- 源节点标记槽位为
MIGRATING - 目标节点标记槽位为
IMPORTING - 客户端对该槽位 key 的访问会触发
ASK重定向 - 实际 key 被逐个搬移
- 完成后,槽位归属正式切换到目标节点
业务高峰期的风险:
- ASK 重定向风暴:大量客户端收到重定向,增加网络往返和延迟
- 大 key 阻塞:单个 key 过大时,
MIGRATE命令会阻塞源节点 - 迁移中断:网络抖动可能导致槽位卡在
importing/migrating中间态(即「open slots」故障)
建议:在业务低峰期执行,并分批迁移(每次 100-500 个槽位),而非一次性迁移 4000+。
# 验证迁移完成
# 1. 集群状态检查
redis-cli --cluster check <any-node>:6379
期望:
[OK] All 16384 slots covered- 无
[WARNING] Node ... has slots in importing/migrating state
# 2. 槽位分布确认
redis-cli -h <new-master> -p 6379 cluster info
检查:
cluster_state:okcluster_slots_assigned:16384cluster_slots_ok:16384
# 3. 数据写入验证
# 向新节点写入测试数据
redis-cli -c -h <new-master> -p 6379 set test:key:on:new:node "test_value"
# 验证能读回(可能路由到其他节点,-c 会自动跟随)
redis-cli -c -h <any-node> -p 6379 get test:key:on:new:node
2
3
4
5
# 4. 节点负载均衡确认
redis-cli --cluster info <any-node>:6379
检查每个主节点的 key 数量和内存占用是否大致均衡。
# 坑与边界
迁移中断后的 open slots:如果迁移过程中脚本/网络中断,槽位可能卡在
importing或migrating状态。解决:# 确认槽位是否已搬空 redis-cli -h <target-node> cluster countkeysinslot <slot-id> # 如果返回 0,直接置为 stable redis-cli -h <target-node> cluster setslot <slot-id> stable1
2
3
4
5详细处理见《Redis Cluster 新增节点 slot 迁移卡住故障复盘》。
迁移期间的多 key 操作原子性:Redis Cluster 不支持跨槽位的多 key 事务(
MULTI/EXEC)。迁移期间,即使 key 原本在同一槽位,如果恰好在迁移瞬间,事务可能失败。避免在迁移期间执行跨 key 事务。新节点默认无槽位:
add-node只把节点加入集群拓扑,不会自动分配槽位。忘记 reshard 会导致新主节点成为「空主」——集群状态正常,但它不处理任何请求。从节点的 slot 分布不显示:
cluster nodes只显示主节点的槽位范围,从节点显示为空。这是设计如此——从节点复用主节点的槽位信息。reshard 期间的读写性能:迁移大量槽位时,源节点的
MIGRATE操作会消耗 CPU 和网络带宽,可能导致正常请求的延迟上升。监控instantaneous_ops_per_sec和latency指标。batch 迁移脚本的风险:本文附带的批量迁移脚本使用循环逐个处理槽位,效率较低但可控。极端场景下(单槽位包含数百万 key),建议改用
redis-cli --cluster reshard的交互式模式,可以控制每次迁移的 key 数量和超时。