Skip to content

05|rsync 同步与备份

上一篇的 scp 每次都全量传——一个 10GB 的备份目录改了一个文件,scp 又得传 10GB。rsync 不一样:它对比源和目标两边,只传变化的部分。改了一个文件,就只传那一个。

这种增量传输让 rsync 成了运维里最高频的文件同步工具——发布、备份、同步日志都靠它。本篇讲 rsync 怎么用,重点讲几个最容易出事的坑(路径斜杠、--delete)。

一、适用边界

适合的场景

  • 发布静态资源到 Web 目录
  • 从多台服务器拉日志到分析机
  • 配置文件定期备份
  • 跨机房同步文件(断线续传)
  • 让目标目录与源目录保持镜像

不适合的场景

  • 数据库数据目录(MySQL/PostgreSQL):有事务状态,同步出来的可能是损坏的
  • 需版本快照的长期归档:应加专门的备份软件(borg、restic)
  • 合规加密/索引/演练:rsync 不做这些
  • 实时同步:rsync 是周期性触发,非 inotify 驱动

rsync 是文件级别同步工具,不是完整备份系统。小规模配置/脚本备份用 rsync 很方便,数据库应用各自的备份工具。

二、基础用法

rsync 的命令和 scp 长得像,区别在参数和它增量比对的特性。最基础的本地到远端:

bash
# 本地到远端
rsync -av /etc/nginx/ backup@10.10.1.20:/backup/server-a/nginx/

# 远端拉回本地
rsync -av backup@10.10.1.20:/backup/server-a/nginx/ ./nginx-restore/

第一次跑是全量(两边没有比对基础),之后只传变化的部分。

常用参数组合

bash
# 最常用
rsync -avhP /data/app/ backup@host:/backup/app/

# 远端指定端口
rsync -avhP -e 'ssh -p 2222' /data/app/ backup@host:/backup/app/
参数含义
-aarchive 模式:递归+保留权限/时间戳/软链接/属主
-v显示文件列表
-h人类可读单位
-P--partial --progress:断点续传+进度
-z传输时压缩(文本类有效,已压缩的文件无效)
-n--dry-run:预演,不实际修改
--delete使目标端完全=源端,删除多余文件
--backup覆盖或删除前保留备份
--bwlimit=限制带宽
--link-dest基于快照的硬链接增量(类快照)

三、路径斜杠陷阱

rsync 源路径末尾的斜杠,结果完全不同:

bash
# 带斜杠:复制目录内容
rsync -av /data/app/ backup@host:/backup/app/
# → /backup/app/<files> (不产生 app 层)

# 不带斜杠:复制目录本身
rsync -av /data/app backup@host:/backup/
# → /backup/app/<files> (多一层 app/)

带斜杠=复制内容,不带斜杠=复制目录。 未确认时先用 --dry-run 预览:

bash
rsync -avhP --dry-run /data/app/ backup@host:/backup/app/

四、预演与变化查看

bash
# 预演
rsync -avhP --dry-run /data/app/ backup@host:/backup/app/

# 带变化标记
rsync -avhP -n --itemize-changes /data/app/ backup@host:/backup/app/

变化标记解读:

标记含义
>f+++++++++源端有,目标端无,将会传输
cd+++++++++目标端无此目录,将创建
*deleting目标端有但源端无,将被删除(仅 --delete)
.f...p....内容相同,只改权限

看到 *deleting 时要慢下来确认路径。

五、排除规则

bash
# 命令行排除
rsync -avhP \
    --exclude 'node_modules/' \
    --exclude '*.log' \
    /srv/app/ backup@host:/backup/app/

# 从文件读取排除规则
cat > rsync-exclude.txt <<'EOF'
node_modules/
tmp/
*.log
.git/
.cache/
EOF

rsync -avhP --exclude-from rsync-exclude.txt /srv/app/ backup@host:/backup/app/

排除规则是相对源目录做匹配的。 源目录是 /srv/app/,规则 tmp/ 匹配 /srv/app/tmp/,不是系统 /tmp/

六、--delete 镜像同步

--delete 会让目标端变成源端的精确镜像:目标端多出来的文件会被删掉:

