Skip to content

rsync 同步与备份

rsync 在运维里用得很多——发布代码、同步配置、拉日志、做简单备份。它的核心能力是增量传输:源和目标两边比对,只传变化的内容。传完第一次之后,后续同步只处理有差异的文件,网络开销小很多。

一、rsync 适合做什么,不适合做什么

适合不太适合
发布静态资源到 Web 目录把 mysqld、PostgreSQL 数据目录当备份(数据库有自己的备份工具)
从多台服务器拉日志到日志分析机需要版本快照的长期归档(应加专门备份软件)
配置文件的定期备份需要文件加密、索引和恢复演练的合规备份
跨机房同步大文件,断线能续传实时同步(rsync 是周期性跑的,不是 inotify)
让目标目录和源目录保持镜像数据库事务一致性的备份——rsync 不知道文件内容是否正在被写入

rsync 是文件级别的同步工具,不是完整的备份系统。它能把文件搬过去,但一致性保证、快照管理、加密、索引、恢复演练这些事,要自己做方案。小规模配置和脚本备份用 rsync 很方便,数据库这种有事务状态的东西,应该用 mysqldumppg_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/

常用参数:

参数做什么
-aarchive 模式:递归目录 + 保留权限、时间戳、软链接(属主属组在权限允许时也会保留)
-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 -e
cron
# 每天凌晨 2:10 同步 nginx 配置
10 2 * * * /usr/local/sbin/backup-nginx.sh

cron 里的环境变量很少——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.sh

flock -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 和快照目录时,清理策略和同步脚本一样重要——不配清理策略只是把磁盘满的风险从源机器搬到了备份机。

同步很慢

先找瓶颈在哪一段:

瓶颈方向怎么观察
网络iftopnloadsar -n DEV 1
磁盘 I/Oiostat -x 1,看 %util 和 await
CPUtop,看 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 加锁