备份最怕两件事:一是全堆在本地,哪天磁盘爆了或者机器挂了直接全丢;二是每天勤勤恳恳备份,等真出故障要恢复时,才发现压缩包早就是损坏的空壳。

我在维护几台生产服务器时采用的原则很简单:本地零留存(即焚)+ 专用密钥推送到异地温备 + 异地保留最新 N 份 + 定期跑解包演练。

这样生产机不会被历史备份把磁盘撑满,异地也有可靠的副本随时能顶上。下面把这套脚本架构和踩过的坑完整拆开聊聊。


灾备核心原则:为什么坚持本地即焚

一套健康的生产级异地备份架构必须满足以下四项核心准则:

  1. 本地零留存(即焚哲学):生产服务器硬盘寸土寸金,备份压缩包在本地生成并校验完毕后,一旦确认远端接收成功,本地瞬态文件立即物理粉碎,杜绝垃圾膨胀;
  2. 免密安全传输通道:配置专用的独立 SSH 密钥对,限制执行权限,杜绝将服务器 root 主私钥到处分发的危险行径;
  3. 保留最新 N 份智能滚动轮转:在异地温备机端自动维护版本链条(如保留最新 7 份),旧版本自动淘汰清理,维持远端磁盘水位平稳;
  4. 闭环恢复演练(未经验证的备份等于没有备份):定时自动模拟从远端拉回备份包,执行解包与哈希比对测试,确保灾难降临时 100% 具备回滚自愈能力。

规划备份目录与核心变量

在敲击命令前,先集中定义生产端与异地温备端的核心变量:

# ================= 全局参数定义 =================
BACKUP_NAME="prod-core-backup"      # 备份业务标识前缀
SRC_DIRS="/opt/hermes /etc/nginx"   # 需要打包备份的关键业务目录 (空格分隔)
REMOTE_USER="backup_user"          # 异地温备机的接收用户名
REMOTE_HOST="standby.example.com"  # 异地温备机公网 IP 或域名
REMOTE_PORT="22"                   # 异地温备机 SSH 端口
REMOTE_DIR="/data/backups/prod"    # 异地温备机落盘存储目录
KEEP_COPIES="7"                    # 异地保留的最新历史版本数量
# ================================================

生产端打包与 SHA256 完整性指纹

备份切忌“全盘无脑打包”,系统临时文件(/proc、/sys、/tmp、Docker 镜像层等)不仅体积庞大,而且恢复时毫无意义。我们要精准圈定配置与数据持久化目录。

1. 编写打包与校验指令

采用 tar 配合 gzip 或多线程 pigz 压缩,并剔除无用的 .cache 与日志:

🖥️ 【生产服务器】

# 创建临时瞬态工作区
TMP_DIR="/tmp/backup_staging"
mkdir -p ${TMP_DIR}
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
ARCHIVE_FILE="${TMP_DIR}/${BACKUP_NAME}_${TIMESTAMP}.tar.gz"

# 执行打包,排除无用缓存与临时日志
tar -czf ${ARCHIVE_FILE} \
    --exclude='*.log' \
    --exclude='*cache*' \
    --exclude='*/node_modules/*' \
    -C / opt/hermes etc/nginx

# 计算并生成 SHA256 校验指纹文件
sha256sum ${ARCHIVE_FILE} > ${ARCHIVE_FILE}.sha256

# 查看生成的归档与指纹
ls -lh ${ARCHIVE_FILE}*

配置异地传输专用的受限 SSH 密钥

绝不要在备份脚本里硬编码 SSH 密码!我们通过专用密钥对建立互信通道。

1. 在生产端生成专用备份密钥

🖥️ 【生产服务器】

# 生成无密码保护的专用备份密钥对
ssh-keygen -t ed25519 -N "" -C "backup-transport@production" -f ~/.ssh/id_ed25519_backup

2. 将公钥授权至异地温备端

将公钥部署到异地机,并在 authorized_keys 中加入安全限制,确保该密钥仅能用于文件传输,不可获取交互式 shell:

🖥️ 【异地温备机】

# 确保温备机备份目录就位
sudo mkdir -p /data/backups/prod
sudo chown -R backup_user:backup_user /data/backups/prod

# 在 backup_user 的 authorized_keys 中配置受限公钥
cat << 'EOF' >> ~/.ssh/authorized_keys
# 限制此密钥仅用于 rsync/scp
command="rsync --server -vlogDtprze.iLsfxCIvu . /data/backups/prod",no-pty,no-agent-forwarding,no-X11-forwarding ssh-ed25519 AAAAC3...你的生产端备份公钥... backup-transport@production
EOF

异地推送与本地临时文件清理

使用 rsync 或 scp 将归档包与校验和文件推送到远端,并在确认传输成功后立即物理销毁本地临时文件。

🖥️ 【生产服务器】

# 执行安全断点续传
rsync -avz -e "ssh -i ~/.ssh/id_ed25519_backup -p ${REMOTE_PORT}" \
    ${ARCHIVE_FILE}* \
    ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/

