Files
sa-charts/docker-swarm-review/redis-review-132/failover-to-review.sh
2026-05-13 15:27:39 +08:00

870 lines
32 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
#
# Review Redis 故障切换脚本
# 当生产环境 Redis 故障时,使用此脚本将 Review 环境切换为主服务
#
# 使用方法:
# ./failover-to-review.sh status - 查看当前状态
# ./failover-to-review.sh failover - 执行故障切换(提升 132 为 Master
# ./failover-to-review.sh sentinel - 仅部署 Review Sentinel
# ./failover-to-review.sh full - 完整切换failover + sentinel
# ./failover-to-review.sh rollback - 安全回滚(先同步数据到生产,再切换)
#
# 安全回滚流程:
# 前置条件:【重要】必须先停止或暂停生产 Sentinel防止自动故障转移
# 1. 让 10.56 先作为 132 的从节点同步数据(反向同步)
# 2. 等待数据同步完成
# 3. 将 132 设为只读,防止切换期间数据丢失
# 4. 提升 10.56 为主节点
# 5. 132 重新指向 10.56,恢复级联复制结构
# 6. 重新启动生产 Sentinel
#
# 数据一致性保证:
# - 回滚前:通过反向同步确保故障期间的数据同步到生产
# - 回滚中:将 132 设为只读min-replicas-to-write=99防止数据丢失
# - 回滚后:验证偏移量差异,确保数据完全同步
#
set -e
# ==================== 全局参数 ====================
# -y 或 --yes: 跳过确认提示(危险操作仍需确认)
# --force: 强制执行,跳过所有确认
AUTO_CONFIRM=false
FORCE_MODE=false
# 解析命令行参数
parse_global_args() {
for arg in "$@"; do
case $arg in
-y|--yes)
AUTO_CONFIRM=true
;;
--force)
AUTO_CONFIRM=true
FORCE_MODE=true
;;
esac
done
}
# 确认提示(如果 AUTO_CONFIRM=true 则自动确认)
confirm_prompt() {
local message=$1
local default=${2:-n} # 默认值 n
if [ "$AUTO_CONFIRM" = true ]; then
log_info "自动确认: $message"
return 0
fi
read -p "$message (y/n): " confirm
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
return 0
fi
return 1
}
# 危险操作确认(即使 AUTO_CONFIRM=true 也需要确认,除非 FORCE_MODE
confirm_dangerous() {
local message=$1
if [ "$FORCE_MODE" = true ]; then
log_warn "强制模式: 跳过危险操作确认"
return 0
fi
read -p "$message (y/n): " confirm
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
return 0
fi
return 1
}
# ==================== 配置 ====================
REDIS_PASSWORD="sino#650"
SENTINEL_PASSWORD="sino#650"
# Review 节点
NODE_132="192.168.3.132"
NODE_133="192.168.3.133"
NODE_134="192.168.3.134"
# 生产节点
PROD_MASTER="192.168.10.56"
PROD_PORT=6379
REDIS_PORT=6379
SENTINEL_PORT=26379
# 脚本目录
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# ==================== 颜色输出 ====================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# ==================== Docker/Redis 命令封装 ====================
# Redis 镜像(用于临时容器执行命令)
REDIS_IMAGE="bitnami/redis:7.0.11"
# 通过 Docker 容器执行 Redis 命令
# 优先使用已运行的 review_redis_master 容器,否则使用临时容器
redis_cmd() {
local host=$1
local port=$2
shift 2
# 尝试获取运行中的 redis master 容器
local container_id=$(docker ps -q -f name=review_redis_master 2>/dev/null | head -1)
if [ -n "$container_id" ]; then
# 使用已运行的容器
docker exec "$container_id" redis-cli -h "$host" -p "$port" -a "$REDIS_PASSWORD" --no-auth-warning "$@" 2>/dev/null
else
# 使用临时容器执行命令
docker run --rm --network host "$REDIS_IMAGE" redis-cli -h "$host" -p "$port" -a "$REDIS_PASSWORD" --no-auth-warning "$@" 2>/dev/null
fi
}
# 检查 Redis 镜像是否存在,不存在则提示
check_redis_image() {
if ! docker image inspect "$REDIS_IMAGE" &>/dev/null; then
log_warn "Redis 镜像 $REDIS_IMAGE 不存在"
log_info "请先拉取镜像: docker pull $REDIS_IMAGE"
return 1
fi
return 0
}
# ==================== 功能函数 ====================
# 检查节点状态
check_status() {
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} 当前 Redis 状态 ${NC}"
echo -e "${BLUE}========================================${NC}"
for node in $NODE_132 $NODE_133 $NODE_134; do
echo ""
echo -e "${GREEN}[$node]${NC}"
local info=$(redis_cmd "$node" $REDIS_PORT INFO replication 2>/dev/null)
if [ -z "$info" ]; then
echo -e " ${RED}无法连接${NC}"
continue
fi
local role=$(echo "$info" | grep "^role:" | cut -d: -f2 | tr -d '\r\n')
local master_host=$(echo "$info" | grep "^master_host:" | cut -d: -f2 | tr -d '\r\n')
local master_port=$(echo "$info" | grep "^master_port:" | cut -d: -f2 | tr -d '\r\n')
local master_link=$(echo "$info" | grep "^master_link_status:" | cut -d: -f2 | tr -d '\r\n')
local slaves=$(echo "$info" | grep "^connected_slaves:" | cut -d: -f2 | tr -d '\r\n')
echo -e " 角色: ${YELLOW}$role${NC}"
if [ "$role" = "slave" ]; then
echo -e " 主节点: $master_host:$master_port"
echo -e " 同步状态: $master_link"
else
echo -e " 从节点数: $slaves"
fi
done
echo ""
}
# 检查生产环境状态
check_prod_status() {
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} 生产环境 Redis 状态 ${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
local info=$(redis_cmd "$PROD_MASTER" $PROD_PORT INFO replication 2>/dev/null)
if [ -z "$info" ]; then
echo -e "${RED}生产环境 $PROD_MASTER:$PROD_PORT 无法连接${NC}"
return 1
fi
local role=$(echo "$info" | grep "^role:" | cut -d: -f2 | tr -d '\r\n')
echo -e "生产主节点 $PROD_MASTER: ${GREEN}$role${NC}"
return 0
}
# 提升 132 为 Master
promote_132() {
echo ""
log_info "正在提升 $NODE_132 为 Master..."
# 执行 SLAVEOF NO ONE
local result=$(redis_cmd "$NODE_132" $REDIS_PORT SLAVEOF NO ONE)
if [ "$result" != "OK" ]; then
log_error "SLAVEOF NO ONE 执行失败: $result"
return 1
fi
sleep 2
# 验证角色
local role=$(redis_cmd "$NODE_132" $REDIS_PORT INFO replication | grep "^role:" | cut -d: -f2 | tr -d '\r\n')
if [ "$role" = "master" ]; then
log_info "$NODE_132 已成功提升为 Master"
else
log_error "$NODE_132 提升失败,当前角色: $role"
return 1
fi
}
# 配置从节点指向新 Master
configure_slaves() {
echo ""
log_info "正在配置从节点指向 $NODE_132..."
for node in $NODE_133 $NODE_134; do
local master_host=$(redis_cmd "$node" $REDIS_PORT INFO replication | grep "^master_host:" | cut -d: -f2 | tr -d '\r\n')
if [ "$master_host" = "$NODE_132" ]; then
log_info "[$node] 已指向 $NODE_132"
else
log_warn "[$node] 当前指向 $master_host,正在重新配置..."
redis_cmd "$node" $REDIS_PORT SLAVEOF "$NODE_132" $REDIS_PORT
sleep 1
# 验证
master_host=$(redis_cmd "$node" $REDIS_PORT INFO replication | grep "^master_host:" | cut -d: -f2 | tr -d '\r\n')
if [ "$master_host" = "$NODE_132" ]; then
log_info "[$node] 配置成功"
else
log_error "[$node] 配置失败"
fi
fi
done
}
# 部署 Sentinel
deploy_sentinel() {
echo ""
log_info "正在部署 Review Sentinel..."
cd "$SCRIPT_DIR"
if [ ! -f "./docker-compose-sentinel.yml" ]; then
log_error "未找到 docker-compose-sentinel.yml"
return 1
fi
if [ ! -f "./env_review" ]; then
log_error "未找到 env_review"
return 1
fi
# 部署
env $(cat ./env_review | xargs) envsubst < ./docker-compose-sentinel.yml | docker stack deploy --compose-file - review_sentinel
log_info "Sentinel 部署命令已执行,等待服务启动..."
sleep 10
# 检查 Sentinel 状态
log_info "检查 Sentinel 状态..."
for node in $NODE_132 $NODE_133 $NODE_134; do
local master=$(redis_cmd "$node" $SENTINEL_PORT SENTINEL get-master-addr-by-name reviewmaster 2>/dev/null)
if [ -n "$master" ]; then
log_info "[$node:$SENTINEL_PORT] Sentinel 运行正常Master: $master"
else
log_warn "[$node:$SENTINEL_PORT] Sentinel 未就绪或未响应"
fi
done
}
# 移除 Sentinel
remove_sentinel() {
echo ""
log_info "正在移除 Review Sentinel..."
docker stack rm review_sentinel 2>/dev/null || true
sleep 5
log_info "Sentinel 已移除"
}
# ==================== 安全回滚相关函数 ====================
# 检查生产节点是否可连接
check_prod_connectable() {
local info=$(redis_cmd "$PROD_MASTER" $PROD_PORT PING 2>/dev/null)
if [ "$info" = "PONG" ]; then
return 0
else
return 1
fi
}
# 获取节点的复制偏移量
get_repl_offset() {
local host=$1
local info=$(redis_cmd "$host" $REDIS_PORT INFO replication 2>/dev/null)
local offset=$(echo "$info" | grep "master_repl_offset:" | cut -d: -f2 | tr -d '\r\n')
if [ -z "$offset" ]; then
offset=$(echo "$info" | grep "slave_repl_offset:" | cut -d: -f2 | tr -d '\r\n')
fi
echo "$offset"
}
# 获取节点角色
get_node_role() {
local host=$1
local info=$(redis_cmd "$host" $REDIS_PORT INFO replication 2>/dev/null)
echo "$info" | grep "^role:" | cut -d: -f2 | tr -d '\r\n'
}
# 获取从节点同步状态
get_slave_sync_status() {
local host=$1
local info=$(redis_cmd "$host" $REDIS_PORT INFO replication 2>/dev/null)
echo "$info" | grep "master_link_status:" | cut -d: -f2 | tr -d '\r\n'
}
# 远程执行生产节点命令(尝试自动执行,失败则提示手动)
exec_prod_redis_cmd() {
local redis_args="$@"
local result=""
# 尝试通过网络直接执行命令
log_info "尝试远程执行 Redis 命令..."
result=$(redis_cmd "$PROD_MASTER" $PROD_PORT $redis_args 2>/dev/null)
if [ -n "$result" ]; then
echo "$result"
return 0
fi
# 无法远程执行,提示手动操作
log_warn "无法远程执行命令,请在生产服务器上手动执行"
return 1
}
# 步骤 1: 让生产 10.56 作为 132 的从节点
rollback_step1_make_prod_slave() {
echo ""
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} 步骤 1: 配置生产节点为 132 的从节点 ${NC}"
echo -e "${BLUE}========================================${NC}"
# 检查 132 当前是否为 Master
local role_132=$(get_node_role "$NODE_132")
if [ "$role_132" != "master" ]; then
log_error "132 当前不是 Master (角色: $role_132),无法执行回滚"
log_error "请先确认 132 已通过 failover 提升为 Master"
return 1
fi
log_info "确认 132 当前为 Master"
# 检查生产节点是否可连接
if ! check_prod_connectable; then
log_error "生产节点 $PROD_MASTER 无法连接,请先确保生产 Redis 已恢复"
return 1
fi
log_info "生产节点 $PROD_MASTER 可连接"
# 配置 10.56 作为 132 的从节点
log_info "正在配置 $PROD_MASTER 作为 $NODE_132 的从节点..."
# 尝试直接远程执行
local result=$(redis_cmd "$PROD_MASTER" $PROD_PORT SLAVEOF "$NODE_132" $REDIS_PORT 2>/dev/null)
if [ "$result" = "OK" ]; then
log_info "远程执行成功: SLAVEOF $NODE_132 $REDIS_PORT"
else
# 无法远程执行,提示手动操作
echo ""
echo -e "${YELLOW}========================================${NC}"
echo -e "${YELLOW} 无法远程执行,请在生产服务器 ($PROD_MASTER) 上执行: ${NC}"
echo -e "${YELLOW}========================================${NC}"
echo ""
echo "# 方法1: 进入 Redis 容器执行"
echo "docker exec -it \$(docker ps -q -f name=redis) redis-cli -a '$REDIS_PASSWORD' --no-auth-warning SLAVEOF $NODE_132 $REDIS_PORT"
echo ""
echo "# 方法2: 使用临时容器执行"
echo "docker run --rm --network host $REDIS_IMAGE redis-cli -h $PROD_MASTER -p $PROD_PORT -a '$REDIS_PASSWORD' --no-auth-warning SLAVEOF $NODE_132 $REDIS_PORT"
echo ""
if ! confirm_dangerous "已在生产服务器执行上述命令?"; then
log_warn "操作已取消"
return 1
fi
fi
# 验证配置
sleep 2
local prod_role=$(get_node_role "$PROD_MASTER")
local prod_sync=$(get_slave_sync_status "$PROD_MASTER")
if [ "$prod_role" = "slave" ]; then
log_info "生产节点已配置为从节点"
log_info "同步状态: $prod_sync"
if [ "$prod_sync" = "up" ]; then
log_info "数据同步连接正常"
else
log_warn "同步连接状态: $prod_sync,请等待连接建立"
fi
else
log_error "配置失败,生产节点角色: $prod_role"
return 1
fi
}
# 步骤 2: 等待数据同步完成
rollback_step2_wait_sync() {
echo ""
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} 步骤 2: 等待数据同步完成 ${NC}"
echo -e "${BLUE}========================================${NC}"
local max_wait=300 # 最大等待 5 分钟
local wait_time=0
local check_interval=5
while [ $wait_time -lt $max_wait ]; do
local master_offset=$(get_repl_offset "$NODE_132")
local slave_offset=$(redis_cmd "$PROD_MASTER" $REDIS_PORT INFO replication 2>/dev/null | grep "slave_repl_offset:" | cut -d: -f2 | tr -d '\r\n')
local sync_status=$(get_slave_sync_status "$PROD_MASTER")
echo -e " Master(132) 偏移量: ${GREEN}$master_offset${NC}"
echo -e " Slave(10.56) 偏移量: ${YELLOW}$slave_offset${NC}"
echo -e " 同步状态: $sync_status"
echo -e " 偏移量差异: $((master_offset - slave_offset))"
if [ "$sync_status" != "up" ]; then
log_warn "同步连接未建立,等待中..."
sleep $check_interval
wait_time=$((wait_time + check_interval))
continue
fi
# 检查偏移量差异
local offset_diff=$((master_offset - slave_offset))
if [ $offset_diff -lt 1000 ]; then
log_info "数据同步基本完成(偏移量差异: $offset_diff"
# 停止 Review 写入后再次确认
echo ""
log_warn "建议:在执行下一步前,先停止 Review 应用对 Redis 的写入"
if confirm_prompt "是否已停止写入并继续?"; then
sleep 2
master_offset=$(get_repl_offset "$NODE_132")
slave_offset=$(redis_cmd "$PROD_MASTER" $REDIS_PORT INFO replication 2>/dev/null | grep "slave_repl_offset:" | cut -d: -f2 | tr -d '\r\n')
offset_diff=$((master_offset - slave_offset))
if [ $offset_diff -eq 0 ]; then
log_info "数据完全同步(偏移量差异: 0"
return 0
else
log_info "当前偏移量差异: $offset_diff"
# 偏移量差异小于 100 字节时自动继续
if [ $offset_diff -lt 100 ] || confirm_prompt "偏移量差异 $offset_diff 字节,是否继续执行切换?"; then
return 0
fi
fi
fi
fi
echo "等待同步... ($wait_time/$max_wait 秒)"
sleep $check_interval
wait_time=$((wait_time + check_interval))
echo ""
done
log_error "同步超时,请检查网络和 Redis 状态"
if confirm_dangerous "是否强制继续?"; then
return 0
fi
return 1
}
# 步骤 3: 切换主从关系
rollback_step3_switch_master() {
echo ""
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} 步骤 3: 切换主从关系 ${NC}"
echo -e "${BLUE}========================================${NC}"
# 3.0 先将 132 设为只读,防止切换期间有新数据写入
log_info "将 132 设为只读模式,防止切换期间数据写入..."
redis_cmd "$NODE_132" $REDIS_PORT CONFIG SET min-replicas-to-write 99
log_info "132 已设为只读min-replicas-to-write=99"
# 等待最后的数据同步到 10.56
log_info "等待最后的数据同步..."
sleep 3
# 再次确认偏移量
local master_offset=$(get_repl_offset "$NODE_132")
local slave_offset=$(redis_cmd "$PROD_MASTER" $REDIS_PORT INFO replication 2>/dev/null | grep "slave_repl_offset:" | cut -d: -f2 | tr -d '\r\n')
local offset_diff=$((master_offset - slave_offset))
log_info "最终偏移量差异: $offset_diff"
# 偏移量差异小于 100 字节时自动继续,否则需要确认
if [ $offset_diff -gt 100 ]; then
log_warn "仍有 $offset_diff 字节数据未同步"
if ! confirm_dangerous "是否继续切换?"; then
# 恢复 132 可写
redis_cmd "$NODE_132" $REDIS_PORT CONFIG SET min-replicas-to-write 0
log_warn "已取消132 已恢复可写"
return 1
fi
elif [ $offset_diff -gt 0 ]; then
log_info "偏移量差异 $offset_diff 字节(<100自动继续"
fi
# 3.1 提升 10.56 为主节点
log_info "正在提升 $PROD_MASTER 为主节点..."
# 尝试直接远程执行
local result=$(redis_cmd "$PROD_MASTER" $PROD_PORT SLAVEOF NO ONE 2>/dev/null)
if [ "$result" = "OK" ]; then
log_info "远程执行成功: SLAVEOF NO ONE"
else
# 无法远程执行,提示手动操作
echo ""
echo -e "${YELLOW}========================================${NC}"
echo -e "${YELLOW} 无法远程执行,请在生产服务器 ($PROD_MASTER) 上执行: ${NC}"
echo -e "${YELLOW}========================================${NC}"
echo ""
echo "# 方法1: 进入 Redis 容器执行"
echo "docker exec -it \$(docker ps -q -f name=redis) redis-cli -a '$REDIS_PASSWORD' --no-auth-warning SLAVEOF NO ONE"
echo ""
echo "# 方法2: 使用临时容器执行"
echo "docker run --rm --network host $REDIS_IMAGE redis-cli -h $PROD_MASTER -p $PROD_PORT -a '$REDIS_PASSWORD' --no-auth-warning SLAVEOF NO ONE"
echo ""
if ! confirm_dangerous "已执行上述命令?"; then
# 恢复 132 可写
redis_cmd "$NODE_132" $REDIS_PORT CONFIG SET min-replicas-to-write 0
log_warn "已取消132 已恢复可写"
return 1
fi
fi
sleep 2
local prod_role=$(get_node_role "$PROD_MASTER")
if [ "$prod_role" = "master" ]; then
log_info "$PROD_MASTER 已提升为 Master"
else
log_error "提升失败,当前角色: $prod_role"
# 恢复 132 可写
redis_cmd "$NODE_132" $REDIS_PORT CONFIG SET min-replicas-to-write 0
return 1
fi
# 3.2 配置 132 指向生产(立即执行,减少时间窗口)
log_info "正在配置 $NODE_132 指向 $PROD_MASTER..."
redis_cmd "$NODE_132" $REDIS_PORT SLAVEOF "$PROD_MASTER" $PROD_PORT
# 恢复 132 可写设置(作为从节点后这个设置不影响,但保持配置干净)
redis_cmd "$NODE_132" $REDIS_PORT CONFIG SET min-replicas-to-write 0
# 恢复隐藏配置
log_info "恢复 132 的隐藏从节点配置..."
redis_cmd "$NODE_132" $REDIS_PORT CONFIG SET replica-announced no
redis_cmd "$NODE_132" $REDIS_PORT CONFIG SET replica-priority 0
sleep 2
local role_132=$(get_node_role "$NODE_132")
local sync_132=$(get_slave_sync_status "$NODE_132")
if [ "$role_132" = "slave" ] && [ "$sync_132" = "up" ]; then
log_info "$NODE_132 已配置为从节点,同步状态: $sync_132"
else
log_warn "$NODE_132 配置完成,角色: $role_132, 同步状态: $sync_132"
fi
# 3.3 配置 133/134 指向 132
log_info "正在配置从节点指向 $NODE_132..."
for node in $NODE_133 $NODE_134; do
redis_cmd "$node" $REDIS_PORT SLAVEOF "$NODE_132" $REDIS_PORT
sleep 1
local role=$(get_node_role "$node")
local sync=$(get_slave_sync_status "$node")
log_info "[$node] 角色: $role, 同步状态: $sync"
done
}
# 安全回滚主函数
safe_rollback() {
echo ""
echo -e "${RED}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${RED}║ 安全回滚流程 ║${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}║ 此流程会先将故障期间 132 的新数据同步到生产 10.56${NC}"
echo -e "${RED}║ 确保数据不丢失后再切换回原有架构。 ║${NC}"
echo -e "${RED}╚════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo "回滚流程说明:"
echo " 1. 让 10.56 先作为 132 的从节点(反向同步)"
echo " 2. 等待数据完全同步"
echo " 3. 提升 10.56 为主节点132 重新作为隐藏从节点"
echo " 4. 恢复 133/134 级联复制"
echo ""
echo "前置条件:"
echo " - 132 当前是 Master故障切换状态"
echo " - 10.56 已恢复并可连接"
echo -e " - ${YELLOW}【重要】所有应用已切换到 Review Sentinel${NC}"
echo -e " - ${YELLOW}【重要】生产 Sentinel 已停止${NC}"
echo " - 建议先停止 Review 应用的写入"
echo ""
echo -e "${YELLOW}========================================${NC}"
echo -e "${YELLOW} ⚠️ 生产 Sentinel 处理提醒 ${NC}"
echo -e "${YELLOW}========================================${NC}"
echo ""
echo "在执行回滚前,必须先停止生产 Sentinel"
echo ""
echo "原因:"
echo " 1. 回滚步骤1会让 10.56 变成 132 的从节点"
echo " 2. 如果 Sentinel 还在运行,会检测到 master 下线并触发故障转移"
echo " 3. 此外,任何仍通过生产 Sentinel 连接的应用会收到 READONLY 错误"
echo ""
echo -e "${RED}【重要】请确认以下条件:${NC}"
echo " 1. 所有应用都已切换到 Review Sentinel (reviewmaster)"
echo " 2. 生产 Sentinel 已停止"
echo ""
echo "停止生产 Sentinel 命令(在生产服务器执行):"
echo " docker stack rm prod_sentinel"
echo " # 或者根据实际部署名称调整"
echo ""
# 这是危险操作,必须确认(除非 --force
if ! confirm_dangerous "已确认所有应用已切换且生产 Sentinel 已停止?"; then
log_warn "请先完成上述操作后再执行回滚"
return 1
fi
check_prod_status || {
log_error "生产环境不可用,无法执行回滚"
return 1
}
check_status
if ! confirm_prompt "确认开始安全回滚?"; then
echo "已取消"
return 0
fi
# 移除 Review Sentinel
remove_sentinel
# 执行三个步骤
rollback_step1_make_prod_slave || return 1
rollback_step2_wait_sync || return 1
rollback_step3_switch_master || return 1
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN} 安全回滚完成 ${NC}"
echo -e "${GREEN}========================================${NC}"
check_status
echo ""
echo "后续操作:"
echo " 1. 重新启动生产 Sentinel如果之前停止了"
echo " 2. 验证生产 Sentinel 是否正常识别 10.56 为主节点"
echo " 3. 修改应用配置,连接回生产 Sentinel"
echo " 4. 确认 132/133/134 对生产 Sentinel 不可见"
}
# 旧的不安全回滚(保留但标记警告)
unsafe_rollback() {
echo ""
echo -e "${RED}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${RED}║ ⚠️ 警告 ⚠️ ║${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}║ 此操作会直接让 132 重新指向 10.56,可能导致数据丢失! ║${NC}"
echo -e "${RED}║ 故障期间 132 上的所有写入数据将被覆盖! ║${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}║ 推荐使用: ./failover-to-review.sh rollback ║${NC}"
echo -e "${RED}╚════════════════════════════════════════════════════════════╝${NC}"
echo ""
read -p "确认执行不安全回滚(数据可能丢失)? 输入 'YES' 确认: " confirm
if [ "$confirm" != "YES" ]; then
echo "已取消"
return 0
fi
log_warn "正在执行不安全回滚..."
# 先移除 Sentinel
remove_sentinel
# 重新配置 132 指向生产
log_info "配置 $NODE_132 指向生产 $PROD_MASTER..."
redis_cmd "$NODE_132" $REDIS_PORT SLAVEOF "$PROD_MASTER" $PROD_PORT
sleep 2
# 重新配置 133/134 指向 132
for node in $NODE_133 $NODE_134; do
log_info "配置 $node 指向 $NODE_132..."
redis_cmd "$node" $REDIS_PORT SLAVEOF "$NODE_132" $REDIS_PORT
sleep 1
done
log_warn "不安全回滚完成,请检查数据一致性"
}
# 显示最终状态和连接信息
show_final_info() {
echo ""
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} 切换完成 ${NC}"
echo -e "${BLUE}========================================${NC}"
check_status
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN} Review 应用连接配置 ${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo "直连模式:"
echo " host: $NODE_132"
echo " port: $REDIS_PORT"
echo " password: $REDIS_PASSWORD"
echo ""
echo "Sentinel 模式 (Spring Boot):"
echo " spring:"
echo " redis:"
echo " sentinel:"
echo " master: reviewmaster"
echo " nodes: $NODE_132:$SENTINEL_PORT,$NODE_133:$SENTINEL_PORT,$NODE_134:$SENTINEL_PORT"
echo " password: $SENTINEL_PASSWORD"
echo " password: $REDIS_PASSWORD"
echo ""
}
# ==================== 主函数 ====================
main() {
# 解析全局参数
parse_global_args "$@"
echo -e "${GREEN}"
echo "╔════════════════════════════════════════════╗"
echo "║ Review Redis 故障切换脚本 ║"
echo "╚════════════════════════════════════════════╝"
echo -e "${NC}"
if [ "$AUTO_CONFIRM" = true ]; then
log_info "已启用自动确认模式 (-y)"
fi
if [ "$FORCE_MODE" = true ]; then
log_warn "已启用强制模式 (--force),将跳过所有确认"
fi
# 对于需要执行 Redis 命令的操作,检查镜像是否存在
case "${1:-help}" in
status|failover|full|rollback|unsafe-rollback)
check_redis_image || exit 1
;;
esac
case "${1:-help}" in
status)
check_prod_status || true
check_status
;;
failover)
check_status
echo ""
if confirm_prompt "确认执行故障切换(提升 132 为 Master?"; then
promote_132
configure_slaves
check_status
else
echo "已取消"
fi
;;
sentinel)
deploy_sentinel
;;
full)
check_prod_status || true
check_status
echo ""
if confirm_prompt "确认执行完整故障切换(包含 Sentinel 部署)?"; then
promote_132
configure_slaves
deploy_sentinel
show_final_info
else
echo "已取消"
fi
;;
rollback)
safe_rollback
;;
unsafe-rollback)
unsafe_rollback
check_status
;;
help|*)
echo "用法: $0 {status|failover|sentinel|full|rollback|unsafe-rollback} [-y|--yes] [--force]"
echo ""
echo "命令说明:"
echo " status - 查看当前 Redis 状态"
echo " failover - 执行故障切换(提升 132 为 Master"
echo " sentinel - 仅部署 Review Sentinel"
echo " full - 完整切换failover + sentinel"
echo " rollback - 安全回滚(先同步数据到生产,再切换)"
echo " unsafe-rollback - 不安全回滚(直接切换,可能丢数据)"
echo ""
echo "可选参数:"
echo " -y, --yes - 自动确认普通提示(危险操作仍需手动确认)"
echo " --force - 强制模式,跳过所有确认(慎用!)"
echo ""
echo "示例:"
echo " $0 full -y # 自动确认执行完整切换"
echo " $0 rollback # 交互式安全回滚"
echo " $0 rollback --force # 强制执行回滚(跳过所有确认)"
echo ""
echo "故障切换流程:"
echo " 1. 执行 ./failover-to-review.sh status 检查状态"
echo " 2. 执行 ./failover-to-review.sh full 完整切换"
echo " 3. 修改应用配置,连接 Review Sentinel"
echo ""
echo "安全恢复流程(推荐):"
echo " 1. 确认生产环境 Redis 已恢复"
echo " 2. 【重要】确认所有应用已切换到 Review Sentinel"
echo " 3. 【重要】停止生产 Sentinel: docker stack rm prod_sentinel"
echo " 4. 建议先停止 Review 应用对 Redis 的写入"
echo " 5. 执行 ./failover-to-review.sh rollback"
echo " - 步骤1: 让 10.56 作为 132 的从节点(反向同步)"
echo " - 步骤2: 等待数据完全同步"
echo " - 步骤3: 将 132 设为只读,提升 10.56 为主节点,恢复原架构"
echo " 6. 重新启动生产 Sentinel"
echo " 7. 修改应用配置,连接生产 Sentinel"
echo ""
echo "注意:"
echo " - 所有 Redis 操作通过 Docker 容器执行,需要 $REDIS_IMAGE 镜像"
echo " - 如果能连接生产 Redis命令会自动远程执行"
echo " - 如果无法连接,会提示手动在生产服务器执行"
echo " - 如果 review_redis_master 容器运行中,优先使用该容器"
echo " - 否则使用临时容器执行命令"
;;
esac
}
main "$@"