bash
rsync -avhP --delete /srv/app/dist/ web@host:/var/www/app/

这个能力适合静态资源发布(旧版本的哈希文件不需要保留)。**但不适合备份场景:**源端误删时备份也跟着删。

bash
# 第一步:N 先用 --dry-run 确认
rsync -avhP --delete --dry-run /srv/app/dist/ web@host:/var/www/app/

--backup:删除前保留旧文件

bash
rsync -avhP \
    --delete \
    --backup \
    --backup-dir="/backup/deleted/$(date +%F-%H%M%S)" \
    /srv/app/dist/ web@host:/var/www/app/

--backup 在覆盖或删除前,把目标端旧文件保留到 --backup-dir即便误删了,也能从 deleted/ 找回。

七、权限与属主

-a 默认保留属性,但能否保留属主/属组取决于执行用户:

bash
# root 执行:保留原属主/属组
# backup 用户执行:属主变为 backup

强制设置权限

Web 静态文件发布场景:

bash
rsync -avh \
    --chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r \
    ./dist/ web@host:/var/www/app/
写法含义
Du=rwx目录:属主读写执行
Dgo=rx同组/其他:读执行
Fu=rw文件:属主读写
Fgo=r文件:同组/其他只读

别把 HTML 和图片传过去都设成可执行。

八、带宽与超时

bash
# 限制带宽(约 20 MiB/s)
rsync -avhP --bwlimit=20m /backup/full.tar.gz backup@host:/backup/

# I/O 超时(60 秒无数据传输则退出)
rsync -avhP --timeout=60 /data/app/ backup@host:/backup/app/

# SSH 保活
rsync -avhP \
    -e 'ssh -o ConnectTimeout=10 -o ServerAliveInterval=30 -o ServerAliveCountMax=3' \
    /data/app/ backup@host:/backup/app/

--bwlimit 不同版本单位有差异——首次使用用小文件实测速度。 --timeout 是 I/O 超时,不是总时长——只要在传数据就不会触发。

九、备份脚本

bash
#!/usr/bin/env bash
# 备份脚本模板
set -euo pipefail

src="/etc/nginx/"
backup_host="backup@10.10.1.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

定时执行(cron + flock)

cron
# 每天凌晨 2:10
10 2 * * * /usr/local/sbin/backup-nginx.sh

防止并发:

bash
#!/usr/bin/env bash
flock -n /var/run/backup-nginx.lock /usr/local/sbin/backup-nginx-inner.sh

cron 里的 PATH 极少,脚本中所有命令用绝对路径。

只保留一份 current/ 不够——源端误删后,备份也跟着丢。用 --link-dest 保存按日期的快照目录:

bash
backup_date="$(date +%F)"

rsync -avh \
    --link-dest="/backup/app/current" \
    /srv/app/ \
    "backup@host:/backup/app/snapshots/$backup_date/"

工作原理:文件内容与 --link-dest 目录相同的,创建硬链接,不额外占用空间;变化的文件才真正传输。

每天一个快照目录,物理上只占一份数据+每日变化量。

bash
# 更新 current 软链接
ssh backup@host 'cd /backup/app && ln -sfn snapshots/$(date +%F) current'

# 清理 30 天前的快照(先确认范围!)
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 确认,再删除。

十一、恢复验证

只同步不验证恢复等于没有备份:

bash
# 恢复到临时目录
rsync -avh backup@host:/backup/app/current/ /tmp/app-restore/

# 对比变化
diff -ruN /etc/nginx/ /tmp/nginx-restore/

# 确认后写回
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 会使服务异常。

十二、故障排查

现象排查方向
目标端空间爆df -h;du -sh /backup/app/*;清理策略配了吗?
同步慢iftop / nload 看网络;iostat -x 1 看 IO;小文件过多→扫描慢
权限不对备份时加 -AX 保留 ACL 和扩展属性
cron 没跑grep CRON /var/log/cron;脚本有执行权限?SSH key 要口令?
ssh key 要口令备份账号用无 passphrase 的专用密钥
目标主机名解析失败脚本里写 IP,或写 /etc/hosts 里确保有记录