rsync 增量同步实战:末尾斜杠、删除红线与传完之后的三种校验

站点跑起来之后,最容易被低估的一项日常操作是「把东西从 A 搬到 B」。老服务器快到期了、要加一台机器做备份、要把附件从本地盘挪到对象存储挂载、要在测试环境复刻一份生产数据——这些事都绕不开增量同步,而增量同步的事实标准就是 rsync。

很多人用 rsync 的方式是记住一条 rsync -avz 源 目标,能跑通就当会了。但这条命令在真实场景里有三个隐含假设:源和目标在同一条路径语义下、删除是安全的、传输过程中没有人改数据。这篇文章按「先量再传、先算再删、先试再跑」的顺序,把这三个假设拆开讲清楚。

一、永远先跑一次 dry-run,并且把它当正式流程

rsync 最危险的两个参数是 --delete 和 --delete-after。前者在同步完成后删除目标端多余的文件,后者在传输完成后再删,但逻辑一样:目标端「多出来」的东西会被删掉,而 rsync 判断「多出来」的依据只是「源里没有」,不是「该不该有」。

一次典型的翻车是这样发生的:源路径末尾漏了一个斜杠。rsync 对末尾斜杠极其敏感——rsync -a /data/site/ host:/backup/ 是把 site 目录里的内容同步进 /backup/;而 rsync -a /data/site host:/backup/ 是把 site 这个目录本身同步成 /backup/site。看起来差别不大,但如果目标是 /backup/site/,第一种会正确地把内容放进去,第二种会在 /backup/site/site/ 下再套一层。配上 --delete 之后,这种错位会让决策逻辑整个反转,删掉的东西往往不是你以为的那部分。

所以固定动作是:任何带删除参数的同步,第一次必须加 -n(或 --dry-run)并把输出落到文件里看。更重要的是看数量级而不是看有没有报错:

rsync -avn --delete /data/site/ /backup/site/ > /tmp/sync_dry_$(date +%F).log 2>&1
grep -c '^deleting ' /tmp/sync_dry_*.log
grep -c '^>f' /tmp/sync_dry_*.log

第二条命令数的是要传的文件数,第一条数的是要删的文件数。我给自己定的红线是:删除数量超过目标端文件总数的 5%,就必须停下来人工核对。合法的同步里删除量通常是几十个,一旦出现几千个,八成是源路径错了、挂载掉了、或者源端因为磁盘满而部分文件消失。

还有一种更隐蔽的情况:源端目录整个不存在。rsync 会报错退出,但如果这条命令写在 cron 或 shell 脚本里,报错信息很容易被忽略,脚本继续往下跑。等发现时已经过了几天没有任何备份。这就是为什么后面会讲「校验」和「心跳」——不只看退出码。

二、末尾斜杠、路径语义与排除规则

上一节已经点出末尾斜杠的差异,这里把它讲完整,因为这是新手最容易反复出错的地方。判断标准很简单:末尾有斜杠表示「我要这个目录里的内容」,没有斜杠表示「我要这个目录本身」。同步目录内容时(绝大多数情况),源路径一定带斜杠。

排除规则是第二个高频坑。常见的写法是 --exclude=cache,但这条规则只匹配路径里任意层级中名字恰好为 cache 的文件或目录;如果要排除整个目录树,写 --exclude=/cache/ 更明确(开头的 / 表示锚定到传输根目录,末尾的 / 表示只匹配目录)。这些细节的差别在有嵌套同名目录时非常关键。

更实用的方式是把排除规则写进一个文件,纳入版本管理:

# /etc/rsync/site.exclude
- .git/
- node_modules/
- runtime/cache/
- uploads/tmp/
- *.log
- .env
+ uploads/
- uploads/tmp/

这里用到 rsync 的「规则顺序」机制:规则从上往下匹配,第一条命中的生效;+ 是包含、- 是排除。所以先排掉 uploads/tmp/ 再整体包含 uploads/,顺序反了就会把 tmp 也一起同步走。这类规则一定要用 --dry-run 加 --itemize-changes 验证一遍,看输出里到底有没有意外的路径进入传输列表。

另外注意 --exclude 与 --exclude-from 的叠加关系:两者都会生效,不是后者覆盖前者。如果既在命令行里写了 exclude 又用了 exclude 文件,两边都参与匹配,出现「明明排除了却还在传」时先检查这个。

三、增量判断靠什么:mtime、size 还是 checksum

rsync 默认判断文件是否需要传输的依据是「大小 + 修改时间」(size + mtime),而不是内容。这个默认值在大部分场景下都成立而且快,但它有三个典型失效场景。

