用 rsync 组织多代备份
目标
使用 rsync 配置 mirror sync 与 generation backup,并以实际运维形式加入 --delete guard 和负载控制。
为什么重要
--link-dest generation backup 是 rsync 最优雅的功能之一。每一代目录看起来都像完整 snapshot,但磁盘只占用变更部分。其原理是 hard link,因此用 du 测量时会比预期小得多——相同 inode 不会被重复计算。
而且,--delete 必须配合 guard 使用。如果 source 为空或 mount 已断开时执行,它会删除 target 中的全部数据。这是现实中反复发生的事故。
步骤
- 在
/root/rs/src下创建app/main.py、conf/app.yaml、logs/app.log、tmp/cache.bin,并同步到/root/rs/dst/。最终必须存在/root/rs/dst/app/main.py。 - 使用 dry-run + itemize 执行并保存到
/root/rs/itemize.txt。target 必须是尚未同步过的新目录——如果再次指向第 1 步的dst/,由于状态已经相同,rsync 不会输出任何行,文件会为空。itemize 用来查看“什么会因为什么原因改变”,因此必须先有待变更内容。 - 编写
/root/rs/backup.sh。以set -euo pipefail开始;source 为空时以退出码 1 中止;使用--delete将/root/rs/src/同步到/root/rs/mirror/。source 路径必须通过第一个参数接收,没有参数时使用/root/rs/src/——评分器会把空目录作为第一个参数传入,检查 guard 是否真的执行。运行脚本创建/root/rs/mirror。 - 在
/root/rs/exclude.txt中写入排除模式(logs/与tmp/),并使用它同步到/root/rs/clean/。不得存在/root/rs/clean/logs和/root/rs/clean/tmp。 - 创建 generation backup。先完整同步到
/root/rs/gen/daily.1/,再通过--link-dest创建/root/rs/gen/daily.0/。未变更文件必须在两代中共享同一个 inode。 - 使用
--numeric-ids同步到/root/rs/numeric/,并把app/main.py的 owner UID 作为单独一行数字写入/root/rs/uid.txt。 - 编写
/root/rs/throttled.sh。其中的 rsync 命令必须同时包含ionice、nice、--bwlimit三个元素。运行脚本创建/root/rs/slow/。 - 创建包含以下 4 行的
/root/rs/report.txt:MIRROR_FILES=<mirror 안 파일 수>/CLEAN_HAS_LOGS=no/SHARED_INODE=yes/GEN_COUNT=2
参考
rsync -ain <원본>/ <대상>/是 dry-run + itemize 组合。- 排除列表通过
--exclude-from=/root/rs/exclude.txt指定。 - 使用
stat -c %i <두 세대의 같은 파일>比较 inode 是否共享。 - 负载控制示例:
ionice -c 3 nice -n 19 rsync -a --bwlimit=1M <원본>/ <대상>/ - 常见错误 1:第 3 步脚本硬编码 source 路径,无法通过空目录测试。请确保从第一个参数接收 source 路径,无参数时使用默认值。
- 常见错误 2:第 5 步为
--link-dest提供相对路径,它会相对于 target 解析,导致无法创建 link。
执行基础 mirror sync
在 /root/rs/src 下创建 app/main.py、conf/app.yaml、logs/app.log、tmp/cache.bin,并同步到 /root/rs/dst/。最终必须存在 /root/rs/dst/app/main.py。
记住 trailing slash 规则。target 内不应再出现 source 目录名称。
显示变更明细
使用 dry-run + itemize 执行并保存到 /root/rs/itemize.txt。target 必须是尚未同步过的新目录——如果再次指向第 1 步的 dst/,由于状态已经相同,rsync 不会输出任何行,文件会为空。itemize 用来查看“什么会因为什么原因改变”,因此必须先有待变更内容。
dry-run 与 itemize 一起使用时,会逐行显示哪些内容将如何改变。
编写带 guard 的 backup 脚本
编写 /root/rs/backup.sh。以 set -euo pipefail 开始;source 为空时以退出码 1 中止;使用 --delete 将 /root/rs/src/ 同步到 /root/rs/mirror/。source 路径必须通过第一个参数接收,没有参数时使用 /root/rs/src/——评分器会把空目录作为第一个参数传入,检查 guard 是否真的执行。运行脚本创建 /root/rs/mirror。
source 为空时必须立即停止,并通过退出码区分该情况。
创建排除列表文件
在 /root/rs/exclude.txt 中写入排除模式(logs/ 与 tmp/),并使用它同步到 /root/rs/clean/。不得存在 /root/rs/clean/logs 和 /root/rs/clean/tmp。
把 pattern 集中在文件中更容易管理,每行写一个 pattern。
创建 generation backup
创建 generation backup。先完整同步到 /root/rs/gen/daily.1/,再通过 --link-dest 创建 /root/rs/gen/daily.0/。未变更文件必须在两代中共享同一个 inode。
为 --link-dest 使用绝对路径更安全。可通过 link count 验证。
保留数字 owner
使用 --numeric-ids 同步到 /root/rs/numeric/,并把 app/main.py 的 owner UID 作为单独一行数字写入 /root/rs/uid.txt。
有一个选项可使用数字 ID 而不是名称,这是为恢复到不同系统做准备。
控制负载
编写 /root/rs/throttled.sh。其中的 rsync 命令必须同时包含 ionice、nice、--bwlimit 三个元素。运行脚本创建 /root/rs/slow/。
同时使用 bandwidth 限制选项和降低磁盘优先级的命令,并把完整命令保存在脚本中。
生成各代容量报告
创建包含以下 4 行的 /root/rs/report.txt:
MIRROR_FILES=<mirror 안 파일 수> / CLEAN_HAS_LOGS=no / SHARED_INODE=yes / GEN_COUNT=2
由于 hard link,du 的结果会比预期小,而这一事实本身就是本实验的结论。