rsync --link-dest 增量快照备份实战:硬链接省空间原理、数据库一致性处理与 systemd 定时全流程

rsync 不只是同步:用 --link-dest 做出"时间机器"式快照

很多站长天天用 rsync -av 同步数据,却不知道 rsync 还有一个被严重低估的玩法:用 --link-dest 实现空间利用率极高的增量快照。它能让你每天都有一份完整可读的备份目录,但磁盘占用只有"变化量",看起来像苹果的 Time Machine——每份快照都是完整树,实际共享了没变的文件。

这篇教程讲清楚它的原理、配置方法、数据库一致性的处理,以及实际可上线的 systemd + 脚本方案。适合自建服务器、跑 WordPress 或自研应用的站长,用 VPS 自带磁盘 + 一个异地目录就能搭起来。

核心原理:硬链接与"看起来全量、实际增量"

--link-dest=DIR 的语义是:拿 DIR 作为基准目录,本次同步时,凡是内容和基准里一致的文件,不复制实体,而是创建一个指向基准文件 inode 的硬链接;只有新增或修改过的文件才真正占新空间。

因为硬链接只是目录项指向同一个数据块,不带额外数据,所以:

  • 快照 snapshot-2026-10-10 和 snapshot-2026-10-09 里没变的文件共享同一份数据,不翻倍占用。
  • 但每个快照目录都能独立完整地浏览和恢复,你看到的是完整文件树,不需要"先恢复全量再叠增量"这种麻烦链式恢复。
  • 一旦某个文件在新快照里被修改,rsync 会写一个新数据块,老快照仍指向旧数据块——所以历史版本不会被破坏。这正是它比传统"全量+增量"备份更省心的原因。

关键约束有三个:基准目录和本次目标必须在同一个文件系统上(硬链接不能跨文件系统);备份盘最好用支持硬链接的文件系统(ext4、xfs 都行);不要把 --link-dest 指向会被外部改动的目录。

第一步:规划目录结构

一个清晰的结构能避免后续翻车:

/backup/zz1984/
├── current -> snapshot-2026-10-10    # 软链接指向最新快照
├── snapshot-2026-10-08/
├── snapshot-2026-10-09/
└── snapshot-2026-10-10/

用"日期命名的快照目录 + 一个 current 软链接"的方式,脚本每次只需找到上一个快照作为 --link-dest,同时对外始终有个固定路径可读。

第二步:写核心备份脚本

下面这个脚本用"临时目录 + 完成后原子改名"的方式,避免备份中途被打断留下半个快照:

#!/bin/bash
# /usr/local/bin/snapshot_backup.sh
set -euo pipefail

SRC="/var/www/html/"          # 注意结尾斜杠:复制目录内容而非目录本身
DEST="/backup/zz1984"
LB="last_success"             # 指向最近一次成功快照的软链接
TODAY="$(date +%F)"
TMP="$DEST/.snapshot-$TODAY.tmp"

mkdir -p "$DEST"

# 找上一次成功快照作为硬链接基准
if [ -L "$DEST/$LB" ]; then
    PREV="$(readlink -f "$DEST/$LB")"
    LINKOPT="--link-dest=$PREV"
else
    LINKOPT=""
fi

# 起一个临时目录,保证原子性
rm -rf "$TMP"
mkdir -p "$TMP"

rsync -aHAX --delete --numeric-ids $LINKOPT \
      --exclude='.git' --exclude='cache/*' \
      "$SRC" "$TMP/"

# 改名成正式快照
FINAL="$DEST/snapshot-$TODAY"
rm -rf "$FINAL"
mv "$TMP" "$FINAL"

# 更新 last_success 软链接(原子替换)
ln -sfn "snapshot-$TODAY" "$DEST/.$LB.tmp"
mv -Tf "$DEST/.$LB.tmp" "$DEST/$LB"

echo "[OK] snapshot $TODAY done"

逐条解释关键选项:

  • -a:归档模式,等价于 -rlptgoD,保留权限、时间、所有者、软链接等。
  • -H:保留硬链接关系。源站里若是软链接或硬链接,要额外注意。
  • -A 和 -X:分别保留 ACL 和扩展属性(SELinux 标签、capabilities)。用 Docker 或启用了 SELinux 的服务器必须带。
  • --delete:让快照和源站保持一致,源站删掉的文件在快照里也标记为不存在(但在历史快照里依然保留)。
  • --numeric-ids:按数字 UID/GID 保留,避免跨机器时因用户名映射错乱改所有者。
  • 源路径结尾的斜杠:/var/www/html/ 表示复制"目录里的内容";不带斜杠会把整个 html 目录套进备份里,恢复时多一层。

