Shell脚本如何实现回滚策略

wen 实用脚本 25

本文目录导读:

Shell脚本如何实现回滚策略

  1. 基于函数和状态标记的简单回滚
  2. 使用 trap 捕获异常
  3. 事务性回滚(保存和恢复)
  4. 带版本的增量回滚
  5. 最佳实践建议

在Shell脚本中实现回滚策略,核心思路是在执行每个关键步骤前,保存当前状态或可恢复信息,一旦后续步骤失败,就调用预先定义的恢复函数

主要有以下几种实现方式:

基于函数和状态标记的简单回滚

这是最基础也最实用的方法,适合大多数部署场景。

#!/bin/bash
# 定义颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
# 记录已执行的步骤
executed_steps=()
# 定义回滚函数
rollback() {
    local step=$1
    echo -e "${RED}[ROLLBACK] 开始回滚步骤 $step...${NC}"
    case $step in
        1)
            echo "回滚步骤1: 删除备份文件"
            rm -f /tmp/backup.tar.gz
            ;;
        2)
            echo "回滚步骤2: 恢复原文件"
            mv /tmp/original_bak.conf /etc/app/config.conf 2>/dev/null
            ;;
        3)
            echo "回滚步骤3: 停止新服务,启动旧服务"
            systemctl stop app-new 2>/dev/null
            systemctl start app-old 2>/dev/null
            ;;
        *)
            echo "未知步骤: $step"
            ;;
    esac
}
# 执行步骤并检查
execute_step() {
    local step=$1
    local desc=$2
    shift 2
    echo -e "${GREEN}[STEP $step] $desc${NC}"
    if "$@"; then
        executed_steps+=($step)
        echo "步骤 $step 成功"
    else
        echo -e "${RED}[ERROR] 步骤 $step 失败${NC}"
        # 倒序回滚已执行成功的步骤
        for ((i=${#executed_steps[@]}-1; i>=0; i--)); do
            rollback ${executed_steps[$i]}
        done
        exit 1
    fi
}
# 主流程
main() {
    # 步骤1: 备份原文件
    execute_step 1 "备份配置文件" cp /etc/app/config.conf /tmp/original_bak.conf
    # 步骤2: 部署新配置
    execute_step 2 "部署新配置" cp /tmp/new_config.conf /etc/app/config.conf
    # 步骤3: 重启服务
    execute_step 3 "重启服务" systemctl restart app
    echo -e "${GREEN}[SUCCESS] 所有步骤完成${NC}"
}
main

使用 trap 捕获异常

利用 trap 在脚本异常退出时自动执行回滚。

#!/bin/bash
# 临时文件列表
temp_files=()
# 清理函数
cleanup() {
    echo "[CLEANUP] 开始清理..."
    for file in "${temp_files[@]}"; do
        if [ -f "$file" ]; then
            echo "删除临时文件: $file"
            rm -f "$file"
        fi
    done
    # 如果有需要恢复的服务状态
    if [ -n "$OLD_SERVICE_STATUS" ]; then
        echo "恢复服务状态..."
        systemctl $OLD_SERVICE_STATUS app 2>/dev/null
    fi
    echo "清理完成"
    exit 1
}
# 设置退出时的清理
trap cleanup EXIT ERR INT TERM
# 主逻辑
echo "开始部署..."
# 记录原服务状态
OLD_SERVICE_STATUS=$(systemctl is-active app 2>/dev/null || echo "unknown")
echo "原服务状态: $OLD_SERVICE_STATUS"
# 步骤1: 创建临时备份
temp_files+=("/tmp/app_backup/")
mkdir -p /tmp/app_backup
cp -r /usr/local/app/* /tmp/app_backup/ 2>/dev/null
# 步骤2: 部署新版本
cp -r /tmp/newapp/* /usr/local/app/ || exit 1
# 步骤3: 重启服务
systemctl restart app || exit 1
echo "部署成功!"
# 解除 trap,正常退出
trap - EXIT ERR INT TERM

事务性回滚(保存和恢复)

适用于需要保存多个文件状态的场景。

#!/bin/bash
# 事务日志文件
TRANSACTION_LOG="/tmp/deploy_transaction.log"
STATE_SAVE_DIR="/tmp/deploy_state_$(date +%s)"
# 初始化事务日志
init_transaction() {
    mkdir -p "$STATE_SAVE_DIR"
    > "$TRANSACTION_LOG"
    echo "事务初始化: $STATE_SAVE_DIR"
}
# 记录备份
backup_file() {
    local src=$1
    local backup_path="$STATE_SAVE_DIR/$(echo "$src" | tr '/' '_')"
    if [ -f "$src" ]; then
        cp "$src" "$backup_path"
        echo "backup:$src:$backup_path" >> "$TRANSACTION_LOG"
        echo "备份: $src -> $backup_path"
    fi
}
# 记录目录状态
backup_dir_state() {
    local dir=$1
    local state_file="$STATE_SAVE_DIR/dir_$(echo "$dir" | tr '/' '_').state"
    if [ -d "$dir" ]; then
        find "$dir" -type f > "$state_file"
        echo "dir_state:$dir:$state_file" >> "$TRANSACTION_LOG"
        echo "记录目录状态: $dir"
    fi
}
# 完整回滚
full_rollback() {
    echo "[ROLLBACK] 开始回滚..."
    if [ ! -f "$TRANSACTION_LOG" ]; then
        echo "没有找到事务日志,无法回滚"
        return 1
    }
    # 从日志读取并执行回滚(逆序)
    while IFS=':' read -r action src dest; do
        case $action in
            backup)
                if [ -f "$dest" ]; then
                    cp "$dest" "$src"
                    echo "恢复: $dest -> $src"
                else
                    echo "警告: 备份文件不存在: $dest"
                fi
                ;;
            restart_service)
                systemctl start "$src" 2>/dev/null
                echo "启动服务: $src"
                ;;
        esac
    done < <(tac "$TRANSACTION_LOG")
    # 清理临时文件
    rm -rf "$STATE_SAVE_DIR" "$TRANSACTION_LOG"
    echo "回滚完成"
}
# 使用示例
deploy_with_rollback() {
    # 当脚本退出时自动回滚
    trap full_rollback EXIT
    init_transaction
    echo "=== 开始部署 ==="
    # 步骤1: 备份配置文件
    backup_file "/etc/app/config.conf"
    backup_file "/etc/app/database.yml"
    # 步骤2: 备份目录状态
    backup_dir_state "/usr/local/app/static"
    # 步骤3: 部署新文件
    cp /tmp/new_config.conf /etc/app/config.conf || exit 1
    # 步骤4: 重启服务
    systemctl stop app
    backup_before_restart "app"
    systemctl start app || {
        echo "服务启动失败"
        exit 1
    }
    # 部署成功,取消 trap
    trap - EXIT
    rm -rf "$STATE_SAVE_DIR" "$TRANSACTION_LOG"
    echo "=== 部署成功 ==="
}
# 执行部署
deploy_with_rollback

带版本的增量回滚

适合需要回滚到特定版本的场景。

#!/bin/bash
VERSION_DIR="/opt/app/versions"
CURRENT_LINK="/opt/app/current"
ROLLBACK_MARKER="/tmp/rollback_marker"
# 创建新版本部署
deploy_version() {
    local version=$1
    local deploy_dir="$VERSION_DIR/$version"
    echo "部署版本: $version"
    # 创建版本目录
    mkdir -p "$deploy_dir"
    # 复制新版本文件
    cp -r /tmp/app_package/* "$deploy_dir/" || exit 1
    # 备份当前版本
    if [ -L "$CURRENT_LINK" ]; then
        local old_target=$(readlink "$CURRENT_LINK")
        if [ -n "$old_target" ]; then
            echo "当前版本: $(basename $old_target)"
            # 创建回滚点
            echo "$old_target" >> "$ROLLBACK_MARKER"
        fi
    fi
    # 更新符号链接
    ln -sfn "$deploy_dir" "$CURRENT_LINK"
    echo "版本 $version 部署完成"
}
# 回滚到上一版本
rollback_to_previous() {
    if [ ! -f "$ROLLBACK_MARKER" ]; then
        echo "没有可回滚的版本"
        return 1
    fi
    local previous_version=$(tail -1 "$ROLLBACK_MARKER")
    sed -i '$d' "$ROLLBACK_MARKER"  # 删除最后一行
    if [ -d "$previous_version" ]; then
        echo "回滚到版本: $(basename $previous_version)"
        ln -sfn "$previous_version" "$CURRENT_LINK"
        systemctl restart app
        echo "回滚完成"
    else
        echo "错误: 版本目录不存在: $previous_version"
        return 1
    fi
}
# 列出版本历史
list_versions() {
    echo "可用的版本:"
    ls -1 "$VERSION_DIR" 2>/dev/null || echo "  没有版本"
    echo -e "\n回滚历史:"
    if [ -f "$ROLLBACK_MARKER" ]; then
        cat "$ROLLBACK_MARKER"
    fi
}
# 使用示例
case "${1:-}" in
    deploy)
        deploy_version "${2:-v$(date +%Y%m%d%H%M%S)}"
        ;;
    rollback)
        rollback_to_previous
        ;;
    status)
        list_versions
        ;;
    *)
        echo "用法: $0 {deploy|rollback|status} [version]"
        exit 1
        ;;
esac

最佳实践建议

  1. 保持简单:回滚逻辑不要过于复杂,清晰的 if-else 或 case 语句比魔法函数更可靠。

  2. 幂等性:回滚操作应该是幂等的,多次执行不会造成额外损害。

  3. 日志记录:记录每一步的操作,方便调试。

  4. 测试回滚:在测试环境中先验证回滚逻辑。

  5. 超时处理:长时间运行的步骤应该设置超时,避免回滚也被阻塞。

  6. 互斥锁:避免多个部署同时进行导致的混乱。

选择哪种实现方式取决于你的具体需求:简单场景用第一种,需要事务性保证的用第三种,需要版本管理的用第四种。

抱歉,评论功能暂时关闭!