rsync --link-dest 硬链接增量快照备份实战

举报
yd_246123679 发表于 2026/10/31 23:07:30 2026/10/31
【摘要】 用 rsync 的 --link-dest 在同一文件系统内做硬链接增量快照:首次全量复制,后续快照把未变更文件硬链接到上一份,仅复制变更数据;给出脚本、排除规则、用 du 与 find -samefile 校验空间和链接关系,以及旧快照的安全删除顺序。

适用场景

需要按天/按小时保留多份“完整可浏览”的目录副本,但又不想每份都占满磁盘。典型对象是配置文件目录、代码工作区、日志归档前的原始数据、小型静态站点内容等。这类场景不需要块级快照,只要有能直接 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 的链接计数减一,其他快照仍然完整。但顺序仍要守规矩,否则一旦最新快照本身有问题,会同时失去回滚点。

  1. 确认最新快照可用:rsync 退出码为 0(脚本中 set -e 已保证),并抽查关键目录的文件数量与大小是否与源一致。
  2. 确认 latest 指向的不是待删目录。
  3. 从最旧的一份开始删,逐个删,不要一次批量:
OLDEST=$(ls -1d /backup/snap/snap-* | sort | head -n 1)
CURRENT=$(readlink -f /backup/snap/latest)
if [ "$OLDEST" != "$CURRENT" ]; then
  rm -rf -- "$OLDEST"
fi
  1. 删除后重新执行 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 替代,但切换瞬间不是原子的。
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。