服务器如何自动备份指定目录?增量备份、压缩与历史版本管理

备份这件事,几乎所有运维都知道要做,但真正做到"出事时能恢复"的却不多。常见的失败模式有三种:只做了全量备份、备份文件堆了一磁盘却从没验证过、需要某一周前的版本时发现早被覆盖了。备份的价值不在于"每天都跑",而在于"需要某个历史版本时,它还在且能用"。

这篇文章讲怎么用 rsync 和 tar 配合,对指定目录做自动化备份,重点是三个能力:增量(只传变化的,不重复搬全量)、压缩(省空间)、历史版本(能回到过去的某一天)。会给出可直接运行的脚本、定时任务配置、以及恢复演练方法。

文中的保留份数、增量策略、磁盘占用估算都是经验值,请按数据量和可承受的恢复时间(也就是 RPO/RTO)调整。涉及 --delete 这类会删除目标端文件的参数,我会单独说明风险和规避方式,这类参数用错会导致备份目录被清空。

一、先想清楚备份的三个问题

在敲命令之前,先把策略想明白。一个能用的备份方案要回答三个问题:备份什么、保留多久、怎么恢复。

备份什么:不是越多越好。要备份的是"丢了会痛"的东西——应用代码与配置、用户上传的数据、数据库导出文件、证书和密钥。操作系统本身、缓存目录、日志(如果不作审计用)通常不需要备,重装系统比恢复更快。把 / 整个备下来既慢又没意义。

保留多久:取决于业务和合规要求。个人项目保留 7 天可能就够;有合规要求的业务可能需要 30 天、90 天甚至更长。保留策略的核心是"平衡空间和可恢复的时间点",常见做法是保留最近 7 天的每日版本加最近 4 周的每周版本。

怎么恢复:这是最容易被忽略的一环。没演练过的备份等于没有备份。至少要定期做一次"从备份恢复到一个临时目录"的演练,确认备份文件完整、可解压、内容正确。

备份方式 空间占用 备份速度 恢复速度 适用
全量 tar 压缩 高(每次一份完整) 慢 快 数据量小、要求简单
rsync 增量同步 低(只存变化,硬链接复用) 快 快(目录即恢复) 数据量大、要历史版本
快照(LVM/云盘) 中 极快 快 有快照能力、要快速回滚
数据库逻辑导出 中 中 中 数据库本身的备份
异地/对象存储同步 中 中(受带宽限制) 中 容灾,防整机故障

实践中常见组合是:本地用 rsync 增量做每日版本,再定期把增量结果同步到异地或对象存储。本地负责"快速恢复",异地负责"容灾"。

二、rsync 增量备份的原理与参数

rsync 是增量备份的主力。它的"增量"体现在传输层面:默认只传输源和目标差异的部分。但要注意,单纯的 rsync 同步得到的是"当前状态",不是历史版本。要做历史版本,靠的是 --link-dest 参数配合硬链接。

原理是:每天的备份目录里,和前一天完全相同的文件用硬链接指向同一份数据,只有变化的文件才真正占用新空间。这样每个日期目录看起来都是一份"完整备份",但实际磁盘占用只比一份全量多一点。这是 rsync 做版本化备份的精髓。

关键参数:

参数 作用 注意
-a 归档模式(等同 -rlptgoD) 保留权限、时间、属主等
-v 显示详细过程 排查用
-z 传输压缩 慢网络下有用
--delete 删除目标端多余文件 危险,慎用,见下文
--link-dest=DIR 相同文件用硬链接指向 DIR 历史版本的关键
--exclude 排除文件/目录 如排除 cache、tmp
--exclude-from 从文件读排除列表 排除项多时好用
--bwlimit 限速(KB/s) 避免占满带宽
-e ssh 通过 SSH 传输 远程备份必用
--numeric-ids 按数字 ID 保留属主 跨机备份避免用户映射错乱