第一是时间戳被重置的迁移。用 tar、cp -r、或者某些同步工具搬过来的文件,mtime 可能变成搬运时间,于是 rsync 认为所有文件都「更新」,全量重传。解决办法是加 -t(archive 模式 -a 已包含)保住时间戳,迁移时用 rsync -a 而不是 cp -r。

第二是mtime 相同但内容不同的文件。这在「生成静态页」的场景里很常见:某些生成器会保留原文件的 mtime,但模板改动后输出内容变了。这时必须用 -c(--checksum)逐文件比对校验和——代价是两边都要读完整个文件,几万个文件的话会明显变慢,通常只在排查阶段临时开启。

第三是时区与文件系统精度。ext4 的 mtime 精度是纳秒,而某些文件系统(FAT、部分网络文件系统)只到秒甚至两秒,跨文件系统同步时会出现「看起来一样但 rsync 认为不同」的反复重传。这种情况用 --modify-window=2 放宽比较窗口即可。

关于 -z 压缩:在内网或同机房同步时,-z 基本是负收益——压缩消耗 CPU,而带宽并不是瓶颈;只有在跨地域、带宽受限时才有意义。图片、视频、压缩包这类已经压过的文件,-z 几乎没有效果,反而拖慢速度。一个折中做法是用 --compress-level=1 只做浅压缩。

四、传完不等于传对:三种校验方式

rsync 成功退出只代表「传输过程没出错」,不代表「目标端内容和源端一致」。磁盘写满、目标端目录权限问题、传输被中断后重跑时的判断偏差,都可能让文件数量看起来对、内容却不对。

最轻量的校验是数文件数和对总字节数:

find /data/site -type f -printf '%s\n' | awk '{n++; s+=$1} END {print n, s}'
find /backup/site -type f -printf '%s\n' | awk '{n++; s+=$1} END {print n, s}'

第二种是再用一次 rsync 做「空跑校验」,这是最省事的做法,因为 rsync 自己就会逐文件比对:

rsync -an --delete --itemize-changes -c /data/site/ /backup/site/ | wc -l

输出应该是 0 行(或者只有目录时间戳变化这类无关项)。如果还有大量 >f 开头的行,说明两边不一致。注意这里的 -c 会让比对变慢,但作为每周一次的核对是值得的。

第三种是抽样比对关键文件,尤其是数据库导出文件、.env、上传目录里的新文件。用 sha256sum 取源和目标的哈希比一比即可。我在一次事故里就是靠这一条发现问题:rsync 报告成功,但目标端的 MySQL 备份文件比源端小了 12 字节——源端在 rsync 读取的同时,mysqldump 还在往同一个文件写入,拿到了一个不完整的快照。

这正是要讲的第五条原则:同步正在写入的文件,一定要先做一致性快照。数据库备份应该先 dump 到临时文件、写完再移动或改名,而不是直接 dump 到被同步的目录;日志这类持续追加的文件,要么排除在同步之外,要么接受「同步到的是某一时刻的副本」并在文档里写清楚。

五、放到定时任务里,以及回滚预案

把 rsync 装进 cron 之后,最大的变化是「没人在旁边看」。此时要补三样东西:锁、日志、心跳。

锁是为了避免上一次还没跑完、下一次又启动了。两次同步并行会导致目标端互相覆盖,尤其是带 --delete 的时候。用 flock 最省事:

flock -n /var/lock/rsync_site.lock \
  rsync -a --delete --exclude-from=/etc/rsync/site.exclude \
  /data/site/ /backup/site/ >> /var/log/rsync_site.log 2>&1

日志要轮转,否则几个月后自己把磁盘撑满,这种事故相当常见。配置一条 logrotate 规则,把保留期设成 14 天。

心跳是不能省的那一环:脚本跑完后比较文件数,把结果写到一个小文件里,再用外部监控读取这个文件的修改时间。如果超过预期周期没有更新,就报警。只靠 cron 的退出码是不够的——cron 只在有输出时发邮件,而很多机器根本没配 MTA。

最后是回滚预案。带 --delete 的同步在方向搞反时会变成「用空目录覆盖生产」。两个保护措施值得都上:一是 rsync 3 之后的 --backup --backup-dir=/backup/trash/$(date +%F),被删除和覆盖的文件会先落到备份目录,留一条后路;二是脚本里写死方向和角色判断,在目标主机上放一个标记文件,脚本发现标记不存在就直接退出,避免在错误的机器上跑。

如果要把同步做到生产环境上线这一步,--delay-updates 比直接逐文件覆盖更安全:它先把所有文件传到一个隐藏临时目录,全部传完再一次性改名换入,避免站点在同步过程中处于「一半新一半旧」的状态。对静态站、图片站这类由大量小文件组成的站点,这个参数能明显减少用户看到的 404 与样式错乱。

Last modification:November 11th, 2026 at 10:24 pm

Leave a Comment