1. 备份的基本要素
一句话总结: 备份方案要先回答三个问题——备份什么、能容忍丢多少、多久能恢复;没有恢复演练的备份,只是「一份尚未验证的副本」。
1.1 三个必须回答的问题
| 问题 | 对应指标 | 影响的设计 |
|---|---|---|
| 能丢多少数据 | RPO(恢复点目标) | 同步频率、是否增量 |
| 多久必须恢复 | RTO(恢复时间目标) | 全量还是快照、是否本地保留 |
| 备份到哪里 | 容灾等级 | 是否异地、是否加密 |
#!/usr/bin/env bash
set -euo pipefail
# 备份前的体检:先确认源存在,避免备份出空目录
SRC=${SRC:-/var/lib/app}
[ -d "$SRC" ] || { echo "source missing: $SRC" >&2; exit 1; }
# 记录源的大小与文件数,作为事后比对的基线
du -sh "$SRC"; find "$SRC" -type f | wc -l
「备份了一个空目录却报告成功」是备份事故的第一名:挂载点没挂上、路径写错、权限不足,都会让 rsync 同步了 0 个文件并返回 0。
一句话总结: 备份脚本必须显式校验「源非空」,否则挂载失败会被静默当成「成功备份了空数据」。
1.2 3-2-1 原则
3 份副本、2 种介质、1 份异地。落到脚本上就是三条流水线:/backup/local/ 本地快照(快速恢复)、/backup/offsite/ 异地同步(容灾)、/backup/archive/ 归档(合规与长期保留)。
2. rsync 增量同步
一句话总结: rsync 的核心是「只传差异」:它先在两端比较文件列表,再用滚动校验算法只传输变化的部分,所以首次全量、之后增量。
2.1 尾斜杠语义
这是 rsync 最经典的坑:源路径末尾有没有斜杠,含义完全不同。
# 有斜杠:把 src 的内容复制到 dst → 结果 /backup/dst/file1
rsync -a /data/src/ /backup/dst/
# 无斜杠:把 src 目录本身放进 dst → 结果 /backup/dst/src/file1
rsync -a /data/src /backup/dst/
# 用 --dry-run 先确认行为,这是最省事的自查手段
rsync -av --dry-run /data/src/ /backup/dst/
记住口诀:源加斜杠是「拷贝里面的东西」,源不加斜杠是「拷贝这个目录」;目标端有没有斜杠不影响结果。
一句话总结: 源路径尾斜杠决定「拷内容还是拷目录」,养成先
--dry-run再执行的习惯。
2.2 常用选项组合
# 最常用的备份组合
rsync -aHAX --numeric-ids --partial --info=progress2 /data/src/ /backup/dst/
| 选项 | 作用 |
|---|---|
-a | 归档模式,等于 -rlptgoD |
-H | 保留硬链接关系 |
-A | 保留 ACL |
-X | 保留扩展属性(xattr) |
--numeric-ids | 按 uid/gid 数字同步,不做名字映射 |
--partial | 中断后保留半截文件,下次续传 |
--info=progress2 | 显示整体进度而非逐文件刷屏 |
# 跨主机时走 SSH,并指定密钥与端口
rsync -aHAX -e 'ssh -p 2222 -i ~/.ssh/backup_key' \
--numeric-ids /data/src/ backup@host:/backup/dst/
# 慢链路加压缩(CPU 换带宽,本机同步不要加 -z),并限速
rsync -az --bwlimit=5000 /data/src/ remote:/backup/dst/
-z 只在网络传输时才有意义;本机或高速内网同步加 -z 反而浪费 CPU。--bwlimit 的单位是 KB/s。
3. 排除规则与删除语义
一句话总结: 排除规则用「模式」描述不要同步的东西,删除语义用
--delete家族描述目标端多出来的文件怎么处理;两者都要先用--dry-run验证。
3.1 排除与包含
rsync -a --exclude='*.tmp' --exclude='.git/' --exclude='cache/' \
--exclude-from=/etc/backup/exclude.txt /data/src/ /backup/dst/
# /etc/backup/exclude.txt
*.log
*.pid
node_modules/
.DS_Store
# 以 / 开头表示锚定到传输根,避免误伤同名子目录
/tmp/
顺序很重要:rsync 按先匹配先生效处理规则,所以「先排除整个目录、再用 + 包含其中某个文件」是无效的,必须把 + 写在前面。
# 反面教材:先排除 cache/,再想包含 cache/keep.txt 是无效的
rsync -a --exclude='cache/' --include='cache/keep.txt' src/ dst/
# 正确顺序:先包含,再排除
rsync -a --include='cache/keep.txt' --exclude='cache/' src/ dst/
一句话总结: 规则按顺序匹配、先命中先生效,所以「例外」必须写在「通配排除」之前。
3.2 删除语义
# 目标端多出来的文件也删掉(做镜像同步)
rsync -a --delete /data/src/ /backup/dst/
# 连被排除的文件也从目标端删除(危险:可能删掉本地保留的东西)
rsync -a --delete --delete-excluded /data/src/ /backup/dst/
# 把删除延后到传输结束,减少目标端「短暂缺文件」的窗口
rsync -a --delete-delay /data/src/ /backup/dst/
# 保险丝:一次最多删这么多文件,超过就中止
rsync -a --delete --max-delete=100 /data/src/ /backup/dst/
# 先看会删什么,确认无误再去掉 --dry-run
rsync -av --delete --dry-run /data/src/ /backup/dst/ \
| grep -E '^deleting'
--delete 只作用于传输根之下,不会删到目标端的父目录。但把源路径写错(比如源指向空目录)时,--delete 会把目标端清空——--max-delete 就是为这种情况准备的保险丝。
4. 硬链接快照
一句话总结:
--link-dest让「本次未变化的文件」以硬链接指向上一次快照,于是每份快照看起来都是完整的全量,实际只占用增量空间。
4.1 link-dest 的原理
#!/usr/bin/env bash
set -euo pipefail
BASE=/backup/snapshots
SRC=/data/src
TODAY=$(date +%Y-%m-%d)
LATEST=$(readlink -f "$BASE/latest" 2>/dev/null || true)
mkdir -p "$BASE/$TODAY"
# 与上一份快照比对:没变的文件直接建硬链接,不占额外空间
if [ -n "$LATEST" ] && [ -d "$LATEST" ]; then
rsync -aHAX --delete --link-dest="$LATEST" "$SRC/" "$BASE/$TODAY/"
else
rsync -aHAX --delete "$SRC/" "$BASE/$TODAY/" # 首次全量
fi
ln -sfn "$BASE/$TODAY" "$BASE/latest" # 指向最新快照
du -sh --exclude=latest "$BASE"/* | sort -h # 查看每份快照的独有数据量
关键约束:--link-dest 的路径必须是目标端的路径(相对目标根或绝对路径都行),且只有在文件「属性完全一致」时才会建立硬链接。
# 验证硬链接是否生效:同一文件的链接数应该大于 1
stat -c '%h %n' "$BASE/latest/some/big/file.bin"
一句话总结: 硬链接快照让「每日全量」只花增量成本,但一旦有人对快照做原地修改,就会破坏所有引用同一 inode 的快照。
4.2 轮转与保留
#!/usr/bin/env bash
set -euo pipefail
BASE=/backup/snapshots
KEEP_DAILY=7
# 保留最近 7 份每日快照,其余删除
mapfile -t snaps < <(find "$BASE" -maxdepth 1 -type d -name '20*' | sort)
count=${#snaps[@]}
if [ "$count" -gt "$KEEP_DAILY" ]; then
for old in "${snaps[@]:0:$((count - KEEP_DAILY))}"; do
echo "pruning $old"; rm -rf -- "$old"
done
fi
轮转顺序必须是「先建新快照、再删旧快照」:反过来的话,中间一旦失败就会既没有新的、又删掉了旧的。
# 安全写法:先完成同步与校验,确认成功后再轮转
rsync -aHAX --delete --link-dest="$LATEST" "$SRC/" "$BASE/$TODAY/" \
&& ln -sfn "$BASE/$TODAY" "$BASE/latest" \
&& prune_old_snapshots
5. 校验与完整性检查
一句话总结: rsync 的默认校验是「大小 + 修改时间」,要更严格就用
--checksum;跨系统长期保存还需要自己生成校验和清单。
5.1 同步校验
# 1. 空跑 + 逐项列出差异(--dry-run 不实际传输)
rsync -aHAX --delete --dry-run --itemize-changes /data/src/ /backup/dst/
# ">f+++++++++" 表示新增,">f.st......" 表示内容与时间都变了
# 2. 按内容校验(更严格,但代价高)
rsync -aHAX -c --dry-run --itemize-changes /data/src/ /backup/dst/
# 3. 同步后用统计摘要确认「传了多少、删了多少」
rsync -aHAX --delete --stats /data/src/ /backup/dst/ | tail -20
-c(--checksum)会读取两端所有文件内容计算校验和,代价很高,只适合小数据量或关键数据的事后复核,不要作为日常选项。
一句话总结: 日常同步靠「大小 + 时间」就够快,关键数据的事后复核再用
-c或独立校验和清单。
5.2 清单与校验和
#!/usr/bin/env bash
set -euo pipefail
SNAP=/backup/snapshots/latest
# 生成清单:路径 + sha256
find "$SNAP" -type f -print0 | sort -z | xargs -0 sha256sum > "$SNAP.manifest.sha256"
# 事后验证(在快照目录下执行,路径才对得上)
(cd "$SNAP" && sha256sum -c "$SNAP.manifest.sha256" --quiet) && echo "integrity OK"
# 更轻量的存在性检查:比对文件数
src_count=$(find /data/src -type f | wc -l)
dst_count=$(find "$SNAP" -type f | wc -l)
[ "$src_count" -eq "$dst_count" ] || echo "WARN: file count mismatch"
注意 sha256sum -c 的路径是相对于清单生成时的位置,所以要么用相对路径生成清单,要么在验证时先 cd 到快照根。
6. 异地同步、加密与保留策略
一句话总结: 异地同步的核心约束是带宽与安全:用
--bwlimit避免打满专线、用 SSH 加密通道、用 age/gpg 做端到端加密,最后用分级保留策略控制成本。
6.1 异地同步与带宽控制
# 通过 SSH 隧道同步,限速 5MB/s,避免影响业务带宽
rsync -aHAX --numeric-ids --partial --append-verify --bwlimit=5000 \
--exclude-from=/etc/backup/exclude.txt \
-e 'ssh -o Compression=no -i ~/.ssh/backup_key' \
/backup/snapshots/latest/ backup@offsite:/backup/app/
# 用 --partial-dir 让中断的传输不污染目标目录
rsync -a --partial-dir=.rsync-partial /data/src/ remote:/backup/dst/
# 无人值守场景必须限制 SSH 命令,避免密钥泄露后被滥用
# ~/.ssh/authorized_keys 中这样写:
# command="rsync --server --sender -logDtpre.iLsfxC . /backup/",no-pty,no-agent-forwarding ssh-ed25519 AAAA...
--append-verify 适合「只追加的大文件」(如日志归档);对普通文件树,--partial 更通用。
一句话总结: 异地备份的密钥要用
command=限制成只能跑 rsync,这样即使密钥泄露也无法拿到 shell。
6.2 加密与保留
# 端到端加密:tar 打包后用 age 加密再上传
tar -C /backup/snapshots -cf - latest \
| age -r "$(cat /etc/backup/age.pub)" \
> /backup/encrypted/app-$(date +%Y%m%d).tar.age
# 解密验证(演练时用)
age -d -i /etc/backup/age.key app-20261001.tar.age | tar -tf - | head
#!/usr/bin/env bash
# 分级保留:日备保留 7 份、周备 4 份、月备 12 份
set -euo pipefail
BASE=/backup/archive
# 每日保留 7 天
find "$BASE/daily" -maxdepth 1 -type d -mtime +7 -exec rm -rf -- {} +
# 每周保留 4 周
find "$BASE/weekly" -maxdepth 1 -type d -mtime +28 -exec rm -rf -- {} +
# 每月保留 12 个月
find "$BASE/monthly" -maxdepth 1 -type d -mtime +365 -exec rm -rf -- {} +
保留策略必须写成「同时满足天数与份数」:只按天数删,遇到备份失败会出现「天数到了但没新备份」的空窗;只按份数删,则长期停摆时会把历史全删光。
7. 恢复演练
一句话总结: 恢复演练是把备份真正变成「可用资产」的唯一手段:定期在隔离环境还原、校验内容、记录耗时,才能确认 RTO 是否达标。
7.1 演练脚本
#!/usr/bin/env bash
set -euo pipefail
SNAP=${1:-/backup/snapshots/latest}
DRILL=/tmp/restore-drill-$$
trap 'rm -rf "$DRILL"' EXIT
mkdir -p "$DRILL"
start=$(date +%s)
# 1. 还原(硬链接会被解开成独立文件)
rsync -aHAX "$SNAP/" "$DRILL/"
# 2. 校验文件数
src_files=$(find "$SNAP" -type f | wc -l)
dst_files=$(find "$DRILL" -type f | wc -l)
[ "$src_files" -eq "$dst_files" ] || { echo "FAIL: count mismatch"; exit 1; }
# 3. 抽样验证关键文件的校验和
for f in etc/app.conf var/lib/app/db.sqlite; do
[ -f "$SNAP/$f" ] || continue
a=$(sha256sum "$SNAP/$f" | cut -d' ' -f1)
b=$(sha256sum "$DRILL/$f" | cut -d' ' -f1)
[ "$a" = "$b" ] || { echo "FAIL: $f differs"; exit 1; }
done
echo "drill OK: $dst_files files, $(( $(date +%s) - start ))s" # 实测 RTO
# 用 systemd timer 每月跑一次演练,结果写进日志:
# journalctl -u backup-drill.service --since "30 days ago"
7.2 常见恢复陷阱
# 陷阱一:忘了权限与属主(恢复后服务起不来)——-a 已含权限与属主
rsync -aHAX --numeric-ids /data/src/ /backup/dst/
# 陷阱二:恢复到已有数据的目录,叠加出「混合状态」
# 正确做法:恢复到全新目录,校验通过后再切换
mv /var/lib/app /var/lib/app.old && mv "$DRILL/app" /var/lib/app
# 陷阱三:硬链接快照里的文件被误改,导致所有快照一起坏
chmod -R a-w /backup/snapshots/2026-10-01 # 快照目录设为只读
# 陷阱四:备份脚本自身失败却返回 0
# 用「同步 + 校验 + 记录标记」三段式,任一步失败都让流水线失败
rsync -aHAX --delete "$SRC/" "$DST/" \
&& rsync -aHAX -c --dry-run --itemize-changes "$SRC/" "$DST/" \
&& touch "$DST/.backup-ok-$(date +%Y%m%d)"
最后一条 .backup-ok-* 标记文件是很多团队验证「备份是否真的跑过」的抓手:监控系统只要检查「最近 25 小时内是否出现了新的标记文件」即可。
8. 总结
| 环节 | 要点 |
|---|---|
| 目标 | 先定 RPO/RTO,再决定频率与形态 |
| 前置校验 | 源必须存在且非空,否则「成功备份了空目录」 |
| 尾斜杠 | 源带斜杠拷内容,不带斜杠拷目录本身 |
| 选项 | -aHAX --numeric-ids --partial 是备份基线 |
| 排除 | 规则先命中先生效,例外要写在通配排除之前 |
| 删除 | --delete 配 --max-delete 做保险丝,先 --dry-run |
| 快照 | --link-dest 硬链接实现「全量外观、增量占用」 |
| 轮转 | 先建新、后删旧,失败时不会两头空 |
| 校验 | 日常靠大小与时间,关键数据用清单加校验和 |
| 异地 | SSH 加密 + --bwlimit 限速 + command= 限制密钥 |
| 保留 | 天数与份数双重条件,避免空窗或全删 |
| 演练 | 定期还原到隔离目录,实测 RTO 并记录 |
备份的可靠性不来自「脚本写得多复杂」,而来自「每一步都可验证」:源非空、传输有摘要、快照有校验、轮转有顺序、恢复有演练。当这套流程被 systemd timer 定时拉起、失败时以非零退出码触发告警,数据安全才从「但愿没事」变成「有据可查」。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。