资讯详情

资讯详情

rsync同步Shell脚本实战:带锁、日志与SSH探活的自动化方案

最怕半夜爬起来手动同步数据。我之前维护一个发布项目时线上和备份目录靠 rsync 维持一致每次改完文件都得手工敲一遍命令敲错一个参数就可能把线上多余文件删掉。后来我把常用的 rsync 同步命令整理成了下面这个 shell 脚本带并发锁、日志、SSH 探活和 dry-run 开关直接丢进 cron 里跑。今天就把这份 rsync 同步 shell 脚本完整分享出来顺便把脚本里每个参数、每个“保命细节”为什么这么设计讲清楚。无论你是刚接触 rsync 的新手还是已经在用但经常被各种同步问题折腾的老手这份脚本都能直接抄作业。1. 直接上脚本一份开箱即用的 rsync 同步 shell 脚本1.1 这个脚本解决的是哪一类问题先说适用范围。这个脚本解决的是单向同步把本地某个目录的内容原样同步到远程服务器或另一台机器的指定目录。典型的场景包括发布静态资源、前端构建产物到服务器将业务数据、配置文件镜像到备份机把多台机器上的某个目录收敛到统一位置。它不适合做双向同步也不适合当版本管理工具。如果你需要两边互相合并文件应该去找双向同步方案如果你需要保留每次变更的历史版本那这个脚本只能作为起点后面我会在第 5 节讲怎么扩展成“带快照的同步”。脚本本身基于常见实践编写核心设计只有一句话先探活、再拿锁、记日志、跑 rsync、按退出码上报结果。整条链路没有任何魔法任何一个环节出问题都能在日志里看到。1.2 完整脚本代码把下面的内容保存到/usr/local/bin/rsync_sync.sh然后执行chmod x /usr/local/bin/rsync_sync.sh。脚本中所有变量集中在“可修改配置区”日常使用基本只需要改这一块。#!/usr/bin/env bash # rsync_sync.sh # 单向同步脚本本地目录 - 远程目录通过 SSH # 用法 # ./rsync_sync.sh # 正式同步 # ./rsync_sync.sh --dry-run # 演练只看差异不动数据 set -euo pipefail # 可修改配置区 SOURCE_DIR/data/www/project/ DEST_USERdevops DEST_HOST10.0.0.20 DEST_PORT22 DEST_DIR/data/www/project/ EXCLUDE_FILE/etc/rsync_sync_exclude.txt LOG_DIR/var/log/rsync_sync LOCK_FILE/tmp/rsync_sync.lock export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # 配置区结束 # 解析参数 DRY_RUN if [ ${1:-} --dry-run ]; then DRY_RUN-n fi # 准备日志目录与锁文件 mkdir -p $LOG_DIR LOG_FILE$LOG_DIR/sync_$(date %Y%m%d).log log() { echo [$(date %Y-%m-%d %H:%M:%S)] $* | tee -a $LOG_FILE } exec 9$LOCK_FILE if ! flock -n 9; then log 另一个 rsync 进程还在运行本次任务跳过。 exit 0 fi log 开始同步: $SOURCE_DIR - $DEST_USER$DEST_HOST:$DEST_DIR (dry-run${DRY_RUN:-no}) # 源目录检查 if [ ! -d $SOURCE_DIR ]; then log 错误源目录不存在$SOURCE_DIR exit 1 fi # 排除文件不存在也没关系rsync 会警告这里提前兜底 if [ ! -f $EXCLUDE_FILE ]; then touch $EXCLUDE_FILE fi # 先通过 SSH 远程建目录顺带当作连通性检查 remote_cmdmkdir -p ${DEST_DIR} if ! ssh -p $DEST_PORT -o BatchModeyes -o ConnectTimeout10 -o StrictHostKeyCheckingno \ $DEST_USER$DEST_HOST $remote_cmd; then log 错误SSH 连接或远程目录创建失败$DEST_USER$DEST_HOST:$DEST_DIR exit 1 fi # 组装 rsync 参数 rsync_args( -avz --delete --partial --timeout60 --exclude-from$EXCLUDE_FILE -e ssh -p ${DEST_PORT} -o BatchModeyes -o ConnectTimeout10 -o StrictHostKeyCheckingno ) if [ -n $DRY_RUN ]; then rsync_args(--dry-run) fi rsync_args( $SOURCE_DIR $DEST_USER$DEST_HOST:$DEST_DIR ) # 执行同步 if rsync ${rsync_args[]}; then log 同步完成 else status$? log 同步失败rsync 退出码${status} exit $status fi1.3 第一次运行建议先 dry-run拿到脚本后不要直接跑正式同步。建议先执行一遍演练模式让脚本告诉你“如果同步会发生什么”/usr/local/bin/rsync_sync.sh --dry-run--dry-run会在 rsync 命令里加入-n参数rsync 只做“扫描对比”列出需要传输和删除的文件不会实际修改远程目录。这一步非常关键尤其是第一次运行、目标目录里已经存在大量文件时你需要在模拟输出里先确认三件事有没有不打算同步的文件出现在传输列表里有没有目标机上的文件会被--delete删除源目录和目标目录的层级是否正确。确认无误后再执行一次不带--dry-run的真实同步。第一次同步数据量可能很大日志会一直增长这是正常的。脚本输出同时会在终端显示一份并追加到日志文件方便一边看一边排查。2. 拆开看参数为什么我要这样写 rsync2.1 -avz 不是银弹但大多数场景够用很多教程上来就让你用-avz这四个字符确实覆盖了绝大多数同步需求但我建议你对它的含义有数-a是--archive的简写等同于-rlptgoD也就是递归、保留符号链接、保留权限、保留时间戳、保留属主和属组、保留设备文件和特殊文件。对服务器文件同步来说这一条基本决定了“同步后的目录像不像原来的目录”。-v是--verbose输出传输细节。没有它脚本日志里只能看到一句“完成”排查时很难定位是哪次同步动到了哪个文件。-z是压缩传输。远程同步时文件内容经过压缩后再传能明显降低带宽消耗。但如果两台机器在同一内网、带宽充裕-z反而会消耗两端 CPU大文件多的时候可能变慢。我现在的做法是跨机房或低带宽场景保留-z纯内网同步会去掉。还有一个容易忽略的参数是-H保留硬链接。默认情况下 rsync 并不会保存硬链接关系处理大量同名不同 inode 的文件时会各自复制一份造成目标机空间膨胀。如果你的源目录里有硬链接建议在rsync_args里加上-H。2.2 --delete 是双刃剑得配合 exclude 和演练使用--delete的作用是让目标目录完全镜像源目录源目录中已不存在的文件目标目录中也会被删除。对“同步”这件事来说这是保证一致性的核心参数没有它目标目录会保留一堆历史残留文件。但--delete也是最危险的参数。常见的翻车现场是源目录漏挂载、清空、或者排除规则写错结果一次执行后目标目录被删掉大量文件。所以我在脚本里做了三层防护--exclude-from$EXCLUDE_FILE把不需要同步的目录先排除掉第一次使用或改动排除规则后强制先跑--dry-run更保守的做法是给 rsync 加上--backup和--backup-dir在删除前先备份到目标机指定目录rsync -avz --delete --backup --backup-dir/data/trash/$(date %F_%H%M%S) \ $SOURCE_DIR $DEST_USER$DEST_HOST:$DEST_DIR教训是--delete应该开但应该在确认过 dry-run 输出之后开而不是从一开始就永远开着。2.3 用 SSH 通道而不是 rsync daemon 的三个理由rsync 有两种传输方式走 SSH或者走 rsync daemon。这个脚本选择 SSH原因是复用系统账号体系和密钥认证。rsync daemon 需要单独维护一份rsyncd.conf权限控制也依赖 rsync 用户体系多一台机器就多一套配置。SSH 天然加密。文件内容在网关上不会被明文嗅探安全性高一个档次。排障顺手。SSH 出问题可以直接ssh userhost手动测而 daemon 模式下连接报错信息相对抽象。使用 SSH 前需要先配好免密登录否则脚本会因为BatchModeyes直接失败。初始化密钥可以用ssh-keygen -t ed25519 ssh-copy-id -p $DEST_PORT $DEST_USER$DEST_HOST脚本里-o BatchModeyes的意思是不尝试交互式输入密码。把它放在 cron 任务里非常重要否则某个凌晨任务会因为等密码输入而挂在那里。ConnectTimeout10则保证网络异常时能在 10 秒内失败退出而不是长时间卡住。2.4 --timeout、--partial 与排除列表长任务的保命组合一个同步任务动辄几分钟甚至几小时网络抖动、文件锁、磁盘繁忙都可能让 rsync 无响应。--timeout60规定如果 60 秒内没有任何数据传输rsync 就主动放弃避免进程永久悬挂。--partial则是为“断点续传”准备的。默认情况下如果同步大文件途中失败rsync 会删掉已经传输的部分内容加上--partial后临时文件会保留下次同步时从断点继续。这个参数对动辄几十 GB 的目录同步效果非常明显能省下大量重传流量。排除列表EXCLUDE_FILE是我强烈建议维护的文件。内容类似*.log *.tmp cache/ node_modules/ .git/ .DS_Store有了排除列表日志、缓存、依赖目录这些“不需要同步”的内容就永远不会进入传输清单。排除列表的匹配规则和 rsync filter 规则一致遇到目录写dir/遇到文件名写*.log不能靠感觉乱写否则排除不生效。3. 脚本里几个容易忽略的保命细节3.1 flock 锁防止两个 rsync 互相打架cron 任务最经典的问题之一就是“上一次同步还没跑完下一次任务又启动了”。两个 rsync 同时操作同一个目标目录轻则日志混乱重则把目标目录同步到一半的状态造成数据不一致。脚本里我用 flock 解决这个问题exec 9$LOCK_FILE if ! flock -n 9; then log 另一个 rsync 进程还在运行本次任务跳过。 exit 0 fiexec 9lockfile把锁文件绑定到文件描述符 9 上flock -n 9是尝试非阻塞获取锁。如果获取不到说明另一个实例正在运行当前实例直接退出。这里我选择“跳过本次任务”而不是“等上一个任务结束”因为对定时同步来说错过一次任务往往比堆叠多个任务更好处理等到下一个周期再补即可。要注意的是flock 的锁是和打开的文件描述符绑定的不是和锁文件内容绑定的。所以锁文件本身可以一直存在不必手动清理进程结束时锁自动释放。3.2 日志按天落盘排查问题时不必翻半天脚本里日志文件名用了日期LOG_FILE$LOG_DIR/sync_$(date %Y%m%d).log同一天内的多次执行会写进同一个文件按天滚动。这样你想看“今天同步发生过什么”直接grep 同步 /var/log/rsync_sync/sync_$(date %Y%m%d).log就能拿到当天所有执行结果不需要从几十个带时间戳的文件里拼线索。另外脚本里的log()函数同时把内容输出到终端和日志文件用的是tee -a。这个设计是为了第一次人工调试时能实时看到进度如果日志目录权限有问题脚本会在mkdir -p那一步直接失败不会偷偷把日志丢掉。3.3 SSH 探活前置失败要快速失败脚本在真正调用 rsync 之前先用 SSH 执行了一条远程建目录命令ssh -p $DEST_PORT -o BatchModeyes -o ConnectTimeout10 \ $DEST_USER$DEST_HOST mkdir -p ${DEST_DIR}这一步有两个作用一是确认 SSH 能通、密钥能认证二是确保目标目录存在。如果不做探活直接跑 rsync网络出问题时 rsync 会反复重试白等很久而目标目录不存在时 rsync 也会报一堆看似和同步无关的路径错误。我见过不少同事的 cron 脚本里没有这一步某天目标机重启后 SSH 服务没起来脚本就一直挂在 rsync 的反复重试里直到下一个任务把它挤爆。前置探活 快速失败是长任务脚本的重要习惯。3.4 用退出码表达结果而不是靠肉眼看屏脚本最后根据 rsync 的退出码决定是否让整个脚本失败if rsync ${rsync_args[]}; then log 同步完成 else status$? log 同步失败rsync 退出码${status} exit $status fi这个设计对 cron 和监控系统非常友好。cron 能感知到脚本退出码非零从而触发告警邮件或外部监控程序。如果只是把 rsync 扔在脚本最后不管退出码那么即使同步失败外层任务也可能认为“脚本执行成功”问题就藏在日志里没人发现。4. 生产环境中踩过的 rsync 坑4.1 源目录末尾斜杠多一个少一个结果天差地别这是 rsync 新手必踩的第一个坑而且踩了往往很痛。假设源目录是/data/project目标目录是/data/backup/rsync -avz /data/project /data/backup/最终会生成/data/backup/project/...源目录作为一个整体被塞进目标目录。rsync -avz /data/project/ /data/backup/最终会把/data/project里面的内容直接合并到/data/backup/里不会多一层project目录。脚本里我把SOURCE_DIR/data/www/project/末尾斜杠写死就是为了确保“源目录内容”直接同步到DEST_DIR。如果你希望目标目录里出现源目录本身就必须去掉斜杠。这类问题在 dry-run 阶段一眼就能看出来传输列表里第一行是project/还是./决定了层级。所以每次改动路径后都值得跑一次 dry-run 确认。4.2 权限、属主、软链接与硬链接看起来同步成功其实没同步干净-a参数已经包含权限和属主信息但有个前提运行脚本的用户需要对源文件有足够的读取权限最好有 root 权限去保留属主和属组。如果普通用户运行 rsync同步过去的文件属主会变成当期用户目标机上的文件权限看起来零零散散甚至导致服务起不来。遇到跨 uid/gid 的主机同步比如源机用 A 用户、目标机用 B 用户建议考虑--numeric-ids。这个参数强行按数字 uid/gid 同步而不是按用户名映射避免因为两边/etc/passwd不一致导致属主漂移。软链接也是重灾区。rsync 默认按-l保留符号链接这是对的。但如果你同步的是“相对软链接”链接目标指向的目录在目标机上不存在那同步出来的软链接就是一个断链。这种问题 dry-run 看不到只能在同步后到目标机抽查几个链接试读。硬链接则如前面所说需要额外加-H才能完整保留。4.3 网络闪断时--partial 的价值才真正体现我最早写脚本时没有--partial后来同步一个 20 GB 的数据库备份目录跑到一半网络抖动rsync 退出码 11。第二次执行时它又从头开始传那个大文件等于之前跑的几个小时全白费了。加上--partial之后传输中断时已写入目标机的临时文件会被保留下次 rsync 会做块级别对比只传没传完的部分。当然--partial只是“断点续传”不是“数据校验”。如果你担心网络传输过程中文件静默损坏可以定期加一次-c做全量校验但要注意-c会让 rsync 对所有文件做 checksum 计算CPU 和耗时都会明显上升不适合每次运行都加。4.4 exit code 对照表别让 24 吓到你rsync 的退出码不是只有 0 和 1看到非零别急着慌。下面是我实际工作中最常见的几个退出码含义常见原因与处理0同步成功无需处理11I/O 错误检查网络、磁盘空间、文件系统是否只读12协议错误通常是 rsync 版本差异导致重新安装对齐版本23部分传输同步过程中有文件被修改重跑一次24部分文件消失源文件在同步期间被删除或轮转常见于日志文件确认后忽略我被 24 号退出码坑过一次业务日志按小时轮转凌晨同步时源文件被改名rsync 返回 24我一度怀疑是脚本问题折腾半天才发现这是正常现象。现在的建议是脚本捕获退出码后先记录再由人工结合当天的文件变更判断不要因为看到 24 就立刻重跑任务以免反复同步互相踩踏。5. 接入 cron、监控以及下一步能怎么扩展5.1 cron 里的 PATH、日志与执行频率把脚本放进 crontab 时最容易忽略的是 cron 环境里的 PATH 极短。脚本里我已经用export PATH显式设置好路径否则 cron 里可能找不到rsync或者flock。计划任务示例30 1 * * * /usr/local/bin/rsync_sync.sh /var/log/rsync_sync/cron.out 21这里把 stdout 和 stderr 一起重定向到 cron 日志避免 cron 默认发邮件刷爆邮箱。执行频率我建议从小时级开始观察连续正常跑 24 小时后再改成天级。如果任务本身要跑 40 分钟而你又设置了每小时执行flock 的作用就会立刻体现未完成的下一轮任务自动跳过系统不会出现两个进程同时抢写的情况。5.2 把退出码变成告警信号单纯把脚本加入 cron 还不够退出码非零时最好有外部动作。最原始的方案是依赖 cron 的MAILTO设置好MAILTO环境变量后任务失败时的输出会自动发到邮箱。但很多时候 cron 服务器没有配置邮件服务更通用的是写一个很薄的 wrapper#!/usr/bin/env bash if /usr/local/bin/rsync_sync.sh; then exit 0 else /usr/local/bin/notify.sh rsync 同步失败请登录服务器查看日志 exit 1 finotify.sh可以替换成企业微信机器人、短信网关或任何你手头的告警渠道。关键是把 rsync 脚本的退出码作为唯一判断条件不要让 wrapper 自己额外做判断。5.3 从单向同步走向滚动快照与多目标如果确认单向同步稳定之后你还想保留历史版本可以在脚本外面套一层“滚动快照”思路先复制上一份快照用硬链接方式几乎不占空间再对新快照目录执行 rsync删除旧快照即可。命令思路如下prev_snapshot/backup/snapshot.20250101 new_snapshot/backup/snapshot.20250102 cp -al $prev_snapshot $new_snapshot rsync -av --delete $SOURCE_DIR/ $new_snapshot/因为快照之间通过硬链接共享未变化文件所以空间占用接近增量却保留了一个完整目录结构。叠加这个思路你的同步脚本就从“镜像”升级成了“带版本的历史备份”。多目标同步也很简单把DEST_HOST、DEST_DIR改成数组用 for 循环逐个同步。这样发布时一条命令就能把产物推到多台服务器比手工逐台执行可靠得多。最后分享一个我自己的使用习惯任何对脚本的改动包括换源目录、调整排除规则、切换目标机我都会先跑一次--dry-run并把输出留档然后连续盯三天日志再放开。rsync 最大的优点是可预期最大的风险是你未经演练就相信它。这份脚本不会替你解决所有问题但它会把每一步都留成日志和退出码让你从“半夜爬起来修数据”变成“早上看一眼日志就够了”。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →