Appearance
rsync 同步与备份
rsync 在运维里用得很多——发布代码、同步配置、拉日志、做简单备份。它的核心能力是增量传输:源和目标两边比对,只传变化的内容。传完第一次之后,后续同步只处理有差异的文件,网络开销小很多。
一、rsync 适合做什么,不适合做什么
| 适合 | 不太适合 |
|---|---|
| 发布静态资源到 Web 目录 | 把 mysqld、PostgreSQL 数据目录当备份(数据库有自己的备份工具) |
| 从多台服务器拉日志到日志分析机 | 需要版本快照的长期归档(应加专门备份软件) |
| 配置文件的定期备份 | 需要文件加密、索引和恢复演练的合规备份 |
| 跨机房同步大文件,断线能续传 | 实时同步(rsync 是周期性跑的,不是 inotify) |
| 让目标目录和源目录保持镜像 | 数据库事务一致性的备份——rsync 不知道文件内容是否正在被写入 |
rsync 是文件级别的同步工具,不是完整的备份系统。它能把文件搬过去,但一致性保证、快照管理、加密、索引、恢复演练这些事,要自己做方案。小规模配置和脚本备份用 rsync 很方便,数据库这种有事务状态的东西,应该用 mysqldump、pg_dump 或专门的备份工具,同步数据文件不能算是备份。
二、基本用法
本地到本地:
bash
rsync -av /etc/nginx/ /backup/nginx/本地到远端:
bash
rsync -av /etc/nginx/ backup@192.168.10.20:/backup/server-a/nginx/远端拉回本地:
bash
rsync -av backup@192.168.10.20:/backup/server-a/nginx/ ./nginx-restore/常用参数:
| 参数 | 做什么 |
|---|---|
-a | archive 模式:递归目录 + 保留权限、时间戳、软链接(属主属组在权限允许时也会保留) |
-v | 显示同步过程,看到每个文件 |
-h | 大小用人类可读的单位(K、M、G) |
-P | 等于 --partial --progress——保留未传完的文件 + 显示进度条 |
-z | 传输时压缩 |
平时用的组合:
bash
rsync -avhP /data/app/ backup@192.168.10.20:/backup/app/-z 不一定都要加。文本文件(日志、配置、代码)压缩收益明显;已经压缩过的文件(视频、tar.gz、zip、镜像层)再压一遍只会白耗 CPU,体积几乎不变。
三、源路径末尾的斜杠——结果完全不同
这个细节很容易弄错,但结果不一样:
bash
# 把 /data/app/ 里面的内容同步到 /backup/app/ 下
rsync -av /data/app/ backup@192.168.10.20:/backup/app/
# 把 app 这个目录本身放到 /backup/ 下,变成 /backup/app/
rsync -av /data/app backup@192.168.10.20:/backup/有斜杠 = 复制目录里面的内容。没斜杠 = 复制目录本身。从结果来看,带斜杠时目标目录里是散着的文件;不带斜杠时目标目录里多了一层 app/。
所以第一次写同步命令时先用 --dry-run 看一眼输出,确认目录层级是对的:
bash
rsync -avhP --dry-run /data/app/ backup@192.168.10.20:/backup/app/四、预演和查看变化
--dry-run 只打印即将发生的变化,不真正修改目标端:
bash
rsync -avhP --dry-run /data/app/ backup@192.168.10.20:/backup/app/配合 --itemize-changes 能看到每个文件的变化类型:
bash
rsync -avhn --itemize-changes /data/app/ backup@192.168.10.20:/backup/app/-n 就是 --dry-run 的短写法。
输出的变化标记不需要全背,知道这几个就够用了:
| 标记 | 含义 |
|---|---|
>f+++++++++ | 源端有这个文件,目标端没有,将会传过去 |
cd+++++++++ | 目标端不存在该目录,将会创建 |
*deleting | 目标端有这个文件但源端没有,将会被删除(只有加了 --delete 才出现) |
.f...p.... | 文件内容相同,但权限不同,将会只修改权限 |
看到 *deleting 时要慢下来确认路径。--delete 写错路径时,删错了文件比多传了几个文件严重得多。
五、排除不需要同步的目录和文件
备份和同步时,有些目录和文件不应该跟着同步——依赖包、缓存、临时文件之类:
bash
rsync -avhP \
--exclude 'node_modules/' \
--exclude '*.log' \
--exclude 'tmp/' \
/srv/app/ backup@192.168.10.20:/backup/app/规则多了以后,写成排除文件更整洁:
bash
cat > rsync-exclude.txt <<'EOF'
node_modules/
tmp/
*.log
.git/
.cache/
EOF
rsync -avhP \
--exclude-from rsync-exclude.txt \
/srv/app/ backup@192.168.10.20:/backup/app/排除规则是针对源目录做相对路径匹配的。源目录是 /srv/app/,规则 tmp/ 匹配的是 /srv/app/tmp/,不是系统根下的 /tmp/。
常见要排除的内容:
| 排除项 | 理由 |
|---|---|
node_modules/、vendor/ | 依赖可以重新安装,体积大,没必要备份 |
.git/ | 发布目录不需要版本历史 |
tmp/、cache/ | 临时文件和缓存,备份没有意义 |
*.log | 日志有自己的归档和清理策略,不应跟代码一起同步 |
排除日志要看场景——应用发布目录排除日志是合理的;专门做日志归档时当然不能排除。没有绝对对错的规则,看这一次同步的目的。
六、--delete:让目标端和源端完全一致
--delete 不只是"传源端有目标端没有的文件",而是"删掉目标端有但源端没有的文件"。相当于把目标目录变成源目录的精确镜像。
bash
rsync -avhP --delete /srv/app/dist/ web@192.168.10.30:/var/www/app/这个能力很适合静态资源发布——构建产物每次可能包含不同的哈希文件名,旧版本的 js/css 留在目标目录没有意义,反而浪费空间和让人困惑。但对于备份目录,加了 --delete 反而有风险:如果源端误删了文件,下一次同步时备份端也跟着删了,备份失去"恢复被误删文件"的价值。
带 --delete 的同步,第一步一定是预演:
bash
rsync -avhP --delete --dry-run /srv/app/dist/ web@192.168.10.30:/var/www/app/如果不想完全丢掉被删除的文件,可以用 --backup 把它们先移到别处:
bash
rsync -avhP \
--delete \
--backup \
--backup-dir="/backup/deleted/$(date +%F-%H%M%S)" \
/srv/app/dist/ web@192.168.10.30:/var/www/app/--backup 在覆盖或删除之前,把目标端旧文件保留一份。--backup-dir 指定旧文件存放到哪个目录(这个路径是在目标端解释的)。这样万一删错了,还能从 deleted/ 里找回来。
--delete 不建议一上来就写进定时任务。先跑一段时间的纯增量同步(不加 --delete),确认路径正确、排除规则合理、同步量符合预期,再考虑是否需要镜像删除。
七、权限和属主处理
-a 会尽量保留文件的属性,但能不能保留属主属组,取决于执行 rsync 的用户:
| 属性 | -a 的行为 |
|---|---|
| 目录结构 | 完整递归保留 |
| 软链接 | 保留为软链接(不跟随拷贝目标内容) |
| 权限模式 | 保留 |
| 修改时间 | 保留 |
| 属主/属组 | 以 root 执行时可以保留;普通用户通常只能保留为自己 |
普通用户同步时,目标端文件属主一般会变成这个用户。备份场景里这很正常——备份文件的属主是 backup 账号就对了。
如果只关心内容和时间戳,不需要保留 owner/group:
bash
rsync -rltvz /srv/app/ backup@192.168.10.20:/backup/app/去掉 -a 里的属主属组和设备文件保留,只保留递归(-r)、软链接(-l)、时间戳(-t)。
目标端是服务目录时,可能需要强制设权限:
bash
rsync -avh \
--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r \
./dist/ web@192.168.10.30:/var/www/app/D 表示目录,F 表示文件。这条规则的效果:目录可进入(所有人执行位),普通文件不可执行、属主可写。Web 静态文件发布讲究的就是这个——别把整个 dist/ 里的 html 和 png 传过去都设成可执行。
八、带宽限制和超时
生产网段传大文件时,不加限制可能把带宽打满:
bash
rsync -avhP \
--bwlimit=20m \
/backup/full.tar.gz backup@192.168.10.20:/backup/full.tar.gz--bwlimit=20m 约等于 20 MiB/s。rsync 不同版本对单位的处理有差异——有的版本 20m = 20 MiB/s,有的老版本认为是 20 MB/s 甚至 KByte/s。第一次在目标环境用时,可以用小文件实测一下实际传输速度,再固定写入脚本。
设置超时——超过指定秒数没有数据传输就退出:
bash
rsync -avhP \
--timeout=60 \
/data/app/ backup@192.168.10.20:/backup/app/--timeout=60 是 I/O 超时,不是总时长。如果网络一直在传数据(哪怕很慢),计时器会重置。只有卡死不动 60 秒以上才会触发超时。设得太短(比如 10 秒),偶尔的网络抖动就会中断正常传输。
通过 SSH 连接时可以加更多保活参数:
bash
rsync -avhP \
-e 'ssh -o ConnectTimeout=10 -o ServerAliveInterval=30 -o ServerAliveCountMax=3' \
/data/app/ backup@192.168.10.20:/backup/app/这三个 SSH 参数一起控制连接建立和保活——连接超时设了 10 秒避免长时间挂起,每 30 秒发一次心跳,3 次心跳没回应就断开。
九、一份基础备份脚本
bash
#!/usr/bin/env bash
set -euo pipefail
src="/etc/nginx/"
backup_host="backup@192.168.10.20"
backup_root="/backup/$(hostname)/nginx"
log_file="/var/log/rsync-nginx-backup.log"
mkdir -p "$(dirname "$log_file")"
rsync -avhP \
--delete \
--backup \
--backup-dir="$backup_root/deleted/$(date +%F-%H%M%S)" \
"$src" \
"$backup_host:$backup_root/current/" \
>> "$log_file" 2>&1设计上的几个考量:
| 做法 | 为什么 |
|---|---|
set -euo pipefail | 任何一步失败都让脚本退出,收到非 0 退出码知道出事了 |
backup_root 带了 hostname | 多台机器备份到同一个备份机,用主机名区分来源 |
current/ 目录 | 保留一份最新的完整快照,恢复时直接取 |
--backup-dir 用日期时间戳 | 被覆盖或删除的旧文件单独归档,按时间能找到 |
| 输出重定向到日志文件 | cron 中跑完之后能查到底传了什么、有没有报错 |
这个脚本适合配置文件和脚本的备份。数据量大、恢复要求高的话,要上快照(LVM/ZFS snapshot)、专用备份软件(borg、restic)或云平台的备份服务。
十、定时同步
用 cron 定周期执行:
bash
crontab -ecron
# 每天凌晨 2:10 同步 nginx 配置
10 2 * * * /usr/local/sbin/backup-nginx.shcron 里的环境变量很少——PATH 可能只有 /usr/bin:/bin。所以在脚本里所有命令和路径都写绝对路径(/usr/bin/rsync、/usr/local/sbin/backup-nginx.sh),不要依赖交互 shell 的 PATH。
另一个问题是并发:如果上一次同步还没跑完(网络慢了、目录变大了),下一次 cron 又触发了同一脚本,就会出现多个 rsync 进程同时往目标端写。用 flock 加文件锁:
bash
#!/usr/bin/env bash
set -euo pipefail
lock_file="/var/run/backup-nginx.lock"
flock -n "$lock_file" /usr/local/sbin/backup-nginx-inner.shflock -n 是非阻塞模式——拿不到锁立刻退出,不等。这样即使上一次超时或者文件很大,也不会堆积多个 rsync 进程互相争夺带宽和 I/O。
十一、历史快照和保留策略
只保留一份 current 是不够的。如果源端文件被误改或误删,并且已经同步到备份机的 current/ 里,那就等于没有可用的备份了。
用 --link-dest 保存按日期的快照目录,同时用硬链接节省空间:
bash
backup_date="$(date +%F)"
rsync -avh \
--link-dest="/backup/app/current" \
/srv/app/ \
"backup@192.168.10.20:/backup/app/snapshots/$backup_date/"--link-dest 的工作方式:目标端新建一个目录 snapshots/2026-05-26/。如果某个文件在 --link-dest 指定的目录(current/)里已经存在且内容相同,就在新目录里创建一个硬链接指向那个已有的文件,而不是再拷贝一份。内容有变化的文件才真正传输。
这样每天一个快照目录,每个看起来都是完整的数据副本,但实际物理上只占了一份数据量 + 每天变化的数据量。
更新 current 软链接,在备份机上操作:
bash
ssh backup@192.168.10.20 '
cd /backup/app
ln -sfn snapshots/$(date +%F) current
'注意这里假设备份机的日期和源机器相同。跨时区环境下要想好用哪边的日期,避免快照目录名和软链接指向的存在不一致。
定期清理旧快照,先查范围,再删:
bash
# 先看一眼要删哪些
find /backup/app/snapshots -mindepth 1 -maxdepth 1 -type d -mtime +30 -print
# 确认范围没问题后,再真正删除
find /backup/app/snapshots -mindepth 1 -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \;必须先 -print 确认范围,确认无误再替换成 -exec rm -rf。备份目录误删的代价远超其他操作——本意是保护数据,结果把保护手段给清掉了。
十二、备份要验证过才算备份
只跑同步不验证恢复,等于没有真的备份。恢复验证不需要覆盖原目录,先恢复到临时目录:
bash
# 恢复到临时目录
rsync -avh backup@192.168.10.20:/backup/app/current/ /tmp/app-restore/配置文件先 diff 看看改了哪里:
bash
diff -ruN /etc/nginx/ /tmp/nginx-restore/确认差异合理后,再用 rsync 写回原位置:
bash
rsync -avh --dry-run /tmp/nginx-restore/ /etc/nginx/
# 确认预演输出正常
rsync -avh /tmp/nginx-restore/ /etc/nginx/
nginx -t # 先做语法检查
systemctl reload nginx # 再加载nginx -t 放在 reload 前面——如果恢复的配置有语法错误,直接 reload 会让服务使用错误配置,后续行为不可预期。先做语法检查,通过再加载。
十三、常见问题的排查
目标端空间被打满
同步或备份目录增长过快,把目标端的磁盘吃满了。先看容量分布:
bash
df -h
du -sh /backup/app/*确认是快照目录堆积太多、deleted/ 目录没有清理、还是同步的文件量本身就超出了预期。使用 --backup-dir 和快照目录时,清理策略和同步脚本一样重要——不配清理策略只是把磁盘满的风险从源机器搬到了备份机。
同步很慢
先找瓶颈在哪一段:
| 瓶颈方向 | 怎么观察 |
|---|---|
| 网络 | iftop、nload、sar -n DEV 1 |
| 磁盘 I/O | iostat -x 1,看 %util 和 await |
| CPU | top,看 rsync 和 ssh 进程的 CPU 占用 |
| 小文件过多 | rsync 扫描阶段要逐文件比对元数据,百万级小文件会让扫描本身需要很长时间 |
小文件特别多时,rsync 的扫描阶段比传输阶段还慢。极端情况下,先打包再传输反而更快——但打包会失去增量同步的能力(每次都要传整个包)。要在"传输总量"和"扫描开销"之间权衡。
权限恢复后不对
确认备份时有没有带上完整的属性:
bash
rsync -avAX /data/app/ backup@192.168.10.20:/backup/app/-A 保留 ACL,-X 保留扩展属性。SELinux 环境下文件的扩展属性影响服务能否访问——缺少了安全上下文,即使权限数字对了服务也可能读不到。不过不是所有文件系统和远端都支持 -A -X,脚本上线前要先实际跑一次确认。
cron 没执行
查 cron 日志和脚本的日志:
bash
grep CRON /var/log/cron
tail -n 100 /var/log/rsync-nginx-backup.log常见原因:
| 原因 | 怎么发现/修 |
|---|---|
| 脚本没执行权限 | chmod +x script.sh,并确认脚本第一行有 #!/usr/bin/env bash |
| cron 的 PATH 太少 | 脚本里所有命令用绝对路径 |
| SSH key 需要口令交互 | 备份账号用无 passphrase 的专用密钥或配置 ssh-agent |
| 目标主机 DNS 解析失败 | 备份脚本里用稳定的主机名或直接写 IP |
| 上一次还没跑完 | 用 flock 加锁 |