关于 --delete 要重点说明:它让目标端与源端完全一致,会删掉目标端源端没有的文件。这本身是"镜像备份"的常用行为,但如果源目录被误删,--delete 会把备份端的对应文件也删掉,等于"备份跟着一起丢"。所以备份场景里 --delete 要非常谨慎:要么不用,要么务必确认源目录不会出现"整体消失"的情况,并且备份端有独立的保留策略。

三、完整增量备份脚本

下面这份脚本用 --link-dest 实现"每日一个完整快照、共享未变数据"的增量备份,并自动清理过期版本。

#!/usr/bin/env bash
# 目录增量备份脚本(rsync + link-dest 历史版本)
# 用法:bash backup_dir.sh
set -uo pipefail

# ---- 配置区(经验值,按需调整)----
SRC="/data/www"                  # 要备份的源目录
DEST="/backup/www"               # 备份根目录(建议挂独立盘)
KEEP=7                           # 保留最近 7 个每日版本(经验值)
LOG="/var/log/backup_www.log"
EXCLUDE_FILE="/etc/backup_exclude.txt"

# 排除规则:缓存、临时、会话等无需备份的内容
cat > "$EXCLUDE_FILE" <<'EOF'
cache/
tmp/
*.sock
*.pid
node_modules/
.git/
EOF

TODAY=$(date +%F)
DEST_TODAY="$DEST/$TODAY"
# 前一天的版本目录,用于 link-dest(共享未变文件)
PREV=$(ls -1d "$DEST"/????-??-?? 2>/dev/null | sort | tail -1)

log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG"; }

mkdir -p "$DEST"

log "==== 开始备份 $SRC -> $DEST_TODAY ===="

# 组装 link-dest 参数
LINK_OPT=""
[ -n "$PREV" ] && LINK_OPT="--link-dest=$PREV"
log "基准版本: ${PREV:-无(首次全量)}"

# 执行备份:先写临时目录,成功后原子改名,避免半个备份被当成完整版本
TMP="$DEST/.incomplete-$TODAY"
rm -rf "$TMP"
rsync -a --delete \
      --exclude-from="$EXCLUDE_FILE" \
      $LINK_OPT \
      "$SRC"/ "$TMP"/

if [ $? -eq 0 ]; then
  rm -rf "$DEST_TODAY"
  mv "$TMP" "$DEST_TODAY"
  log "[OK] 备份完成: $DEST_TODAY"
  # 记录本次占用(硬链接共享后,实际增量很小)
  log "本次总占用: $(du -sh "$DEST_TODAY" 2>/dev/null | awk '{print $1}')"
else
  log "[FAIL] rsync 失败,保留 $TMP 供排查"
  exit 1
fi

# 清理过期版本
COUNT=$(ls -1d "$DEST"/????-??-?? 2>/dev/null | wc -l)
if [ "$COUNT" -gt "$KEEP" ]; then
  DEL=$((COUNT - KEEP))
  ls -1d "$DEST"/????-??-?? | sort | head -n "$DEL" | while read -r old; do
    log "清理过期版本: $old"
    rm -rf "$old"
  done
fi

# 汇总
log "当前保留版本数: $(ls -1d "$DEST"/????-??-?? | wc -l)  总占用: $(du -sh "$DEST" | awk '{print $1}')"
log "==== 备份结束 ===="

几个设计点值得强调。先写临时目录再原子改名:如果备份过程中断,DEST 里不会出现一个"看似完整其实残缺"的日期目录——残缺的会留在 .incomplete- 前缀下,不影响正常版本。--link-dest 指向前一天:未变化的文件共享 inode,变化的文件才占新空间,所以保留 7 天不等于占 7 倍空间。排除规则独立成文件:便于维护和统一管理。

四、tar 压缩归档方案

如果更看重"把整份打包成一个可搬运的文件",tar 更合适。它适合数据量不大、或需要把归档传去异地的场景。

