之前写过一篇 MySQL 自动备份的,但备份文件只放在本机,总归不放心。后来有台服务器硬盘坏过一次,丢了两天的数据,从那以后我就开始折腾异地备份。
用了一圈方案,最后稳定跑在生产环境的就是 rsync + SSH。看起来简单,但实际配起来有不少细节,这里把踩过的坑整理一下。
为什么选 rsync 而不是 scp 或 rclone
scp 每次全量传输,文件大了慢得要命。rclone 功能多,但我只需要服务器到服务器的同步,装个 Go 程序有点重。rsync 是 Linux 自带的,增量传输只传差异部分,带宽占用小,断点续传也支持,对我来说刚好够用。
唯一的缺点是 rsync 本身不加密传输,所以必须走 SSH 通道,这也是下面要重点说的。
基本用法先跑通
最简单的同步命令:
rsync -avz -e ssh /data/backup/ user@remote:/data/backup/
几个参数解释:
-a:归档模式,保留权限、时间戳、符号链接等-v:显示传输详情-z:传输时压缩-e ssh:指定用 SSH 做传输通道
注意 /data/backup/ 末尾的斜杠。有斜杠表示同步目录里的内容,没有斜杠会把整个目录同步过去变成 /data/backup/backup/。这个坑我踩过,第一次同步完发现多了一层目录,还以为数据丢了。
第一个坑:SSH 密钥认证不能用密码
手动执行 rsync 输入密码没问题,但放到 crontab 里定时跑,密码认证根本行不通。必须配 SSH 密钥对。
# 在备份源服务器上生成密钥
ssh-keygen -t ed25519 -f ~/.ssh/backup_key -N ""
# 把公钥传到目标服务器
ssh-copy-id -i ~/.ssh/backup_key.pub user@remote
然后在 rsync 命令里指定私钥:
rsync -avz -e "ssh -i ~/.ssh/backup_key" /data/backup/ user@remote:/data/backup/
我第一次用的 RSA 4096 位密钥,后来换成了 ed25519,更短更安全,生成速度也快。
第二个坑:SSH 端口不是 22 的情况
生产环境为了安全,SSH 端口通常会改掉。rsync 指定端口的写法:
rsync -avz -e "ssh -p 2222 -i ~/.ssh/backup_key" /data/backup/ user@remote:/data/backup/
这里有个容易忽略的点:-e 参数里的引号必须用双引号,用单引号在某些 shell 里会出问题。我之前在 CentOS 7 上用单引号跑了两个月没出事,换到 Ubuntu 22.04 就报错了,排查了半天。
第三个坑:带宽限制和大文件传输
白天同步把带宽打满,线上服务直接卡死。rsync 提供了限速参数:
rsync -avz --bwlimit=5000 -e "ssh -i ~/.ssh/backup_key" /data/backup/ user@remote:/data/backup/
--bwlimit=5000 表示限制 5000KB/s,大约 5MB/s。根据自己的带宽调整,别问我怎么知道这个数值的——直接把 100M 带宽打满导致业务接口超时那次,被运维同事念叨了一个星期。
第四个坑:排除文件的正确写法
有些临时文件、日志碎片不需要同步。rsync 的排除规则:
rsync -avz --exclude='*.tmp' --exclude='.cache/' --exclude='node_modules/' -e "ssh -i ~/.ssh/backup_key" /data/backup/ user@remote:/data/backup/
如果排除规则多了,可以写到文件里:
# exclude.txt
*.tmp
.cache/
node_modules/
*.log
rsync -avz --exclude-from=exclude.txt -e "ssh -i ~/.ssh/backup_key" /data/backup/ user@remote:/data/backup/
我之前忘了排除 .cache/,同步了一堆浏览器缓存文件过去,目标服务器磁盘直接报警。
第五个坑:删除目标端多余文件
源端删了文件,目标端默认不会删。时间长了两边数据不一致。加上 --delete 参数:
rsync -avz --delete -e "ssh -i ~/.ssh/backup_key" /data/backup/ user@remote:/data/backup/
但这个参数要慎用。我有一次源端目录挂载失败,rsync 认为所有文件都被删了,--delete 直接把目标端也清空了。双重灾难。
后来我加了防护措施:同步前先检查源目录是否存在且有文件:
#!/bin/bash
SOURCE="/data/backup/"
MIN_FILES=5
count=$(find "$SOURCE" -type f | wc -l)
if [ $count -lt $MIN_FILES ]; then
echo "$(date): Source directory has only $count files, aborting sync" >> /var/log/backup.log
exit 1
fi
rsync -avz --delete -e "ssh -i ~/.ssh/backup_key" "$SOURCE" user@remote:/data/backup/
这样源端目录异常时不会误删目标端数据。
完整的备份脚本
把上面的坑都处理了,最终的脚本长这样:
#!/bin/bash
# /opt/scripts/rsync_backup.sh
set -euo pipefail
SOURCE="/data/backup/"
REMOTE="user@backup-server"
REMOTE_PATH="/data/backup/"
SSH_KEY="$HOME/.ssh/backup_key"
SSH_PORT=2222
LOG="/var/log/rsync_backup.log"
MIN_FILES=5
BWLIMIT=5000
# 检查源目录
file_count=$(find "$SOURCE" -type f | wc -l)
if [ $file_count -lt $MIN_FILES ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') ERROR: Source has only $file_count files, aborting" >> "$LOG"
exit 1
fi
# 执行同步
rsync -avz --delete --bwlimit=$BWLIMIT --exclude='*.tmp' --exclude='.cache/' -e "ssh -p $SSH_PORT -i $SSH_KEY -o StrictHostKeyChecking=no" "$SOURCE" "$REMOTE:$REMOTE_PATH" >> "$LOG" 2>&1
if [ $? -eq 0 ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') OK: Sync completed, $file_count files" >> "$LOG"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') ERROR: rsync failed with exit code $?" >> "$LOG"
fi
StrictHostKeyChecking=no 是为了首次连接时不会因为交互确认而卡住。生产环境建议先把目标服务器的指纹加到 known_hosts 里,然后去掉这个参数。
配 crontab 定时执行
# 每天凌晨 3 点执行
0 3 * * * /opt/scripts/rsync_backup.sh
建议放在凌晨业务低峰期执行,配合 --bwlimit 限制带宽。
第六个坑:rsync 断点续传其实有条件
rsync 理论上支持断点续传,但默认行为是如果文件大小或修改时间变了,会重新传整个文件。对于大文件(比如数据库备份的 sql.gz),断网后重传很浪费。
加上 --partial 和 --partial-dir 参数:
rsync -avz --partial --partial-dir=.rsync-partial -e "ssh -i ~/.ssh/backup_key" /data/backup/ user@remote:/data/backup/
--partial 会保留未传完的文件,下次从断点继续。--partial-dir 指定临时目录,不会污染目标目录。
校验备份是否完整
光同步还不够,得定期校验。用 --dry-run 看看有没有差异:
rsync -avzn --delete -e "ssh -i ~/.ssh/backup_key" /data/backup/ user@remote:/data/backup/
-n 是 dry-run 模式,只列出会传输的文件但不实际执行。如果没有输出,说明两边数据一致。
我每周跑一次 dry-run,把结果写到日志里。如果有大量文件要同步,说明上次同步可能出了问题。
写在最后
rsync 这个工具确实够用,但要注意的细节比想象中多。核心就是三件事:SSH 密钥别用密码、带宽要限制、删除操作要防护。把这三点做好,基本上可以稳定跑很久了。
如果你的数据量特别大(几百 GB 以上),可以考虑 rsync 的 --inplace 参数减少磁盘 IO,或者用 zstd 替代默认的 zlib 压缩。但大部分场景下,上面这套方案够用了。
评论一下?