本文目录导读:

在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
最佳实践建议
-
保持简单:回滚逻辑不要过于复杂,清晰的 if-else 或 case 语句比魔法函数更可靠。
-
幂等性:回滚操作应该是幂等的,多次执行不会造成额外损害。
-
日志记录:记录每一步的操作,方便调试。
-
测试回滚:在测试环境中先验证回滚逻辑。
-
超时处理:长时间运行的步骤应该设置超时,避免回滚也被阻塞。
-
互斥锁:避免多个部署同时进行导致的混乱。
选择哪种实现方式取决于你的具体需求:简单场景用第一种,需要事务性保证的用第三种,需要版本管理的用第四种。