#!/usr/bin/env bash
# tar 压缩归档备份
set -uo pipefail
SRC="/data/www"
DEST="/backup/archives"
KEEP=7
TS=$(date +%F)

mkdir -p "$DEST"

# 压缩打包(排除缓存等)
tar -czf "$DEST/www-$TS.tar.gz" \
    --exclude='cache' --exclude='tmp' --exclude='*.sock' \
    -C "$(dirname "$SRC")" "$(basename "$SRC")"

# 校验归档完整性(重要!)
if tar -tzf "$DEST/www-$TS.tar.gz" >/dev/null 2>&1; then
  echo "[OK] 归档校验通过: www-$TS.tar.gz"
else
  echo "[FAIL] 归档损坏,删除"; rm -f "$DEST/www-$TS.tar.gz"; exit 1
fi

# 清理旧归档
ls -1t "$DEST"/www-*.tar.gz | tail -n +$((KEEP + 1)) | xargs -r rm -f

tar 的压缩选项要选对:

选项 算法 速度 压缩率 建议
-z gzip 中 中 通用,兼容性最好
-j bzip2 慢 较高 归档不常解压时用
-J xz 很慢 最高 长期归档、空间紧张
--zstd zstd 快 高 需较新 tar,兼顾速度与压缩率
不加 仅打包 最快 无 已压缩的文件(图片/视频)不必再压

一个经验判断:文本类数据(代码、日志、配置)压缩率可达 5:1 到 10:1;已经压缩过的文件(jpg、zip、视频)再压几乎没收益,反而白耗 CPU。所以对图片、视频目录,打包不压缩(不加 -z)可能更合理。

五、恢复演练与验证

备份不验证等于没有备份。至少要做两件事:每次备份后校验归档能否正常列出/解压,以及定期从备份恢复一份到临时目录做比对。

# 1) 校验归档完整性(tar)
tar -tzf /backup/archives/www-2027-01-01.tar.gz >/dev/null && echo "归档OK"

# 2) 用 rsync 校验:dry-run 对比备份与源,看差异
rsync -avn --delete /data/www/ /backup/www/2027-01-01/ | tail -20
# -n 是 dry-run,只列出会做的动作,不实际执行

# 3) 真正恢复演练:解到临时目录
mkdir -p /tmp/restore_test
tar -xzf /backup/archives/www-2027-01-01.tar.gz -C /tmp/restore_test/
diff -r /tmp/restore_test/www /data/www | head   # 对比差异

# 4) 从 rsync 版本恢复:直接 rsync 回去即可
rsync -a --delete /backup/www/2027-01-01/ /data/www/

第 4 条命令是恢复的核心,但要注意它带 --delete——它会把 /data/www 里当前存在、而备份里没有的文件删掉,让目录完全回到备份时的状态。如果只想"把备份里多出来的文件补回去、不删现在的文件",就去掉 --delete。恢复比备份更需要谨慎,因为操作对象是生产数据。恢复前务必先对现状再做一次备份,给自己留后路。

六、定时任务与异地同步

# 每日 02:00 做增量备份,02:30 同步到异地(建议错开时间,避免同抢 IO)
sudo tee /etc/cron.d/backup_dir >/dev/null <<'EOF'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 2 * * * root /usr/local/bin/backup_dir.sh >> /var/log/backup_cron.out 2>&1
30 3 * * * root rsync -az --bwlimit=50000 /backup/www/ backupuser@remote:/backup/www/ >> /var/log/backup_cron.out 2>&1
EOF
sudo chmod 644 /etc/cron.d/backup_dir

异地同步用 --bwlimit 限速(单位 KB/s)很有必要,否则备份流量可能把正常业务带宽挤占掉。50000 约等于 50MB/s,是经验值,请按你实际带宽调整。跨机同步建议用 SSH 密钥认证并限制该密钥只能执行 rsync(在目标端 authorized_keys 里加 command= 限制),避免密钥泄露后被当成登录凭证。