# 检查传输返回值,如果成功则即刻销毁本地瞬态文件
if [ $? -eq 0 ]; then
    echo "✅ 异地传输成功,执行本地即焚清理..."
    rm -rf ${ARCHIVE_FILE}*
else
    echo "❌ 异地传输失败,保留本地包待排查!"
    exit 1
fi

异地保留最新 N 份滚动清理

如果长期只推不删,异地机的硬盘迟早被历史版本占满。我们需要一套自动淘汰旧版本的优雅机制。

在异地温备端,或在生产端通过 SSH 远程触发清理命令,只保留最新的 ${KEEP_COPIES} 份文件:

🖥️ 【异地温备机】

# 智能保留最新 7 份备份,将其余旧文件安全清除
cd /data/backups/prod
ls -t ${BACKUP_NAME}_*.tar.gz | sed -e "1,${KEEP_COPIES}d" | while read -r old_file; do
    echo "清理过期归档: ${old_file}"
    rm -f "${old_file}" "${old_file}.sha256"
done

自动化解包与恢复演练

“未经验证的备份只是安慰剂。”
无数事故表明,许多人辛辛苦苦备份了几个月,等到真出故障要恢复时,才发现压缩包在传输中丢包损坏、或者解包后目录结构完全错乱。

我们编写一段轻量自动化演练脚本,每月或每周由机器自动从异地机抽取最新一份归档,在独立沙箱目录进行解包校验:

🖥️ 【生产服务器(或独立演练测试机)】

DRILL_DIR="/tmp/recovery_drill"
rm -rf ${DRILL_DIR} && mkdir -p ${DRILL_DIR}

echo "=== 1. 从异地机拉取最新一份备份及其哈希指纹 ==="
LATEST_BACKUP=$(ssh -i ~/.ssh/id_ed25519_backup -p ${REMOTE_PORT} ${REMOTE_USER}@${REMOTE_HOST} "ls -t ${REMOTE_DIR}/${BACKUP_NAME}_*.tar.gz | head -n 1")

rsync -avz -e "ssh -i ~/.ssh/id_ed25519_backup -p ${REMOTE_PORT}" \
    ${REMOTE_USER}@${REMOTE_HOST}:${LATEST_BACKUP}* \
    ${DRILL_DIR}/

cd ${DRILL_DIR}
ARCHIVE_BASE=$(basename ${LATEST_BACKUP})

echo "=== 2. 执行 SHA256 完整性校验 ==="
sha256sum -c "${ARCHIVE_BASE}.sha256"
if [ $? -ne 0 ]; then
    echo "🚨 致命错误:最新备份文件哈希校验失败,归档已损坏!"
    exit 2
fi

echo "=== 3. 模拟沙箱解包演练 ==="
mkdir -p unpack_test
tar -xzf "${ARCHIVE_BASE}" -C unpack_test/

# 检查核心关键目录是否存在
if [ -d "unpack_test/opt/hermes" ] && [ -d "unpack_test/etc/nginx" ]; then
    echo "🎉 完美验证:关键业务数据与配置完整可恢复!"
else
    echo "🚨 演练警告:解包后缺少必要核心目录!"
    exit 3
fi

# 演练完毕清理沙箱
rm -rf ${DRILL_DIR}

整合脚本与挂载 Linux Crontab

将上述流程封装为标准 Shell 脚本 /opt/scripts/offsite_backup.sh,赋予可执行权限:

sudo chmod +x /opt/scripts/offsite_backup.sh

写入系统定时任务,每天凌晨 03:00 业务低峰期静默执行:

🖥️ 【生产服务器】

(crontab -l 2>/dev/null; echo "0 3 * * * /opt/scripts/offsite_backup.sh >> /var/log/offsite_backup.log 2>&1") | crontab -

现场测试与版本核验

1. 现场干跑一次完整备份流

🖥️ 【生产服务器】

bash /opt/scripts/offsite_backup.sh

核验标准:终端输出传输完成、本地临时文件清空,异地端准确接收 .tar.gz 与 .sha256。

2. 查看异地温备机版本堆叠

🖥️ 【异地温备机】

ls -lh /data/backups/prod/

核验标准:列出的备份文件按时间戳整齐排列,版本数量严格受限于预设的阈值(如 7 份)。


常见报错与处理方案

Q1:生产端打包时提示 file changed as we read it 导致退出码为 1?

  • 根因:有正在运行的进程在写入日志或数据库文件。
  • 自愈方案:
    1. 使用 --exclude='*.log' 过滤高频动态日志;
    2. 对于 MySQL/Postgres 等数据库,不要直接 tar 复制正在运行的 data 目录,必须在打包前通过 mysqldump 或 pg_dump 导出平稳的 .sql 文本文件再行归档。

Q2:异地传输时因网络抖动中断,留下半个残缺文件怎么办?

  • 自愈方案:在 rsync 命令中加入 --partial 参数,支持断点续传;同时远端必须优先比对 .sha256 校验指纹,指纹不符的文件坚决不归入有效版本库。