问题:一个Redis集群如果从原先的3个节点扩容为4个节点,存量数据是否需要迁移?如果需要该怎么迁移?
首先对于数据是否需要迁移答案是肯定的**:需要迁移** ,但只需迁移部分数据(约 25%的数据)。
原因:从 3 个节点扩到 4 个节点,需要重新分配数据分布,使每个节点承担约 25%的数据量,而不是原来的 33%。以下是具体的数据迁移方案:
一、迁移前准备
1.1 环境检查
bash
# 1. 检查当前集群状态
redis-cli --cluster check 192.168.1.101:6379
# 2. 查看槽分布情况
redis-cli -h 192.168.1.101 -p 6379 cluster slots
# 或使用更详细的查看方式
echo "cluster slots" | redis-cli -h 192.168.1.101 -p 6379 | python -m json.tool
# 3. 检查数据量预估
for node in 192.168.1.{101,102,103}:6379; do
echo "=== $node ==="
redis-cli -c -h ${node%:*} -p ${node#*:} dbsize
redis-cli -c -h ${node%:*} -p ${node#*:} info memory | grep used_memory_human
done
# 4. 备份当前集群配置
redis-cli -h 192.168.1.101 -p 6379 cluster nodes > cluster_nodes_backup_$(date +%Y%m%d).txt
redis-cli -h 192.168.1.101 -p 6379 cluster info > cluster_info_backup_$(date +%Y%m%d).txt
1.2 新节点准备
bash
# 1. 新节点配置文件(redis-4.conf)
port 6379
cluster-enabled yes
cluster-config-file nodes-4.conf
cluster-node-timeout 15000
cluster-require-full-coverage yes
appendonly yes
daemonize yes
logfile "/var/log/redis/redis-4.log"
dir /var/lib/redis/4/
maxmemory 8gb
maxmemory-policy volatile-lru
# 2. 创建数据目录和日志目录
mkdir -p /var/lib/redis/4/
mkdir -p /var/log/redis/
chown -R redis:redis /var/lib/redis/4/ /var/log/redis/
# 3. 启动新节点
redis-server /etc/redis/redis-4.conf
# 4. 验证新节点启动
redis-cli -h 192.168.1.104 -p 6379 ping
1.3 业务准备
-
通知业务方:提前通知相关业务团队迁移时间窗口
-
设置维护窗口:建议在业务低峰期进行(如凌晨2:00-5:00)
-
客户端检查:
-
确保客户端使用集群模式连接
-
检查客户端重试机制是否完善
-
验证客户端是否支持
MOVED和ASK重定向
-
二、详细迁移步骤
2.1 添加新节点到集群
如果你的集群数据量不大(<10GB),可以直接将新节点作为主节点加入集群。对于大型Redis集群,先添加为从节点再提升为主节点是更优的扩容策略。(具体可看后面的完整迁移脚本)
bash
# 1. 将新节点作为主节点加入集群
redis-cli --cluster add-node 192.168.1.104:6379 192.168.1.101:6379
# 2. 验证节点已加入但未分配槽
redis-cli -h 192.168.1.101 -p 6379 cluster nodes | grep 192.168.1.104
# 输出应显示新节点,但flags为"master"且没有槽分配
# 3. 确认当前槽分布(迁移前)
echo "当前槽分布:"
redis-cli --cluster info 192.168.1.101:6379
2.2 计算迁移计划
python
#!/usr/bin/env python3
# calculate_slots_redistribution.py
import sys
TOTAL_SLOTS = 16384
CURRENT_NODES = 3
NEW_NODES = 4
# 理想分布
slots_per_node = TOTAL_SLOTS // NEW_NODES # 4096
print("=" * 60)
print("Redis集群槽迁移计算")
print("=" * 60)
print(f"总槽数: {TOTAL_SLOTS}")
print(f"当前节点数: {CURRENT_NODES}")
print(f"目标节点数: {NEW_NODES}")
print(f"目标每个节点槽数: {slots_per_node}")
print()
# 从每个现有节点需要迁移出的槽数
slots_to_move_from_each = (TOTAL_SLOTS // CURRENT_NODES) - slots_per_node
print(f"从每个现有节点需要迁移出槽数: {slots_to_move_from_each}")
print()
# 实际分配方案
print("建议迁移方案:")
print(f"1. 从节点1迁移 {slots_to_move_from_each} 个槽到新节点")
print(f"2. 从节点2迁移 {slots_to_move_from_each} 个槽到新节点")
print(f"3. 从节点3迁移 {slots_to_move_from_each} 个槽到新节点")
print()
print(f"迁移后分布:")
print(f"• 节点1: {slots_per_node} 槽")
print(f"• 节点2: {slots_per_node} 槽")
print(f"• 节点3: {slots_per_node} 槽")
print(f"• 节点4: {slots_per_node} 槽")
print("=" * 60)
2.3 执行迁移操作
方案A:自动重新分片(推荐)
bash
#!/bin/bash
# auto_reshard.sh
CLUSTER_NODE="192.168.1.101:6379"
NEW_NODE_ID="" # 需要先获取新节点的ID
# 1. 获取新节点ID
NEW_NODE_ID=$(redis-cli -h 192.168.1.104 -p 6379 cluster myid | tr -d '"')
echo "新节点ID: $NEW_NODE_ID"
# 2. 执行自动重新平衡
# --cluster-use-empty-masters: 使用空的主节点
# --cluster-threshold: 平衡阈值,默认2表示差异超过2%时触发平衡
# --cluster-timeout: 迁移超时时间(毫秒)
redis-cli --cluster rebalance $CLUSTER_NODE \
--cluster-weight ${NEW_NODE_ID}=1 \
--cluster-use-empty-masters \
--cluster-threshold 1 \
--cluster-timeout 60000
方案B:手动控制迁移(更精确)
bash
#!/bin/bash
# manual_reshard.sh
SOURCE_NODE1="192.168.1.101:6379"
SOURCE_NODE2="192.168.1.102:6379"
SOURCE_NODE3="192.168.1.103:6379"
NEW_NODE="192.168.1.104:6379"
SLOTS_PER_MOVE=100 # 每次迁移100个槽,避免一次性迁移过多
# 1. 获取所有节点ID
NODE1_ID=$(redis-cli -h 192.168.1.101 -p 6379 cluster myid | tr -d '"')
NODE2_ID=$(redis-cli -h 192.168.1.102 -p 6379 cluster myid | tr -d '"')
NODE3_ID=$(redis-cli -h 192.168.1.103 -p 6379 cluster myid | tr -d '"')
NEW_NODE_ID=$(redis-cli -h 192.168.1.104 -p 6379 cluster myid | tr -d '"')
echo "节点ID列表:"
echo "- 节点1: $NODE1_ID"
echo("- 节点2: $NODE2_ID")
echo("- 节点3: $NODE3_ID")
echo("- 新节点: $NEW_NODE_ID")
# 2. 从每个源节点迁移槽到新节点
for source in "$NODE1_ID" "$NODE2_ID" "$NODE3_ID"; do
echo "从节点 $source 迁移槽到新节点..."
# 交互式迁移,每次迁移1365个槽(根据计算得出)
redis-cli --cluster reshard $SOURCE_NODE1 \
--cluster-from $source \
--cluster-to $NEW_NODE_ID \
--cluster-slots 1365 \
--cluster-yes \
--cluster-timeout 30000 \
--cluster-pipeline 10 # 每次迁移10个key
# 等待10秒,让集群稳定
sleep 10
# 检查迁移进度
redis-cli --cluster check $SOURCE_NODE1 | grep -A5 "Slot distribution"
done
# 3. 验证最终槽分布
echo "最终槽分布验证:"
redis-cli --cluster check $SOURCE_NODE1
2.4 迁移过程监控
bash
#!/bin/bash
# migration_monitor.sh
CLUSTER_NODE="192.168.1.101:6379"
LOG_FILE="migration_monitor_$(date +%Y%m%d_%H%M%S).log"
monitor_migration() {
while true; do
echo "========================================" >> $LOG_FILE
echo "时间: $(date)" >> $LOG_FILE
echo "========================================" >> $LOG_FILE
# 1. 检查集群健康状态
echo "[集群状态]" >> $LOG_FILE
redis-cli --cluster check $CLUSTER_NODE 2>&1 | tail -20 >> $LOG_FILE
# 2. 检查槽迁移状态
echo -e "\n[槽状态]" >> $LOG_FILE
redis-cli -h 192.168.1.101 -p 6379 cluster slots | \
awk '{print "节点区间:", $1"-"$2, "数量:", $2-$1+1}' | \
sort -n >> $LOG_FILE
# 3. 检查各节点内存使用
echo -e "\n[内存使用]" >> $LOG_FILE
for node in 192.168.1.{101,102,103,104}:6379; do
mem=$(redis-cli -c -h ${node%:*} -p ${node#*:} info memory | \
grep "used_memory_human" | cut -d: -f2)
echo "$node: $mem" >> $LOG_FILE
done
# 4. 检查迁移中的key数量
echo -e "\n[各节点键数量]" >> $LOG_FILE
for node in 192.168.1.{101,102,103,104}:6379; do
count=$(redis-cli -c -h ${node%:*} -p ${node#*:} dbsize)
echo "$node: $count keys" >> $LOG_FILE
done
# 5. 检查连接数
echo -e "\n[连接数]" >> $LOG_FILE
for node in 192.168.1.{101,102,103,104}:6379; do
conn=$(redis-cli -c -h ${node%:*} -p ${node#*:} info clients | \
grep "connected_clients" | cut -d: -f2)
echo "$node: $conn connections" >> $LOG_FILE
done
# 等待30秒后再次检查
sleep 30
# 清屏并显示最新状态
clear
tail -50 $LOG_FILE
done
}
# 启动监控
monitor_migration
2.5 迁移后验证
bash
#!/bin/bash
# post_migration_validation.sh
echo "=========== 迁移后验证 ==========="
# 1. 验证集群状态
echo "1. 集群状态检查..."
redis-cli --cluster check 192.168.1.101:6379
if [ $? -eq 0 ]; then
echo "✓ 集群状态正常"
else
echo "✗ 集群状态异常"
exit 1
fi
# 2. 验证槽分布
echo -e "\n2. 槽分布验证..."
TOTAL_SLOTS=$(redis-cli -h 192.168.1.101 -p 6379 cluster info | grep "cluster_slots_assigned" | cut -d: -f2)
if [ "$TOTAL_SLOTS" -eq 16384 ]; then
echo "✓ 所有槽已分配 (16384)"
else
echo "✗ 槽分配不完整: $TOTAL_SLOTS"
exit 1
fi
# 3. 验证槽是否均匀分布
echo -e "\n3. 槽分布均匀性检查..."
SLOT_DISTRIBUTION=$(redis-cli --cluster info 192.168.1.101:6379 | grep -A4 "Slot distribution")
echo "$SLOT_DISTRIBUTION"
# 4. 数据抽样验证
echo -e "\n4. 数据抽样验证..."
# 随机测试一些key是否可访问
TEST_KEYS=("user:1001" "session:abc123" "product:500" "order:20230101")
for key in "${TEST_KEYS[@]}"; do
# 先设置测试key(如果不存在)
redis-cli -c -h 192.168.1.101 -p 6379 set "$key" "test_value_$(date +%s)" > /dev/null 2>&1
# 获取key值
result=$(redis-cli -c -h 192.168.1.101 -p 6379 get "$key" 2>/dev/null)
if [ -n "$result" ]; then
echo "✓ Key '$key' 访问正常"
else
echo "✗ Key '$key' 访问失败"
fi
done
# 5. 性能测试
echo -e "\n5. 简单性能测试..."
START_TIME=$(date +%s%N)
for i in {1..100}; do
redis-cli -c -h 192.168.1.101 -p 6379 set "perf_test:$i" "value_$i" > /dev/null 2>&1
done
END_TIME=$(date +%s%N)
DURATION=$((($END_TIME - $START_TIME)/1000000))
echo "100次SET操作耗时: ${DURATION}ms"
# 6. 清理测试数据
echo -e "\n6. 清理测试数据..."
for i in {1..100}; do
redis-cli -c -h 192.168.1.101 -p 6379 del "perf_test:$i" > /dev/null 2>&1
done
for key in "${TEST_KEYS[@]}"; do
redis-cli -c -h 192.168.1.101 -p 6379 del "$key" > /dev/null 2>&1
done
echo -e "\n=========== 验证完成 ==========="
三、迁移优化和注意事项
3.1 性能优化参数
bash
# 在迁移命令中调整以下参数优化性能:
redis-cli --cluster reshard <host>:<port> \
--cluster-slots <num> \
--cluster-pipeline 100 \ # 增大pipeline大小
--cluster-timeout 120000 \ # 增加超时时间
--cluster-replace \ # 允许替换已存在的key
--cluster-yes # 自动确认
3.2 大key特殊处理
如果集群中有大key(> 1MB),需要特殊处理:
bash
# 1. 查找大key
redis-cli -h 192.168.1.101 -p 6379 --bigkeys
# 2. 对大key所在槽单独迁移
# 使用较小的pipeline值
redis-cli --cluster reshard 192.168.1.101:6379 \
--cluster-from <source_node_id> \
--cluster-to <target_node_id> \
--cluster-slots <slot_number> \
--cluster-pipeline 1 \ # 对大key使用较小的pipeline
--cluster-timeout 300000
3.3 回滚方案
如果迁移出现问题,需要回滚:
bash
# 1. 停止新节点
redis-cli -h 192.168.1.104 -p 6379 shutdown
# 2. 将槽迁移回原节点
# 需要记录迁移前的槽分布,然后反向迁移
# 3. 从集群中移除问题节点
redis-cli --cluster del-node 192.168.1.101:6379 <new_node_id>
# 4. 恢复备份配置
# 如果有配置备份,恢复到之前的状态
3.4 客户端配置更新
迁移完成后,更新客户端配置:更新配置时采用滚动更新,避免全量重启
python
# Python客户端配置示例
import redis
# 更新节点列表
cluster_nodes = [
{"host": "192.168.1.101", "port": 6379},
{"host": "192.168.1.102", "port": 6379},
{"host": "192.168.1.103", "port": 6379},
{"host": "192.168.1.104", "port": 6379}, # 新增节点
]
# 重新初始化集群连接
cluster_client = redis.RedisCluster(
startup_nodes=cluster_nodes,
decode_responses=True,
socket_connect_timeout=5,
retry_on_timeout=True,
max_connections=50
)
3.5 平滑迁移策略:
-
分批次迁移:按 key 前缀或槽位范围分批迁移
-
监控迁移速度:控制迁移速度,避免影响线上性能
-
数据校验:迁移后验证数据一致性
-
回滚计划:准备回滚方案
3.6. 最佳实践建议
-
选择低峰期:在业务低峰时段执行迁移
-
充分备份:迁移前备份所有节点数据
-
监控指标:
内存使用率、QPS 和延迟、网络带
-
测试验证:在测试环境先演练完整流程
3.7 注意事项
-
数据一致性:迁移过程中确保数据不丢失
-
客户端连接:客户端需要感知新的节点拓扑
-
主从复制:如果有主从架构,需要考虑复制链路的调整
-
持久化:迁移期间可能需要暂时关闭 AOF 重写等操作
四、完整迁移脚本
适合生产环境中,特别是对于大型Redis集群,**先添加为从节点再提升为主节点,**以利用数据预同步,减少迁移时间。
bash
#!/bin/bash
# complete_migration.sh
#
# Redis集群从3节点扩容到4节点完整迁移脚本
# 使用方法: ./complete_migration.sh <new_node_ip>
set -e # 遇到错误立即退出
NEW_NODE="${1:-192.168.1.104}"
PORT=6379
CLUSTER_ENTRY="192.168.1.101:6379"
LOG_DIR="/var/log/redis_migration"
mkdir -p $LOG_DIR
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a $LOG_DIR/migration.log
}
check_prerequisites() {
log "检查前置条件..."
# 检查Redis版本
redis_version=$(redis-cli -v | awk '{print $2}' | cut -d. -f1)
if [ "$redis_version" -lt 5 ]; then
log "错误: Redis版本需要5.0以上"
exit 1
fi
# 检查集群状态
if ! redis-cli --cluster check $CLUSTER_ENTRY > /dev/null 2>&1; then
log "错误: 集群状态不正常"
exit 1
fi
# 检查新节点是否可访问
if ! redis-cli -h $NEW_NODE -p $PORT ping > /dev/null 2>&1; then
log "错误: 新节点无法访问"
exit 1
fi
log "✓ 前置条件检查通过"
}
add_new_node() {
log "添加新节点到集群..."
redis-cli --cluster add-node ${NEW_NODE}:${PORT} $CLUSTER_ENTRY \
--cluster-slave \
--cluster-master-id $(redis-cli -h $CLUSTER_ENTRY cluster nodes | grep master | head -1 | awk '{print $1}') \
2>&1 | tee -a $LOG_DIR/add_node.log
# 等待节点同步
sleep 10
# 将新节点提升为主节点
NEW_NODE_ID=$(redis-cli -h $NEW_NODE -p $PORT cluster myid | tr -d '"')
redis-cli --cluster rebalance $CLUSTER_ENTRY \
--cluster-weight ${NEW_NODE_ID}=1 \
--cluster-use-empty-masters \
--cluster-threshold 1 \
2>&1 | tee -a $LOG_DIR/promote_node.log
log "✓ 新节点添加完成"
}
perform_migration() {
log "开始数据迁移..."
# 获取新节点ID
NEW_NODE_ID=$(redis-cli -h $NEW_NODE -p $PORT cluster myid | tr -d '"')
# 执行重新分片
redis-cli --cluster reshard $CLUSTER_ENTRY \
--cluster-from all \
--cluster-to $NEW_NODE_ID \
--cluster-slots 4096 \
--cluster-yes \
--cluster-timeout 120000 \
--cluster-pipeline 50 \
2>&1 | tee -a $LOG_DIR/reshard.log
log "✓ 数据迁移完成"
}
post_migration_checks() {
log "执行迁移后检查..."
# 检查集群状态
if redis-cli --cluster check $CLUSTER_ENTRY > $LOG_DIR/final_check.log 2>&1; then
log "✓ 集群状态正常"
else
log "✗ 集群状态异常,请检查日志: $LOG_DIR/final_check.log"
exit 1
fi
# 验证槽分布
slots_assigned=$(redis-cli -h ${CLUSTER_ENTRY%:*} -p ${CLUSTER_ENTRY#*:} cluster info | \
grep "cluster_slots_assigned" | cut -d: -f2 | tr -d '\r')
if [ "$slots_assigned" -eq 16384 ]; then
log "✓ 所有槽已正确分配"
else
log "✗ 槽分配不完整: $slots_assigned/16384"
exit 1
fi
log "✓ 所有检查通过"
}
main() {
log "开始Redis集群扩容迁移"
log "目标: 从3节点扩容到4节点"
log "新节点: $NEW_NODE:$PORT"
check_prerequisites
add_new_node
perform_migration
post_migration_checks
log "迁移完成!"
log "详细信息请查看: $LOG_DIR/"
}
main "$@"
五、监控和告警脚本
迁移期间设置监控:
bash
# 监控关键指标
watch -n 5 '
echo "=== Redis集群迁移监控 ==="
echo "时间: $(date)"
echo
echo "1. 集群状态:"
redis-cli -h 192.168.1.101 -p 6379 cluster info | grep -E "(cluster_state|cluster_slots_ok|cluster_known_nodes)"
echo
echo "2. 迁移进度:"
redis-cli --cluster info 192.168.1.101:6379 | grep -A2 "Slot distribution"
echo
echo "3. 节点状态:"
for node in 101 102 103 104; do
echo -n "192.168.1.$node: "
redis-cli -h 192.168.1.$node -p 6379 ping 2>/dev/null && \
redis-cli -h 192.168.1.$node -p 6379 info memory | grep used_memory_human | cut -d: -f2
done
'
这个详细方案提供了完整的迁移流程,包括准备、执行、验证和监控各个阶段的具体操作步骤和脚本,可以根据实际情况进行调整使用。
六、 Redis Cluster 为什么只有 16384 个槽位?
技术原因 - 心跳包大小限制
Redis 集群节点之间通过 CLUSTER MEET 消息通信,其中包含了节点负责的槽位信息。每个槽位用 1 个 bit 表示(0 或 1)。
-
如果槽位数太多:
-
假设 65536 个槽位(2^16),心跳包需要
65536 / 8 = 8192 bytes = 8KB -
每个节点每秒钟向其他 N-1 个节点发送心跳
-
10 个节点的集群:
8KB * 10 * 2(收发)≈ 160KB/s的网络开销
-
-
16384 个槽位:
-
16384 / 8 = 2048 bytes = 2KB -
同样的 10 节点集群:
2KB * 10 * 2 ≈ 40KB/s -
网络开销更合理
-
性能与扩展性的平衡
python
# 计算示例
65536 槽位:节点数太少时,每个节点槽位太多 → 数据倾斜
16384 槽位:更合理的分布,即使节点少也能均匀分布
# 实际最大节点数理论
16384 个槽位 / 最低建议每个节点 100 个槽位 ≈ 163 个节点
对于绝大多数场景,163 个节点已经足够。
Redis 作者的解释
Antirez(Redis 作者)在 GitHub issue 中解释:
-
16384 是 2^14,足够大以实现良好分布
-
65536 会增加心跳包大小,网络开销较大
-
CRC16 算法输出 16 位(65536),但 Redis 只使用 14 位(16384)
为什么不能修改这个数字?
-
兼容性:所有 Redis 客户端都硬编码了 16384
-
协议固定:Redis 集群协议中槽位数量是固定的
-
工具链依赖:所有集群管理工具都基于这个设计
为什么不需要修改这个数字?
16384 是 Redis Cluster 的硬编码限制,它是在性能、扩展性和实现复杂度之间的最佳平衡点。这个设计虽然限制了理论最大节点数,但足以满足绝大多数生产场景的需求。如果你的应用需要更多节点,可能需要考虑其他分片方案,如基于代理的分片(Codis)或客户端分片。
七、槽位分配示例
3 个节点的分配(原本):
text
Node1: slots 0-5460
Node2: slots 5461-10922
Node3: slots 10923-16383
增加到 4 个节点后:
text
Node1: slots 0-4095 # 迁移 1365 个槽位给 Node4
Node2: slots 4096-8191 # 迁移 1365 个槽位给 Node4
Node3: slots 8192-12287 # 迁移 1365 个槽位给 Node4
Node4: slots 12288-16383 # 获得 4096 个槽位
槽位计算公式
Redis 使用 CRC16 算法计算 key 属于哪个槽位:
python
def slot(key):
# 1. 如果 key 包含 "{}",只计算括号内的部分
# 2. 计算 CRC16(key) mod 16384
return crc16(key_with_hash_tag) % 16384
实践建议
虽然槽位数量固定,但实际应用中:
-
每个节点建议最少 100 个槽位,保证数据分布均匀
-
最大推荐节点数 :
16384 / 100 ≈ 163 个主节点 -
如果节点数 < 槽位数,Redis 会自动平均分配槽位
-
数据倾斜时:可以手动调整槽位分布