七、风险提示与回滚

第一,--delete 是双刃剑。 它保证备份与源一致,但源端误删会传导到备份端。规避办法有三:一是在备份脚本里对源目录做存在性检查(空目录或不存在就别跑,直接告警);二是备份端保留多天版本而不是单一镜像;三是关键数据做两份独立策略。

# 在脚本开头加防护:源目录不存在或为空则中止
if [ ! -d "$SRC" ] || [ -z "$(ls -A "$SRC" 2>/dev/null)" ]; then
  echo "[ABORT] 源目录 $SRC 不存在或为空,疑似异常,终止备份"; exit 1
fi

第二,别把备份放在同一块盘。 如果备份目录和源数据在同一块物理盘上,盘坏了两个一起没,等于没备份。至少放到独立磁盘或独立挂载点,条件允许再同步一份到异地。"3-2-1 原则"(3 份副本、2 种介质、1 份异地)是行业共识。

第三,保留策略要算空间账。 rsync 硬链接方案下,保留 N 天大约占 "1 份全量 + N×每日变化量" 的空间,比 N 份全量省太多;而 tar 方案是实打实的 N 份全量。设计保留天数时要用 du -sh 实测增长速度,别拍脑袋。

回滚方式:本地版本可直接从 /backup/www/<日期>/ 取用(rsync 恢复或直接拷文件);tar 归档用 tar -xzf 解压。删除的过期版本无法找回,所以对关键数据一定要额外做异地保留。若误删了备份目录,只能从异地副本恢复——这正是异地备份存在的意义。

常见问题

Q1:rsync 的 --delete 到底要不要加?
看需求。做"镜像同步"(备份端要跟源端完全一致)时加;做"版本归档"时可不加,甚至建议不加,因为多删的文件留着反而是保护。关键是要理解:加了 --delete,源端删除会传导到备份端,所以必须配合多版本保留或独立防护。

Q2:为什么保留 7 天只占了一点点空间?
因为你用了 --link-dest。未变化的文件在不同日期目录之间是硬链接,共享同一份数据,只有变化的文件真正占新空间。用 du -sh 看单个日期目录会显得很大,但看整个备份根目录才是真实占用(du 对硬链接只算一次)。

Q3:增量备份能恢复到任意一天吗?
用 --link-dest 方案可以,因为每个日期目录在逻辑上都是完整快照(未变文件通过硬链接补齐)。而用 --delete 的单一镜像方案不能,它只保留当前状态。tar 方案能否恢复任意一天取决于你有没有保留那天的归档。

Q4:备份很慢,怎么优化?
几方面:排除无用目录(缓存、node_modules、临时文件);用 --bwlimit 避免影响业务但也别设太低;大数据量首次全量慢是必然的,之后增量就快了;如果数据量特别大,考虑文件系统快照(LVM/云盘)做快速起点,再慢慢同步。

Q5:怎么知道备份是不是真的成功?
三看:一看备份脚本的退出码和日志;二看备份目录的日期、文件数量、总大小是否合理;三做定期恢复演练。只写日志不验证是"假成功"的温床,很多团队都是等真要用时才发现备份文件是空的或损坏的。

总结

  • 备份方案要回答"备什么、留多久、怎么恢复"三个问题,操作系统和缓存不必备,关键数据才要。
  • 用 rsync --link-dest 做增量历史版本:每个日期都是完整快照,未变文件共享硬链接,空间占用远低于多份全量。
  • tar -czf 适合打包归档,注意文本可高效压缩、媒体文件没必要再压;每次备份后校验归档完整性。
  • 先写临时目录再原子改名,避免残缺备份被当成完整版本;脚本开头要检查源目录存在且非空。
  • --delete 会让源端删除传导到备份端,必须配合多版本保留或独立防护;备份务必放到独立磁盘并同步一份异地,且定期做恢复演练。