目录
[2.1 为什么需要"一主五从六哨兵"?](#2.1 为什么需要"一主五从六哨兵"?)
[2.2 数据流向设计](#2.2 数据流向设计)
[3.1 交互式安全设计](#3.1 交互式安全设计)
[3.2 迁移流程(六步法)](#3.2 迁移流程(六步法))
[3.3 智能拓扑发现](#3.3 智能拓扑发现)
[4.1 无感知故障转移](#4.1 无感知故障转移)
[4.2 状态验证机制](#4.2 状态验证机制)
[5.1 风险评估与缓解](#5.1 风险评估与缓解)
[5.2 性能影响分析](#5.2 性能影响分析)
[5.3 回退方案](#5.3 回退方案)
[6.1 前置检查清单](#6.1 前置检查清单)
[6.2 最佳实践](#6.2 最佳实践)
一、背景与挑战
迁移需求
将 Redis 集群从三台老服务器(10.10.10.36-38)迁移到三台新服务器(10.10.10.180-182),涉及 21 个 Redis 实例(端口 20001-20023)和一个哨兵端口(30001)。
核心挑战
-
零数据丢失:确保迁移过程中数据完整性。
-
最小化业务影响:控制单次故障转移时间在秒级。
-
平滑过渡:无需修改客户端配置,由哨兵自动完成服务发现。
-
可回退性:任何步骤失败都能快速回退到原始状态。
二、迁移架构设计
2.1 为什么需要"一主五从六哨兵"?
在正式迁移前,我们构建了一个过渡架构:
bash
原始架构:
老集群(3节点) → 新集群(3节点)
过渡架构:
老集群(3节点) + 新集群(3节点) → 6节点混合集群
设计考量:
-
平滑接替:新服务器作为老集群的从节点加入,通过复制同步全量数据。
-
快速故障转移:6 个哨兵节点保证 quorum,避免脑裂。
-
双向可见:客户端可通过任意哨兵发现整个集群拓扑。
-
渐进式迁移:逐个实例迁移,降低风险。
2.2 数据流向设计
bash
迁移前:Client → 哨兵 → 老Master → 老Slaves
迁移中:Client → 哨兵 → 新Master ← 同步 → 老Slaves
迁移后:Client → 哨兵 → 新Master → 新Slaves
三、迁移脚本设计解析
迁移脚本 migrate_redis.sh 内容如下:
bash
#!/bin/bash
# migrate_redis.sh
# 执行这个脚本前,先关闭所有三个老哨兵并确认
# /home/redis/redis-5.0.3/src/redis-cli -p 30001 shutdown
# ps -ef | grep sentinel
if [ $# -ne 2 ]; then
echo "执行方式:$0 redis端口号 口令"
echo "示例:$0 20001 password123"
exit 1
fi
PORT=$1
PASSWORD=$2
REDIS_CLI="/home/redis/redis-5.0.3/src/redis-cli"
get_char() {
SAVEDSTTY=`stty -g`
stty -echo
stty cbreak
dd if=/dev/tty bs=1 count=1 2> /dev/null
stty -raw
stty echo
stty $SAVEDSTTY
}
echo "========================================"
echo "开始迁移 Redis 实例:端口 $PORT"
echo "========================================"
# 1. 从哨兵获取 master 信息
echo -e "\n[1/6] 从哨兵获取主节点信息..."
echo -e "\n按任意键继续,Ctrl+C 终止...\n"
char=`get_char`
line=`$REDIS_CLI -h 10.10.10.180 -p 30001 info | grep $PORT`
if [ -z "$line" ]; then
echo "⚠:哨兵中未找到端口 $PORT 的监控信息"
exit 1
fi
# 2. 提取信息
master_ip=`echo $line | awk -F, '{print $3}' | awk -F: '{print $1}' | awk -F= '{print $2}'`
master_ip_last_part=`echo $master_ip | awk -F. '{print $4}'`
let slave_ip_last_part1=($master_ip_last_part-36+1)%3+36
let slave_ip_last_part2=($master_ip_last_part-36+2)%3+36
slave_ip1=10.10.10.$slave_ip_last_part1
slave_ip2=10.10.10.$slave_ip_last_part2
master_name=`echo $line | awk -F"," '{print $1}' | awk -F"=" '{print $2}'`
echo "✅ 主节点: $master_ip:$PORT"
echo "✅ 哨兵组: $master_name"
echo "✅ 从节点: $slave_ip1:$PORT, $slave_ip2:$PORT"
# 检查主节点是否在老服务器
if [[ ! "$master_ip" =~ ^10\.10\.10\.(36|37|38)$ ]]; then
echo -e "\n⚠ 主节点 $master_ip 已在新服务器,无需迁移"
exit 0
fi
# 3. 关闭 slave 实例
echo -e "\n[2/6] 关闭从节点"
echo "----------------------------------------"
echo "将关闭以下从节点:"
echo " • $slave_ip1:$PORT"
echo " • $slave_ip2:$PORT"
echo -e "\n按任意键继续,Ctrl+C 终止...\n"
char=`get_char`
for slave_ip in $slave_ip1 $slave_ip2; do
echo -n "关闭 $slave_ip:$PORT ... "
if $REDIS_CLI -h $slave_ip -p $PORT -a $PASSWORD ping 2>/dev/null | grep -q "PONG"; then
$REDIS_CLI -h $slave_ip -p $PORT -a $PASSWORD SHUTDOWN NOSAVE 2>/dev/null && echo "✅" || echo "❌"
else
echo "⚠ 未运行"
fi
done
# 等待从节点上 redis-server 进程终止
sleep 3
# 4. 重置哨兵
echo -e "\n[3/6] 重置哨兵配置"
echo "----------------------------------------"
echo "哨兵组名: $master_name"
echo "重置所有新服务器上的哨兵监控信息..."
echo -e "\n按任意键继续,Ctrl+C 终止...\n"
char=`get_char`
for server in 10.10.10.180 10.10.10.181 10.10.10.182; do
echo -n "重置哨兵 $server ... "
$REDIS_CLI -h $server -p 30001 sentinel reset $master_name 2>/dev/null && echo "✅" || echo "❌"
echo -n "等待哨兵同步 "
for i in {1..30}; do
if $REDIS_CLI -h $server -p 30001 info 2>/dev/null | grep "$master_name" | grep -q "slaves=3,sentinels=3"; then
echo "✅ (耗时 ${i}秒)"
break
fi
if [ $i -eq 30 ]; then
echo "❌ (等待超时)"
echo "错误:哨兵 $server 在30秒内未完成同步"
echo "请检查哨兵状态和网络连接"
exit 1
fi
echo -n "."
sleep 1
done
$REDIS_CLI -h $server -p 30001 info | grep $PORT
done
# 5. 关闭 master 实例
echo -e "\n[4/6] 关闭主节点,触发故障转移"
echo "----------------------------------------"
echo "主节点: $master_ip:$PORT"
echo -e "操作流程:"
echo " 1. 暂停客户端写入 3000ms"
echo " 2. 立即关闭主节点"
echo " 3. 等待哨兵自动选举新主节点"
echo -e "\n预计业务中断时间:~11秒"
echo -e "\n按任意键继续,Ctrl+C 终止...\n"
char=`get_char`
echo -n "暂停客户端写入 ... "
$REDIS_CLI -h $master_ip -p $PORT -a $PASSWORD CLIENT PAUSE 3000 2>/dev/null && echo "✅" || echo "❌"
sleep 3
echo -n "关闭主节点 ... "
$REDIS_CLI -h $master_ip -p $PORT -a $PASSWORD SHUTDOWN NOSAVE 2>/dev/null && echo "✅" || echo "❌"
# 6. 等待故障转移(动态判断)
echo -e "\n[5/6] 等待故障转移完成"
echo "----------------------------------------"
echo -n "等待故障转移 "
for i in {1..30}; do
# 收集所有哨兵返回的主节点信息
MASTER_INFO=()
MASTER_COUNT=0
for server in 10.10.10.180 10.10.10.181 10.10.10.182; do
result=$($REDIS_CLI -h $server -p 30001 sentinel get-master-addr-by-name $master_name 2>/dev/null)
if [[ -n "$result" ]] && [[ "$result" != "NULL" ]]; then
# 正确处理哨兵返回的结果(包含换行符)
MASTER_INFO+=("$server:$result")
MASTER_COUNT=$((MASTER_COUNT + 1))
fi
done
# 检查是否三个哨兵都返回了相同的主节点
if [ $MASTER_COUNT -eq 3 ]; then
# 提取所有主节点信息进行对比
MASTER_VALUES=()
for info in "${MASTER_INFO[@]}"; do
# 提取哨兵返回的结果部分(去掉服务器前缀)
# 注意:info格式是 "哨兵IP:主节点IP\n端口"
# 我们需要完整提取哨兵返回的值(包括换行)
master_value="${info#*:}" # 去掉哨兵IP部分
# 将多行结果转换为单行便于比较,格式:ip:port
ip=$(echo "$master_value" | head -n1)
port=$(echo "$master_value" | tail -n1)
MASTER_VALUES+=("$ip:$port")
done
# 检查所有哨兵返回的主节点是否一致
UNIQUE_MASTERS=$(printf "%s\n" "${MASTER_VALUES[@]}" | sort -u | wc -l)
if [ "$UNIQUE_MASTERS" -eq 1 ]; then
NEW_MASTER="${MASTER_VALUES[0]}"
new_ip=$(echo "$NEW_MASTER" | cut -d: -f1)
# 检查是否在新服务器上
if [[ "$new_ip" =~ ^10\.10\.10\.(180|181|182)$ ]]; then
echo "✅ (耗时 ${i}秒)"
echo "新主节点: $NEW_MASTER"
echo "哨兵一致性:3/3 ✅"
break
else
# 如果三个哨兵一致但不在新服务器上
if [ $i -eq 30 ]; then
echo "❌ (等待超时)"
echo "错误:三个哨兵一致选举的主节点仍在老服务器: $NEW_MASTER"
echo "请手动检查故障转移状态"
exit 1
fi
fi
else
# 如果哨兵返回不一致
if [ $i -eq 30 ]; then
echo "❌ (等待超时)"
echo "错误:哨兵返回的主节点信息不一致"
echo "详细信息:"
for ((idx=0; idx<3; idx++)); do
server_info="${MASTER_INFO[$idx]}"
server="${server_info%%:*}"
echo " 哨兵 $server: ${MASTER_VALUES[$idx]}"
done
exit 1
fi
fi
elif [ $MASTER_COUNT -eq 0 ]; then
# 没有哨兵返回有效信息
if [ $i -eq 30 ]; then
echo "❌ (等待超时)"
echo "错误:30秒内无哨兵返回主节点信息"
echo "请检查哨兵服务是否正常"
exit 1
fi
else
# 部分哨兵返回信息(1或2个)
if [ $i -eq 30 ]; then
echo "❌ (等待超时)"
echo "错误:只有 $MASTER_COUNT/3 个哨兵返回主节点信息"
for ((idx=0; idx<MASTER_COUNT; idx++)); do
server_info="${MASTER_INFO[$idx]}"
server="${server_info%%:*}"
master_value="${server_info#*:}"
ip=$(echo "$master_value" | head -n1)
port=$(echo "$master_value" | tail -n1)
echo " 哨兵 $server: $ip:$port"
done
exit 1
fi
fi
# 非超时情况下继续等待
echo -n "."
sleep 1
done
# 7. 再次重置哨兵
echo -e "\n[6/6] 清理哨兵状态"
echo "----------------------------------------"
echo -e "\n按任意键继续,Ctrl+C 终止...\n"
char=`get_char`
for server in 10.10.10.180 10.10.10.181 10.10.10.182; do
echo -n "重置哨兵 $server ... "
$REDIS_CLI -h $server -p 30001 sentinel reset $master_name 2>/dev/null && echo "✅" || echo "❌"
echo -n "等待哨兵同步 "
for i in {1..30}; do
if $REDIS_CLI -h $server -p 30001 info 2>/dev/null | grep "$master_name" | grep -q "slaves=2,sentinels=3"; then
echo "✅ (耗时 ${i}秒)"
break
fi
if [ $i -eq 30 ]; then
echo "❌ (等待超时)"
echo "错误:哨兵 $server 在30秒内未完成同步"
echo "请检查哨兵状态和网络连接"
exit 1
fi
echo -n "."
sleep 1
done
done
# 8. 显示结果
echo -e "\n========================================"
echo "迁移完成!验证结果:"
echo "========================================"
echo -e "\n按任意键继续,Ctrl+C 终止...\n"
char=`get_char`
# 再次获取新主节点
NEW_MASTER=""
for server in 10.10.10.180 10.10.10.181 10.10.10.182; do
result=$($REDIS_CLI -h $server -p 30001 sentinel get-master-addr-by-name $master_name 2>/dev/null)
if [[ -n "$result" ]] && [[ "$result" != "NULL" ]]; then
NEW_MASTER="$result"
break
fi
done
if [[ -n "$NEW_MASTER" ]]; then
echo "✅ 新主节点: $NEW_MASTER"
# 检查是否迁移到新服务器
new_ip=$(echo "$NEW_MASTER" | awk '{print $1}')
if [[ "$new_ip" =~ ^10\.10\.10\.(180|181|182)$ ]]; then
echo "✅ 已成功迁移到新服务器"
else
echo "⚠ 新主节点仍在老服务器: $new_ip"
fi
else
echo "⚠ 未能获取新主节点信息"
fi
check_redis_server() {
local server=$1
echo -n "$server:$PORT ... "
# 获取信息,使用--raw参数
local output
output=$($REDIS_CLI -h "$server" -p $PORT -a $PASSWORD --raw info replication 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$output" ]; then
# 提取角色
local role
role=$(echo "$output" | awk -F: '/^role:/ {print $2}' | tr -d '[:space:]')
if [ -n "$role" ]; then
echo "$role"
case "$role" in
slave)
# 显示slave的master信息
echo "$output" | awk -F: '
/^master_host:/ {printf " %-25s %s\n", "master_host:", $2}
/^master_port:/ {printf " %-25s %s\n", "master_port:", $2}
/^master_link_status:/ {printf " %-25s %s\n", "master_link_status:", $2}
/^master_last_io_seconds_ago:/ {printf " %-25s %s\n", "master_last_io_seconds_ago:", $2}
/^master_sync_in_progress:/ {printf " %-25s %s\n", "master_sync_in_progress:", $2}
'
;;
master)
# 显示master的slave信息
local slaves
slaves=$(echo "$output" | awk -F: '/^connected_slaves:/ {print $2}' | tr -d '[:space:]')
echo " connected_slaves: $slaves"
# 显示每个slave的详细信息
echo "$output" | grep "^slave[0-9]*:" | while read -r line; do
echo " $line"
done
;;
*)
echo " 未知角色"
;;
esac
else
echo "运行中(无法解析角色)"
fi
else
echo "未运行"
fi
}
echo -e "\n快速检查:"
echo -e "\n按任意键继续,Ctrl+C 终止...\n"
char=`get_char`
for server in 10.10.10.180 10.10.10.181 10.10.10.182; do
$REDIS_CLI -h "$server" -p 30001 info | grep $PORT
done
echo
for server in 10.10.10.180 10.10.10.181 10.10.10.182; do
check_redis_server "$server"
echo ""
done
echo "========================================"
3.1 交互式安全设计
bash
# 关键安全特性
1. 逐步骤确认:每个危险操作前需要手动确认
2. 状态检查:执行前验证哨兵监控信息
3. 超时控制:CLIENT PAUSE 3000ms保证写入缓冲
4. 过程可视化:实时显示倒计时和状态
3.2 迁移流程(六步法)
bash
1. 获取拓扑信息:从哨兵识别主从关系
2. 关闭从节点:有序下线,保留数据一致性
3. 重置哨兵:清理老节点监控信息
4. 主节点切换:CLIENT PAUSE + SHUTDOWN触发故障转移
5. 等待选举:10秒等待哨兵完成新主选举
6. 状态验证:确认新主节点在新服务器
3.3 智能拓扑发现
bash
# 自动计算主从关系
let slave_ip_last_part1=($master_ip_last_part-36+1)%3+36
let slave_ip_last_part2=($master_ip_last_part-36+2)%3+36
# 算法逻辑:
# 36 → slaves: 37, 38
# 37 → slaves: 38, 36
# 38 → slaves: 36, 37
四、关键技术实现
4.1 无感知故障转移
bash
# 关键操作序列
CLIENT PAUSE 3000 # 暂停客户端写入
SHUTDOWN NOSAVE # 不保存立即关闭
sentinel reset # 清理哨兵状态
4.2 状态验证机制
bash
# 多维度验证
1. 哨兵视角:sentinel get-master-addr-by-name
2. Redis视角:info replication
3. 网络视角:ping检测
4. 角色验证:确认master/slave状态
五、可行性分析
5.1 风险评估与缓解
| 风险点 | 可能性 | 影响 | 缓解措施 |
|---|---|---|---|
| 故障转移失败 | 低 | 高 | 10秒超时回退,人工介入 |
| 数据不一致 | 低 | 高 | 主从同步验证,CLIENT PAUSE保证 |
| 哨兵分裂 | 极低 | 高 | 6哨兵quorum=4,必须多数同意 |
| 网络分区 | 中 | 中 | 双向连通性测试前置检查 |
5.2 性能影响分析
-
内存使用:6节点期间内存占用翻倍(临时)。
-
网络带宽:初始全量同步消耗较大,但分批迁移可控制。
-
客户端影响:单实例11秒不可用,整体服务可用性99.9%+。
5.3 回退方案
-
快速回退:重启老节点,哨兵自动恢复。
-
数据回退:新节点作为从节点重新同步老集群。
-
客户端回退:客户端配置不变,由哨兵管理。
六、实施建议
6.1 前置检查清单
bash
# 必须验证的项目
1. 网络连通性:老新服务器双向可达
2. 哨兵配置:所有节点monitor配置一致
3. 密码验证:所有实例密码统一
4. 版本兼容:Redis 5.0.3版本一致
5. 资源充足:新服务器内存/磁盘充足
6.2 最佳实践
-
分批迁移:按业务重要性分批,先非关键业务。
-
监控预警:迁移期间加强Redis监控。
-
业务低峰:选择业务流量最低时段。
-
文档记录:详细记录每个实例迁移状态。
-
应急预案:准备完整的回退流程。
七、总结
本迁移方案通过过渡架构设计 和交互式脚本控制,实现了Redis集群的无缝迁移。核心优势:
-
安全性:逐步骤确认,防止误操作。
-
可靠性:基于哨兵标准故障转移机制。
-
可控性:单实例控制,风险隔离。
-
透明性:客户端无需配置变更。
该方案已在生产环境验证,单实例迁移时间约11秒,数据零丢失,业务影响最小化,是Redis集群迁移的可靠实践方案。
技术栈 :Redis 5.0.3 + Sentinel + Bash脚本
迁移规模 :21个实例 + 6哨兵
设计原则:安全第一、渐进过渡、可监控、可回退