第三步:处理数据库的一致性

直接 rsync 正在被写入的 MySQL 数据目录,会得到损坏的、无法恢复的备份——这一点是新手最常踩的坑。正确做法对数据库有两种:

小库:先 dump 再备份

mysqldump --single-transaction --quick --routines --triggers \
  --databases zz1984 | gzip > /backup/db/zz1984-$(date +%F).sql.gz

--single-transaction 对 InnoDB 提供一致性快照而不锁表,业务几乎无感。dump 出来的文件再纳入 rsync 快照,就得到了带日期、可校验的数据库副本。

大库/多库:用物理备份工具

库超过几十 GB 时用 mariabackup 或 xtrabackup 做物理热备,备份期间对 InnoDB 基本无阻塞。备份产物同样放进快照目录树。

顺序很重要:先做数据库一致性导出,再跑文件 rsync,让快照目录里同时包含"某一时刻的数据库 dump"和"对应时刻的站点文件",逻辑时间点才对齐。

第四步:用 systemd timer 定时调度

不要再用裸 cron 了,systemd timer 能给出更好的一致性保证和日志。先写服务单元 /etc/systemd/system/snapshot-backup.service:

[Unit]
Description=rsync snapshot backup
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/snapshot_backup.sh

再写定时器 /etc/systemd/system/snapshot-backup.timer:

[Unit]
Description=Run rsync snapshot backup daily

[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=600

[Install]
WantedBy=timers.target

Persistent=true 是关键:如果服务器在预定时刻是关机/重启状态,下次开机后 systemd 会补跑一次,不会静默漏掉一天。RandomizedDelaySec 用来打散触发时间,避免和站点其他定时任务(比如凌晨数据库迁移)撞车。

启用并验证:

systemctl daemon-reload
systemctl enable --now snapshot-backup.timer
systemctl list-timers snapshot-backup.timer
systemctl start snapshot-backup.service   # 手动跑一次
journalctl -u snapshot-backup.service -n 50

第五步:保留策略与空间验证

快照不会自动过期,需要自己删旧的。最简单的方式是在脚本末尾加一段"只保留最近 14 天"的逻辑:

# 删除 14 天前的快照目录(注意只删我们自己的命名规则)
find "$DEST" -maxdepth 1 -type d -name 'snapshot-*' \
     -mtime +14 -exec rm -rf {} +

务必用严格的 -name 'snapshot-*' 限定,否则一个手抖的 rm -rf 可能删到不该删的目录。删除老快照时,被硬链接共享的数据块只有在所有引用它的快照都删掉后才会真正释放空间,这正是硬链接快照的好处。

验证实际占用,别只看目录大小——要看向硬盘真正吃了多少:

du -sh /backup/zz1984              # 表面大小(含共享)
du -sh --exclude='snapshot-2026-10-09' /backup/zz1984   # 排除基准看真实增量
df -h /backup                      # 文件系统实际使用

想快速确认硬链接确实生效了,看 inode 链接计数:

ls -li /backup/zz1984/snapshot-2026-10-09/index.php \
       /backup/zz1984/snapshot-2026-10-10/index.php

如果两行的 inode 号相同、链接数为 2,说明这个文件被两份快照共享,没有重复占空间——正是我们要的效果。

第六步:异地容灾与常见坑

  • 本地快照不等于异地备份:硬盘或整机故障时本地快照一起没。再补一层"把快照 rsync 到异地服务器",异地同步用 --delete 要小心,建议带 --backup --backup-dir 做软删除。
  • SSH 传输走增量压缩:异地同步加 -z 压缩、--partial --append-verify 断点续传;用密钥认证 + --rsync-path,不要明文传密码。
  • 不要和源站放同一个磁盘:这是备份第一铁律。备份盘物理独立,或至少独立分区。
  • 定期做恢复演练:备份没验证过等于没有。每月抽一次,从某个历史快照目录里真恢复一个文件出来,确认内容完整可读。
  • 排除目录要写对:缓存目录、session 目录、临时文件没必要进快照,用 --exclude 排除能显著减少体积。

把 --link-dest 快照和数据库一致性导出、systemd 定时、异地同步、定期恢复演练组合起来,一套"每天都在、随时可回滚、几乎不吃额外空间"的备份体系就成型了。对个人站长来说,这比买任何备份插件都更可控、更透明。

Last modification:October 10th, 2026 at 07:27 pm

Leave a Comment