rsync --link-dest 硬链接增量快照备份实战
适用场景
需要按天/按小时保留多份“完整可浏览”的目录副本,但又不想每份都占满磁盘。典型对象是配置文件目录、代码工作区、日志归档前的原始数据、小型静态站点内容等。这类场景不需要块级快照,只要有能直接 cd 进去翻文件的目录树即可。
rsync --link-dest 的思路是:新快照目录里,凡是与参照快照完全相同的文件,都直接做成硬链接,共享同一个 inode;只有新增或内容变化的文件才真正写入新数据。于是每份快照看起来都是完整的目录树,实际磁盘增量只有变更部分。
原理要点
- 硬链接指向同一个 inode,链接计数加一,不额外占用数据块。
- 只有同一文件系统内才能建立硬链接,所以所有快照必须放在同一分区。
--link-dest指向一个已存在的目录,rsync 在传输时先与该目录比对,命中则链接,未命中才复制。-H用于保留源端文件之间已有的硬链接关系,配合--link-dest时应一并加上。
最小可运行示例
以下脚本中的路径均为占位值,按实际环境替换。要求 bash 4.4 以上(空数组展开行为)。
#!/usr/bin/env bash
# snapshot.sh
set -euo pipefail
SRC="${SRC:-/srv/data/}" # 源目录,结尾保留斜杠
DST="${DST:-/backup/snap}" # 快照根目录,须与源在同一文件系统
STAMP="$(date +%Y%m%d-%H%M%S)"
NEW="${DST}/snap-${STAMP}"
LATEST="${DST}/latest"
mkdir -p "${NEW}"
# 若存在上一份快照,则以它为参照做硬链接复用
LINK_OPT=()
if [ -d "${LATEST}" ]; then
LINK_OPT=(--link-dest="$(readlink -f "${LATEST}")")
fi
rsync -aHAXS --numeric-ids --delete \
--exclude='.cache/' \
--exclude='*.tmp' \
--exclude='/logs/' \
"${LINK_OPT[@]}" "${SRC}" "${NEW%/}/"
# 原子切换 latest 软链接,供下次作为 --link-dest 参照
ln -sfn "${NEW}" "${DST}/.latest.tmp"
mv -Tf "${DST}/.latest.tmp" "${LATEST}"
参数说明:-a 归档模式;-H 保留硬链接;-A/-X 保留 ACL 与扩展属性;-S 处理稀疏文件;--numeric-ids 避免按名称映射导致属主变化。--delete 在这里作用是让新目录与源保持一致(新建目录本身为空,实际主要用于重复运行同一时间戳目录时清理残留)。
排除规则建议写在脚本里而不是命令行,便于审计:临时文件、缓存目录、日志滚动中的活动文件都应排除,否则每次都会产生新副本,空间节省效果会大打折扣。
运行方法
chmod +x snapshot.sh
sudo SRC=/srv/data/ DST=/backup/snap ./snapshot.sh
首次运行时 latest 不存在,脚本退化为一次完整复制。第二次及以后,--link-dest 指向上一份快照,未变更文件全部变成链接。可以放进 cron:
15 2 * * * SRC=/srv/data/ DST=/backup/snap /opt/bin/snapshot.sh >> /var/log/snapshot.log 2>&1
调试时先用 --dry-run 加 -i 观察:rsync -aHAXSi --dry-run --link-dest=... src/ dst/,输出中以 hf 开头的行表示被硬链接复用的文件。
验证空间节省与链接关系
看实际占用(du 默认按块统计,跨快照共享的 inode 只计一次):
du -sh /backup/snap/snap-*
看逻辑大小,与上面结果对比即可看出复用比例:
du -sh --apparent-size /backup/snap/snap-20240602-020000
两者差距越大,说明硬链接复用越多。也可以直接看分区剩余空间:df -h /backup。
验证某个未变文件确实是同一个 inode:
find /backup/snap/snap-20240602-020000/etc/hosts \
-samefile /backup/snap/snap-20240601-020000/etc/hosts
有输出即表示两个路径共享 inode。
看硬链接计数:
stat -c '%h %i %n' /backup/snap/snap-20240602-020000/etc/hosts
%h 是链接计数。若源文件只被一份快照引用,值为 1;被 N 份快照共享则为 N。
验证变更文件是独立副本:
a=$(stat -c %i /backup/snap/snap-20240601-020000/etc/app.conf)
b=$(stat -c %i /backup/snap/snap-20240602-020000/etc/app.conf)
[ "$a" != "$b" ] && echo "内容已变更,占用新数据块"
删除旧快照的安全顺序
硬链接的语义决定了:删除某一份快照时,只是把共享 inode 的链接计数减一,其他快照仍然完整。但顺序仍要守规矩,否则一旦最新快照本身有问题,会同时失去回滚点。
- 确认最新快照可用:
rsync退出码为 0(脚本中set -e已保证),并抽查关键目录的文件数量与大小是否与源一致。 - 确认
latest指向的不是待删目录。 - 从最旧的一份开始删,逐个删,不要一次批量:
OLDEST=$(ls -1d /backup/snap/snap-* | sort | head -n 1)
CURRENT=$(readlink -f /backup/snap/latest)
if [ "$OLDEST" != "$CURRENT" ]; then
rm -rf -- "$OLDEST"
fi
- 删除后重新执行
du -sh /backup/snap/snap-*与df -h /backup,确认空间确有释放。
顺序原则:先建新、再验新、后删旧。反向操作(先删旧再建新)等于把自己置于“任何一次失败都无备份可用”的状态。
边界与限制
- 必须同分区。跨文件系统时硬链接会失败,rsync 会静默退化为复制,空间不再节省。
- 快照不是只读快照。因为共享 inode,只要有人就地修改某份快照里的文件,所有引用该 inode 的快照都会跟着变。建议对快照目录设置只读权限或限制写权限,恢复时以只读方式打开。
- 不适合直接备份正在写的数据库文件。应先做逻辑导出或调用数据库自身的备份机制,再对导出目录做硬链接快照。
- 属主与 ACL 需要相应权限。
-A/-X与--numeric-ids只有在具备读取源权限(通常是 root)时才能完整保留。 - 软链接不会被硬链接。rsync 对符号链接是重建而非链接复用,数量极大时会增加元数据开销。
- 快照数量增长后
du与find变慢,属于正常现象,建议定期只保留固定份数。 mv -T依赖 GNU coreutils;在其他实现上可用ln -sfn替代,但切换瞬间不是原子的。
- 点赞
- 收藏
- 关注作者
评论(0)