资讯详情

Bash 文件系统同步实战:rsync 参数详解与脚本避坑指南

📅 2026/10/15 2:18:29 | 华诺云谱 👁 阅读
Bash 文件系统同步实战:rsync 参数详解与脚本避坑指南
1. 从一次数据错乱说起为什么手动同步迟早会出事前阵子帮一个做视频剪辑的朋友处理过一次数据事故。他手头有两块外置硬盘一块是工作盘一块是备份盘平时靠拖拽复制来同步素材。某天他发现备份盘里有一批素材的修改时间比工作盘还新但内容却是三天前的版本——原因很简单他在两个盘之间来回拷过几次中间有一次拷到一半去接了个电话回来之后凭记忆判断应该拷完了结果漏了一批文件。更麻烦的是他自己也说不清到底哪边是最新的。这件事之后我给他写了一套基于 Bash 的文件系统同步脚本。不是因为他买不起现成的同步工具而是因为他的需求很具体只同步指定目录、要能排除缓存和代理文件、要能先预览再执行、要能留下日志方便回溯。现成工具当然能覆盖大部分场景但当你需要把这些行为固化成一个可重复、可审计、可定时触发的流程时Bash 脚本反而是最直接的选择。这篇文章想聊的就是这件事用 Bash 实现 Linux 文件系统同步。我会从同步这件事的本质讲起把 rsync 这个核心工具的参数逻辑拆开揉碎然后给出一套可以直接抄作业的脚本框架最后重点讲那些只有真正跑过同步任务的人才会踩到的坑——比如尾部斜杠的语义、删除选项的杀伤力、权限与属主的保留、以及如何让同步任务在无人值守的情况下依然可靠。不管你是刚接触 Linux 运维的新手还是已经写过一些脚本但总觉得不够稳的老手应该都能从里面找到点有用的东西。2. 同步不是复制先把同步这两个字想清楚2.1 复制和同步的本质区别很多人第一次写同步脚本写出来的其实是复制脚本。这两者的差别用一句话概括复制是把 A 搬到 B同步是让 B 和 A 保持一致。听起来差不多但落到实现上完全是两回事。复制只关心源目录里有什么把东西搬过去就完事。它不管目标目录里多出来的东西也不管两边同名文件谁新谁旧。而同步要回答三个问题源里有、目标没有的要补上源里改了、目标还是旧的要更新目标里有、源里已经删了的要不要跟着删。第三个问题最要命因为它涉及删除操作一旦判断错了丢的就是数据。所以一个合格的同步脚本核心逻辑不是怎么把文件拷过去而是怎么判断两边到底哪里不一样然后只动该动的地方。这个判断过程专业点叫差异比对是同步的灵魂。2.2 全量同步与增量同步的取舍差异比对有两种思路。一种是全量同步每次把源目录整个重新拷一遍简单粗暴但代价是每次都要读写所有文件。对于几十 GB 的素材库全量同步一次可能要几十分钟而且对磁盘寿命也不友好。另一种是增量同步只传输有变化的部分。这里又分两个层次文件级增量只传内容变了的整个文件块级增量连文件内部只改了一小段都能只传那一小段。rsync 默认是文件级增量加上--delta相关的机制后能做到块级增量。实际选型时我的经验是如果是本地磁盘之间同步文件级增量足够了因为本地读写速度快没必要为了省那点传输量去增加计算开销如果是跨网络同步块级增量的价值就体现出来了尤其是那种大文件只改了几行的情况能省下大量带宽。2.3 单向同步、双向同步与镜像还有一个必须先想清楚的问题同步的方向。单向同步是 A 到 BB 永远向 A 看齐。这是最常见的场景比如工作目录同步到备份目录。双向同步是 A 和 B 互相看齐任何一边的改动都会传到另一边这种场景下冲突处理非常复杂一般不建议用脚本硬做容易出乱子。镜像则是单向同步的极端形式目标目录会被完全改造成和源目录一模一样包括删除多余文件。我个人的建议是能用单向就别用双向。双向同步的冲突判断逻辑谁更新、谁优先、要不要保留两份用 Bash 写起来极其容易出错真需要双向的话优先考虑专门的同步方案而不是自己造轮子。这篇文章后面讲的也全部围绕单向同步展开。3. rsync 参数逐个拆每个选项背后都是一次教训3.1 为什么是 rsync 而不是 cp 或 scpLinux 下能搬文件的命令一抓一大把cp、scp、tar 管道都能干。但做同步rsync 几乎是默认答案原因有三。第一它自带差异比对。rsync 会先扫描源和目标通过文件大小和修改时间默认判断哪些文件需要传只传变化的。cp 没这个能力每次都全量。第二它支持断点续传和部分传输。传输中断后重新跑已经传完的部分不会重来。这对大文件和不稳定环境特别重要。第三它的参数体系能精确控制同步行为——排除规则、删除策略、权限保留、压缩传输全都能配。这种可控性正是脚本化所需要的。scp 虽然也能跨机传文件但它本质是加密的 cp没有增量能力做同步不合适。tar 管道适合一次性打包搬运不适合反复执行的同步任务。3.2 尾部斜杠一个字符决定目录结构这是 rsync 最经典的坑没有之一。rsync -av /data/src /backup/dst rsync -av /data/src/ /backup/dst这两条命令看起来只差一个斜杠结果完全不同。第一条会把src这个目录本身拷进dst结果是/backup/dst/src/...第二条只拷src里面的内容结果是/backup/dst/...。我见过太多人因为这个问题同步完之后发现目标目录里多套了一层同名目录然后手动去挪挪着挪着就把结构搞乱了。记住一个口诀源路径带斜杠同步的是内容不带斜杠同步的是目录本身。目标路径带不带斜杠无所谓rsync 会自动处理。3.3 -a 到底包含了什么-a是归档模式等于-rlptgoD这一串的合集。拆开看-r递归处理子目录同步目录必备-l保留符号链接不跟随链接去拷真实文件-p保留权限-t保留修改时间这个极其关键因为 rsync 默认靠修改时间判断文件是否需要更新时间不保留会导致每次都重传-g保留属组-o保留属主-D保留设备文件和特殊文件日常同步用-a基本够了。但要注意-a里的-o和-g在非 root 用户执行时可能报错因为普通用户没权限改属主属组。这时候要么用 root 跑要么去掉这两个选项用-rltpD代替。3.4 --delete 的杀伤力与安全阀--delete让目标目录删除源目录里已经不存在的文件实现真正的镜像。这个选项威力巨大用错了就是数据灾难。我给自己定的规矩是任何带--delete的同步任务第一次执行前必须先用--dry-run跑一遍看清单。--dry-run会模拟执行把将要进行的操作列出来但不真正动手。看一遍输出确认要删的文件都是该删的再去掉 dry-run 正式跑。更稳妥的做法是配合--delete-after让 rsync 先传完所有新文件再执行删除。默认的--delete-during是边传边删万一传到一半断了目标目录会处于一个既不全新也不全旧的状态。--delete-after虽然慢一点但中间状态更安全。还有一个保护选项--max-deleteN限制单次最多删多少个文件。如果某次同步突然要删几千个文件很可能说明源目录出了问题比如挂载点掉了源目录变空了这个限制能给你一个刹车。3.5 排除规则别把垃圾也同步过去同步时最烦的就是各种缓存、临时文件、版本控制目录被一起搬过去。rsync 的--exclude和--exclude-from就是干这个的。rsync -av --exclude*.tmp --exclude.cache/ /data/src/ /backup/dst/排除规则有几个容易搞混的点。首先模式匹配是相对源路径的--exclude.cache/会排除任何层级下的.cache目录。其次如果只想排除根目录下的某个目录要用/开头比如--exclude/.cache/。第三排除目录时结尾的斜杠建议加上语义更明确。当排除规则多起来写在命令行里就不现实了用--exclude-from文件从文件读取。这个文件里每行一条规则#开头的是注释。我一般会维护一个通用的排除清单包含.git/、node_modules/、__pycache__/、*.swp、.DS_Store这些所有同步任务共用。4. 一套能直接用的同步脚本长什么样4.1 脚本的整体骨架设计先给一个我实际在用的脚本框架然后逐块解释为什么这么写。#!/usr/bin/env bash set -euo pipefail SRC/data/src/ DST/backup/dst/ LOG_DIR/var/log/sync EXCLUDE_FILE/etc/sync/exclude.list LOCK_FILE/var/run/sync.lock DATE$(date %Y%m%d_%H%M%S) LOG_FILE${LOG_DIR}/sync_${DATE}.log mkdir -p $LOG_DIR exec 1 (tee -a $LOG_FILE) exec 21 if ! mkdir $LOCK_FILE 2/dev/null; then echo 已有同步任务在运行退出 exit 1 fi trap rmdir $LOCK_FILE EXIT rsync -av --delete-after \ --exclude-from$EXCLUDE_FILE \ --log-file$LOG_FILE \ $SRC $DST echo 同步完成$(date)这个骨架里有几个关键设计下面逐个说。4.2 set -euo pipefail 到底防住了什么set -euo pipefail这三板斧是 Bash 脚本健壮性的基础。-e让脚本在任何命令返回非零时立即退出。没有它rsync 失败了脚本还会继续往下跑最后打印一句同步完成把你骗得团团转。-u让脚本在引用未定义变量时报错退出。比如你写了个$SRC_DIR但实际变量名是$SRC没有-u的话它会当成空字符串rsync 就变成同步空路径后果不堪设想。-o pipefail让管道中任何一个环节失败都算整体失败。默认情况下管道的返回值只看最后一个命令前面出错会被吞掉。这三个选项加起来能挡掉一大半脚本看起来跑完了但实际啥也没干的诡异情况。4.3 锁文件防止任务重叠的土办法定时任务最容易出的问题就是上一次还没跑完下一次又启动了。两个 rsync 同时操作同一对目录轻则互相干扰重则数据错乱。锁文件的思路很简单脚本开始时尝试创建一个目录用mkdir而不是创建文件因为mkdir是原子操作创建成功说明没有其他实例在跑创建失败说明已经有实例在跑直接退出。脚本结束时用trap保证无论正常退出还是被中断锁都会被清理。这里用mkdir而不是touch创建锁是因为mkdir在目录已存在时会失败天然具备互斥性而touch对已存在的文件不会报错起不到锁的作用。用trap ... EXIT而不是在脚本末尾手动删除是为了保证脚本被 CtrlC 或者被 kill 时锁也能释放否则会留下一个永远删不掉的锁后续任务全部被挡。4.4 日志出问题时唯一能救命的东西同步脚本最怕的就是静默失败——你以为它在正常工作实际上已经好几周没同步成功了。所以日志必须做扎实。上面脚本里用了exec 1 (tee -a $LOG_FILE)把标准输出重定向到日志文件同时还在终端显示。exec 21把标准错误也并进去。这样脚本里所有 echo 和命令输出都会进日志。但光有这些还不够rsync 自己的详细输出也要记下来所以加了--log-file参数。这个参数会让 rsync 把传输详情单独写一份包括每个文件是新增、更新还是删除。出问题时翻这份日志能精确知道哪一步出了岔子。日志文件按时间戳命名避免覆盖。但长期跑下去日志会堆积所以还需要一个清理机制比如只保留最近 30 天的日志。这个可以用find $LOG_DIR -name sync_*.log -mtime 30 -delete放在脚本开头执行。4.5 退出码与告警让失败被看见脚本跑在定时任务里没人盯着终端失败必须能主动通知。最基础的做法是检查 rsync 的退出码非零就发告警。rsync 的退出码有具体含义值得记一下退出码含义0成功1语法或用法错误2协议不兼容3源或目标路径相关错误23部分文件传输失败权限等24源文件在传输中消失30传输超时其中 24 比较特殊通常是因为源文件在扫描后被删了属于可容忍的情况很多脚本会把它当成功处理。其他非零码都应该告警。告警方式看环境可以写系统日志、发邮件、或者往某个监控接口推一条消息。核心是别让失败悄无声息。5. 那些只有真跑过才会遇到的坑5.1 修改时间不一致导致的重复传输rsync 默认靠文件大小 修改时间判断文件是否需要更新。如果目标文件的修改时间和源不一致即使内容完全相同rsync 也会重传。什么情况下时间会不一致最常见的是从不同文件系统之间拷贝比如从 ext4 拷到 NTFS 或者某些网络存储时间精度不一样秒级对不上。还有一种是从压缩包解压出来的文件时间戳是打包时的。解决办法有两个。一是加--checksum让 rsync 用校验和判断内容是否相同而不是看时间。代价是每次都要读所有文件算校验和速度慢很多适合文件不多但时间戳混乱的场景。二是加--size-only只看大小不看时间适合内容变化必然导致大小变化的场景比如日志文件但如果有改了内容但大小没变的情况就会漏掉。我的经验是本地同步优先用默认的时间判断快且准跨文件系统或者时间戳明显混乱时才上--checksum。5.2 权限与属主在跨用户场景下的报错用-a同步时rsync 会尝试保留属主和属组。如果执行同步的用户不是 root遇到属主不是自己的文件就会报错退出码 23。这种情况有两个处理方向。如果确实需要保留属主属组就用 root 执行或者用 sudo 提权。如果不需要保留就把-a拆开去掉-o和-g用-rltpD代替。还有一种情况是目标文件系统不支持某些权限位比如 FAT32 不支持 Linux 的权限模型同步过去权限会丢失。这种属于文件系统限制脚本层面解决不了只能接受或者换文件系统。5.3 大目录扫描慢与 --delete 的扫描开销rsync 在传输前要先扫描源和目标建立文件列表。目录里文件数量一多比如几十万个这个扫描过程本身就很耗时而且吃内存。--delete会显著加重这个开销因为它不仅要扫描源还要完整扫描目标来找出多余的文件。如果目标目录很大光是扫描就可能花几分钟。优化思路有几个。一是用--delete-after而不是默认的--delete-during虽然总时间差不多但传输和删除分开中间状态更清晰。二是如果目录结构稳定可以考虑用--delete-excluded配合精确的排除规则减少需要比对的文件数。三是对于超大目录考虑分而治之把同步任务拆成几个子目录分别跑而不是一个任务扫全部。5.4 符号链接、硬链接与稀疏文件的处理-a里的-l是保留符号链接也就是把链接本身拷过去而不是跟随链接拷真实文件。这通常是想要的但如果你希望同步的是链接指向的实际内容就要用-L跟随链接。这两个选项语义相反别搞混。硬链接默认不会被保留rsync 会把每个硬链接当成独立文件传输导致目标端失去硬链接关系占用额外空间。要保留硬链接关系加-H。但这个选项会显著增加内存和 CPU 开销因为 rsync 要维护链接映射表文件多的时候慎用。稀疏文件比如虚拟机磁盘镜像默认会被展开成普通文件占用大量空间。加-S让 rsync 识别并保留稀疏特性。5.5 传输中断后的续传与一致性rsync 支持断点续传但有个前提目标端已经有一部分文件时它才能基于已有部分继续。如果传输中断后目标文件是残缺的rsync 下次会重新传这个文件除非用了--partial保留残缺文件。--partial让 rsync 保留中断时未传完的文件下次可以接着传。配合--partial-dir可以把残缺文件放到单独目录避免污染目标目录。对于大文件传输这两个选项很实用。但要注意续传的前提是源文件在中断期间没有变化。如果源文件变了续传的部分就对不上了rsync 会检测到并重传。所以续传不是万能的源文件频繁变动的场景下意义不大。6. 让同步任务真正可靠定时、监控与演练6.1 用 systemd timer 还是 cron定时触发同步传统做法是 cron现代做法是 systemd timer。两者都能用但 systemd timer 有几个优势能记录每次执行的详细日志journalctl 直接查、能配置依赖关系、能设置错过执行后的补偿策略、能限制资源使用。一个简单的 systemd timer 配置大概是这样# /etc/systemd/system/sync.service [Unit] Description文件同步任务 [Service] Typeoneshot ExecStart/usr/local/bin/sync.sh# /etc/systemd/system/sync.timer [Unit] Description每小时执行同步 [Timer] OnCalendarhourly Persistenttrue [Install] WantedBytimers.targetPersistenttrue是关键它保证如果某次因为机器关机错过了执行开机后会补跑一次。cron 默认没有这个能力错过就错过了。6.2 同步任务的健康检查怎么做定时任务跑起来之后怎么知道它一直在正常工作光看日志文件在增长不够因为可能一直在报错。我的做法是加一个心跳机制每次同步成功后往一个状态文件里写当前时间戳。然后单独写一个检查脚本读这个时间戳如果距离现在超过预期间隔的两倍就告警。这样即使同步脚本本身挂了检查脚本也能发现异常。另一个做法是记录每次同步的文件数量和传输字节数做成趋势。如果某次突然传输量暴增或暴减往往说明有问题——暴增可能是源目录被误删后重新灌数据暴减可能是源目录挂载失败变成了空目录。6.3 定期做恢复演练比什么都重要最后这条是我最想强调的备份和同步的价值只有在恢复的时候才能验证。同步任务跑得再顺如果从没试过从目标端恢复数据你根本不知道它是不是真的可用。我建议至少每季度做一次恢复演练从备份目录里随机挑几个文件恢复到临时位置和源文件做校验和比对。如果对不上说明同步链路有问题得赶紧查。演练还能暴露一些平时发现不了的问题比如权限没保留导致恢复后文件不可读、符号链接断了、稀疏文件展开后空间不够等等。这些问题在真正需要恢复的紧急时刻才暴露代价就太大了。同步这件事说到底不是技术问题是纪律问题。脚本写得再漂亮不定期检查、不定期演练迟早会出岔子。把同步当成一个需要持续维护的系统来对待而不是一次性配好就不管的工具才是真正靠谱的做法。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