一个 40G 的文件,改了一个字节,rsync 会传多少?大多数人脱口而出:一个字节左右。实际答案取决于那一个字节是怎么改进去的——是追加到文件末尾,还是在中间某个位置原地改写,还是整个文件被重新生成了一遍。三种情形落到 rsync 的块校验上,结果从"只传少量块"到"接近全量"都可能出现。
这不是修辞上的绕弯子。rsync 的"增量"从来不是一个无条件成立的承诺,它是一个有前提的算法,前提是新旧两份字节流在大多数位置上还能对齐。对齐成立,节省就成立;对齐被打散,节省就归零。很多人把 rsync 理解成"文件改得少就一定传得少"的工具,这其实是错位——rsync 能省下多少流量,取决于变更的形态,不是变更的数量。
把这个结论反过来用,很多让人困惑的现象就有了统一解释:为什么加了 -a 还是全量传?为什么对日志做增量特别省、对打包文件做增量几乎没有效果?为什么换了一台机器之后同样一条命令表现完全不一样?答案都不在 rsync 命令本身,而在"那份文件的字节是怎么被改出来的"。本文就把这条线捋清楚。
"明明只改了一个字节,怎么又把整个文件传了一遍"——这类抱怨在运维群里几乎是常客。多数人的本能反应是参数没配对,或者 rsync 出了毛病。其实两者都不是。
把这件事拆开看,要分两段:前一段是 rsync 决定"这个文件要不要进传输流程",后一段是决定"进了流程之后要不要走增量算法"。多数人抱怨的"整个重传",发生在后一段——文件确实进了流程,而增量算法在这一次没有省下任何东西。
关键在于 rsync 的粒度。rsync 的增量不是字节级的,是块级的。它不去找"哪几个字节变了",它找的是"旧文件里有哪些块可以原封不动地搬过来用在新文件里"。这两个问题看着差不多,答案却可能在"块边界还能不能对上"这件事上彻底分岔。
举一个能说明问题的例子。在一个 40G 文件的开头插入一个字节,那么从插入点往后,所有内容在文件里的偏移量都往后挪了一位。旧文件按固定大小切出来的那些块,和新文件按同样大小切出来的块,从头到尾几乎都对不上了。这时候 rsync 能找到的可复用块接近于零,传输量就接近全量——哪怕语义上你只"加了一个字节"。
反过来,如果是把文件末尾的那个字节从 0 改成 1,块边界一个都没动,只有末尾那一块的内容变了。rsync 只把那一块送过去,剩下的全部用"引用旧文件的第 N 块"来表示,网络上跑的数据少得可怜。
同一句"改了一个字节",两种截然不同的结果。所以开头那个问题的正确回答是:取决于那一个字节是怎么改进去的。这句话听着别扭,但它就是 rsync 的真实行为。rsync 承诺的是"在块能对齐的前提下只传差异",不是"改动小就传得少"。把这句话记牢,后面每一个参数的取舍都有依据了。
要理解增量什么时候生效,得先看清 rsync 在动手之前做的那次"粗判断"。面对一个文件,rsync 默认不会去读它的内容,而是先比两个属性:文件大小和修改时间(mtime)。两个都对得上,这个文件就被直接跳过,连一个字节的内容都不读。
这就是所谓的 quick check。它的成本极低——只需要 stat 一下,拿两个字段比一比。所以你会发现 rsync 第二次跑同一个目录时往往很快就结束,那份"快"不是因为它聪明地只传了差异,而是因为它压根没进传输流程,整片文件都被跳过了。这份便宜来自 mtime,不是来自增量算法。把这两件事分开看,很多误判就消失了。
只要大小或 mtime 里有任何一个对不上,文件就进入传输流程,块校验这一套才真正开始跑。于是有几种典型情况值得单独拎出来:
其一,mtime 变了但内容其实没变。比如某个程序把文件读出来又原样写回去,或者打包脚本每次都重新生成一份内容完全相同的产物。这时候 rsync 会走完整的块校验流程,两边都读一遍,最后发现所有块都匹配,网上传的数据很少。流量是省了,CPU 和磁盘 I/O 却实打实地花掉了。
其二,内容变了但 mtime 被写回原值。这种情况少见但后果严重——rsync 会认为文件没变,直接跳过,改动就被漏掉了。所以别轻易相信那些"为了保持时间戳整洁"而把 mtime 写回去的脚本。
其三,跨平台时间精度不一致。有的文件系统只记到秒,有的能记到纳秒;有的挂载选项会让时间戳出现偏移。跨系统对拷的时候,秒级精度那一边把小数部分截掉,下一次比对就永远差那么一点点,每个文件都被判成"变了"。表现出来的现象是:明明什么都没改,rsync 每次都把所有文件扫一遍。
针对这三种情况,rsync 给了几个开关:--size-only 只看大小不看时间;--ignore-times(-I)忽略两者强制比对;--checksum(-c)改成用内容校验和来判断。这三个开关各自解决不同的问题,代价也完全不同,后面会单独讲。
文件进了传输流程之后,rsync 的增量算法开始工作。这里有个很多人搞反的点:rsync 是双向都要读文件的——不是"源端读一遍算完推过去"那么简单。
准确的过程是这样的。持有旧文件的那一侧(receiver)先把自己的旧文件按块切开,对每一块算出一对校验值:一个是弱校验(滚动校验,可以被快速地滑动计算),一个是强校验(用于最终确认)。这份校验集被送到持有新文件的那一侧(sender)。
sender 拿到校验集之后,在新文件上从头做滚动扫描:每滑到一个位置就算一次弱校验,去校验集的哈希表里找候选;候选命中了,再比一次强校验确认。确认成功的块就不传了,用一个"引用第 N 块"的标记代替;那些怎么滑都匹配不上的字节,就原样写进差异流。
最终 sender 送出去的是这个差异流,receiver 拿着差异流加上自己手里的旧文件,把新文件重建出来。
为什么要有弱强两级?因为纯靠弱校验会撞车——弱校验位数少, collisions 的概率不低,直接采信可能把内容不同的块当成同一块,重建出来的文件就错了。所以弱校验用来快速定位候选,强校验用来兜底确认。这是两道筛子:头一道求快,后一道求准。
这里有一个非常重要的推论:哪怕最终只传了少量块,两端的磁盘 I/O 都是按整个文件大小计的。receiver 要把旧文件完整读一遍算校验集,sender 要把新文件完整扫一遍做滚动匹配。所谓"省流量",省的是网络那一层;CPU 和磁盘这两笔账,一次都没省。所以当你发现 rsync 跑起来以后两端磁盘都很忙、CPU 也上去了,别奇怪——这正是它在干活的样子。
既然一切都建立在"切块"上,那就必然要问:一块多大?
答案是:块大小由 rsync 依据文件大小动态确定,不是固定值;文件越大,块越大。如果你不想让它自己决定,可以用 --block-size(-B)手工指定一个值覆盖掉。
这个"随文件大小走"的设计是有道理的。如果不管多大的文件都用同一个小块尺寸,那么一个几十 G 的文件会被切成海量个块,校验集本身就大到要先传半天,哈希查找的开销也会压过一切。反过来,如果小文件也用很大的块,那一个 4K 的配置文件就只有一块,改一个字节就得整块重传,增量完全失去意义。
手工指定块大小时,取舍方向是这样的:块越大,块数越少,校验集越小,哈希查找和 CPU 开销越轻,但一个字节的改动会拖累整块重传;块越小,粒度越细,浪费的字节越少,但校验集更大、CPU 更重。所以方向性建议是"改动越分散,就越值得往小了调",但不要无脑调小——校验集本身也要走网络,块切得太碎,光是校验集就可能吃掉你省下来的流量。
还有一点容易被忽略:块大小影响的是"分块的位置",而分块位置是相对于文件开头的绝对偏移。这就解释了为什么头部插入一个字节会毁掉一切——从插入点往后,所有块的起点全部错位,滚动匹配虽然在理论上有能力在错位之后重新找到对齐点(因为它是逐字节滑的,不是按块跳的),但错位的字节本身会作为"未匹配数据"被塞进差异流,尾部那些内容虽然能重新匹配上,可中间那一段的位移代价已经产生了。滚动匹配能救回一部分,但救不回全部。
到这里可以把核心结论摆出来了。同样是"文件变了",落到字节流上只有三种基本形态,而 rsync 对这三种形态的响应天差地别。
形态一:追加写。新内容只出现在文件末尾,前面所有内容一个字节都没动,块边界完全不变。这是 rsync 最爱的形态——前面所有块全部命中,只有末尾新增的那部分(以及最后一个不满块)需要传输。日志文件就是典型:新的记录往后挂,历史部分不动。对这类文件,rsync 的增量效果接近理论上限。
形态二:原地改写。改的是文件中间的一部分,但长度不变,或者只在局部变化。这里要再分两种子情形,差别极大:
等长改写(把某个位置的字节换成同样长度的别的字节)——块边界不变,只有被改动覆盖到的那几个块需要重传。改动集中在一个块里,就只传一个块;改动散落在多个块里,就传多个块。这是最理想的一种"改中间"。
不等长改写(在中间插入或删除若干字节)——从变动点往后,所有内容的偏移全部改变,块边界整体位移。这就是前面说的那个"插入一个字节毁掉 40G"的场景。滚动匹配能在错位之后重新找到对齐,但位移产生的未匹配数据要全部传出去,结果往往接近全量。很多人踩的坑就在这里:他们以为自己是"原地改写",其实程序做的是"读入内存、改一个字段、整体重新写回",写回的字节流长度变了、内容排布也变了。
形态三:整文件重新生成。打包、导出、压缩、转码这类操作,输出的是一个全新的字节流。哪怕源数据只改了一行,生成过程也是从头跑到尾,产物在字节层面跟上一版可能大部分不同,长度也大概率不同。对 rsync 而言,这跟"给你一个完全不相干的新文件"差别不大,传输量接近全量。
所以先判断形态,再谈参数。如果变更形态本身就是"整文件重建",那么无论你怎么调 rsync 参数,都救不回传输量——这时候该改的是生成方式(能不能不重新生成?能不能拆成多个小文件?能不能用支持追加的容器格式?),不是 rsync 命令。
整文件重建里有一个子类特别值得单独说,因为它骗了最多的人:压缩归档。
压缩算法有一个基本性质——雪崩效应。原始内容改一个字节,压缩之后的字节流从那个位置往后,大部分都不一样了,总长度也会跟着变。这是压缩本身的原理决定的,不是哪个实现的毛病:压缩靠的是"用短的编码表示重复出现的模式",一旦输入变了,模式统计变了,后面的编码结果就全变了。
rsync 看到的是压缩之后的字节流,不是你脑子里的那份原始内容。所以会出现一个让人非常困惑的现象:我明明只往归档里加了一个 1K 的小文件,为什么 rsync 把整个 40G 的 tar.gz 重传了一遍?因为这个 tar.gz 从头到尾被重新压缩过,字节流大半不同,块对齐基本失效。
同样的道理适用于加密数据、已经压过的图片视频格式、以及任何"输出对输入变化高度敏感"的编码格式。
那怎么办?方向是说清楚的:能传目录就别先打包。一个未压缩的目录树,里面绝大多数文件根本没变,rsync 靠 quick check 就把它们跳过了,真正变的只有那一个文件——这是完全不同的量级。打包这一步把"只有一处变化"的信息彻底抹平了。
当然不是所有场景都能避免打包:海量小文件的情况下,遍历和握手的开销会反过来压过一切,这时候打包是有道理的。但你要清楚自己在换什么——你用小文件的遍历开销,换走了增量的有效性。这笔账要在动手之前算明白,而不是传完之后对着进度条发愣。
顺便说一句:面对已经压缩过的数据,rsync 的 -z 也帮不上忙。-z 是在传输时对流再做一次压缩,对已经是压缩态的数据几乎压不动,只会白白烧 CPU。这一点下面单独展开。
把三种形态套到具体文件类型上,判断就有抓手了。
日志类文件:典型追加写,rsync 最友好。新的记录往末尾挂,前面内容不动,增量效果最好,--append 这类参数也正是在这个前提下才有意义。但有两个陷阱要避开。一个是日志轮转:很多日志方案做的是"重命名旧文件 + 新建同名新文件",重命名之后,目标端出现的是一个从没见过的新文件名,头一次同步必然是全量;另一种做法是 copytruncate(复制一份之后把原文件截断),截断意味着文件长度归零重写,对 rsync 来说这是天翻地覆的变化,接近全量。另一个陷阱是时间戳:如果轮转后新文件的 mtime 比目标端的旧文件还早,在某些比对规则下会被判成"目标端更新"而跳过,改动就漏了。
数据库文件:以原地改写为主,但有几个例外会把它推向整文件重建。常规的行级写入落到数据文件上,通常是页级别的等长改写,块边界不变,rsync 的表现不错。但数据库的维护操作会改变这一点:整理碎片、重建表、收缩空间、批量更新导致的页分裂与重组,这些动作会大幅度改写文件内部的排布,传输量随之上升。更要紧的是另一个问题——在数据库持续写入的时候去同步它的数据文件,拿到的很可能是一份内部不一致的副本:某些页是新版本,某些页还是旧版本,拷到对端可能根本起不来。所以正确的做法是用数据库自身的一致性导出或快照机制先生成一份静止的副本,再去同步这份副本。而一旦走了导出这条路,产物就是"整文件重建",增量又没了。这就是大型库的真实困境,没有两全的解法:要么接受全量,要么把导出粒度做细。
虚拟机镜像:取决于格式。裸格式(raw)的镜像接近原地改写,客户机内部改了哪些块,镜像文件的对应位置就改哪些块,rsync 能吃到增量。带分配表的格式(qcow2 之类)除了数据区还有内部元数据,一次写入可能同时改动数据区和元数据区,且分配策略会让同一份逻辑内容落到镜像文件的不同位置,增量效果打折。还有一个很少被提到的特性:客户机内部删掉一个大文件,镜像文件通常不会随之变小——那段空间在客户机文件系统里被标记为空闲,在宿主机看来镜像文件的大小纹丝不动。所以镜像文件往往只增不减。
把这三种类型放在一起看,判断规则很朴素:先问这个文件是被"追加"的、被"就地改"的,还是被"重新生成"的。答案出来之前,任何参数调整都是在碰运气。
把上面这些形态和对应的参数选择并排摆出来,判断就有据可依了:
| 变更形态 | rsync 实际传输量级别 | 该配的参数 | 为什么 |
|---|---|---|---|
| 追加写(日志类) | 只传少量块,接近理论下限 | 默认即可;确认只追加时可用 --append 或 --append-verify | 前面所有内容字节不动、块边界不变,旧块全部命中;只有末尾新增部分需要传输 |
| 原地改写(数据库、镜像) | 取决于块是否对齐:等长改写只传少量块,不等长的插入删除会接近全量 | 保持默认增量算法,不要用 --whole-file;改动分散时可考虑调小块大小 | 等长改写不改变后续内容的偏移,块边界保持不动;一旦长度发生变化,后面所有块的起点整体位移 |
| 整文件重新生成(打包、导出、压缩归档) | 接近全量,参数层面救不回来 | 调参数意义不大,应改生成方式(传目录而非传包、拆小导出粒度);链路上可考虑 -z 与断点续传 | 压缩与编码存在雪崩效应,源数据小改动会让产物字节流大范围不同,块对齐失效 |
| 目标端不存在该文件 | 接近全量,没有例外 | 增量算法无从生效,只能在 -z、--partial、限速等传输层参数上做取舍 | delta 算法的前提是两端各有一份可比对的字节流,缺一份就退化成普通拷贝 |
| 跨平台时间精度不一致 | 传输量未必大,但每次都会把所有文件拉进比对流程 | 优先统一时钟与挂载选项;确实无法保证 mtime 可信时再用 --checksum | mtime 快检失效,文件被反复判成"已变更",代价是两端都要完整读一遍 |
讲了一整篇怎么让增量生效,这里要拐个弯:有些场合,主动关掉增量才是对的。
--whole-file(-W)的作用就是关掉 delta 算法,整份文件直接传。听起来是在开倒车,但算一笔账就清楚了:增量算法省的是网络流量,花的是两端的 CPU 和磁盘 I/O(前面说过,两边都得把文件完整读一遍)。那么在什么情况下这笔交换不划算?答案是当网络不是瓶颈的时候。
局域网内、同机房内网的机器之间、同一台机器的两块本地盘之间——这些场景下链路宽得很,把 40G 全传过去可能比"两端各读 40G 算校验再传几 M"还要快。多花的那点传输时间,换回的是省下的 CPU 和磁盘压力,非常划算。
还有一个很多人不知道的细节:当源路径和目标路径都是本地路径时,rsync 本身就会默认关闭 delta 算法,因为这种情况下两端其实在同一台机器上,读两遍再传一份 delta 毫无意义。所以本地对拷不用特意去加 -W,它本来就是这么干的。真正需要显式加 -W 的场景是:一端是远程、但链路足够宽(比如通过 rsync 协议或远程 shell 走内网),你判断 delta 的开销大于收益。
反过来,跨公网、跨地域、链路明显是瓶颈的场景,千万不要加 -W——那等于把 rsync 唯一的价值亲手扔掉。
-z 是另一个被用反的参数。它的作用是在传输过程中对流做压缩(zlib),注意这是传输层的压缩,跟"增量算法省下的流量"是两码事,别混为一谈。
判断要不要开 -z,看两件事。
头一件:数据本身压得动吗?文本、日志、SQL 导出、源代码、JSON 这类冗余度高的数据,压缩收益明显;已经是压缩态的归档(tar.gz、zip)、图片、音视频、加密流,压了也白压,收益接近于零,只有 CPU 在烧。所以"我在传一个 .tar.gz,加个 -z 会不会更快"这个问题的答案通常是不会,反而更慢。
后一件:链路和 CPU 谁先到瓶颈?链路窄、延迟高、跨公网跨地域的场合,用 CPU 换带宽是划算的;链路已经很宽(局域网、内网)的场合,压缩本身反而成了新的瓶颈——数据在两端各多出一道压缩解压的工序,吞吐被 CPU 卡住。所以同一个 -z,在慢链路上是加速器,在快链路上是减速带。
还有一层要提醒:压缩是在"要传输的数据"上做的,它压不掉那些被跳过的文件。如果你面对的是一整棵目录树里绝大多数文件都没变的情况,真正省时间的是让 quick check 把那些文件跳过去,而不是给少数几个要传的文件套一层压缩。
真要做跨机房的大文件搬迁,出网带宽这一层的口径要在下单之前确认清楚——是独享还是共享、是否另有月度流量上限、超出之后的处理方式,这几项直接决定这次传输要跑多久、要不要分批次错峰。一万网络深耕 IDC 19 年(成立于 2007 年),在这类问题上能给到的参考主要集中在机器与端口带宽这一层,其余属于下单前需要向官方确认的内容。
--checksum(-c)是最容易被误用的参数之一。它的作用是:放弃"大小 + mtime"这套便宜的快检,改成用内容校验和来决定文件要不要传。
它能解决什么问题?主要三类。一是 mtime 不可信——程序把时间写回原值、时钟不同步、挂载选项导致时间戳偏移;二是跨平台精度不一致——一边记到秒,一边记到纳秒,秒级那边把小数截掉之后永远对不上;三是目标端的 mtime 比源端还新,导致文件被判成"已经是最新的"而跳过。
它的代价也很实在:哪怕两份文件一个字节都不差,两端也要各自把文件完整读一遍算校验和。对一个 40G 的文件来说,这是两份 40G 的磁盘读取,换来的结论是"不用传"。日常反复跑的任务如果都套上 -c,磁盘 I/O 会长期居高不下,这在机械盘或者 I/O 已经吃紧的机器上尤其明显。
更要紧的是要认清它的边界:--checksum 只影响"要不要传"这个判断,不影响"传的时候用不用增量算法"。很多人以为加了 -c 就能省流量,这是反的。-c 是让"该传的文件一个都不漏",不是让"传的时候更省"。如果你面对的问题是"为什么这个改动没同步过去",-c 可能是对的;如果你面对的问题是"为什么传了这么多",-c 帮不上忙,甚至因为跳过率下降而传得更多。
所以使用顺序应该是:先排查 mtime 为什么会不可信——时钟同步做了没有、挂载选项对不对、是不是有脚本在改时间戳。能修根因就修根因,修不了再长期依赖 -c。
这一节讲的是磁盘空间,也是最容易在半夜出问题的一环。
rsync 的默认行为是:在目标端先建一个临时文件(名字通常是目标名加一个随机后缀),把新内容全部写进这个临时文件,写完之后再用 rename 原子地替换掉目标文件。这个设计的好处很明确——传输中途失败,目标端留下的是完整的旧文件,不是一个改了一半的坏文件;而且 rename 是原子操作,任何时刻去读这个路径,读到的要么是完整的旧版本,要么是完整的新版本。
代价是空间。临时文件是按新文件的完整大小增长的,在它写完并替换成功之前,磁盘上同时存在旧文件和一份新文件的完整副本。也就是说,目标盘需要的余量大致是"旧文件 + 新文件"的量级。对一个 40G 的文件来说,这意味着目标分区上要多准备 40G 左右的空闲空间。很多人只看"新文件多大",忘了旧文件还占着,结果传到一半磁盘满了,任务失败,还得清理重来。
--inplace 就是为这种情况准备的:它不做临时文件,直接在目标文件上就地改写。空间占用大幅下降——大体只需要容纳变化的部分。但代价同样明确:传输中断会就地留下一个改了一半的文件,这个文件的状态既不是旧版本也不是新版本,如果它有下游消费者,读到的就是一份坏数据。另外,如果目标文件正被别的进程打开读取,就地改写会让那个进程读到中间态。
所以取舍很清楚:在乎"目标路径任何时刻都是完整可用的",就不要用 --inplace,老老实实准备双份空间;在乎空间、并且能接受中断之后重跑(或者有下游校验机制兜底),才考虑 --inplace。
跟这个话题相关的还有 --partial:默认情况下传输中断后 rsync 会删掉那个半成品的临时文件,下次从头再来;加了 --partial 就保留它,下一次可以从断点继续。--partial-dir 更进一步,把半成品放到单独的目录里,避免污染目标目录。这几个参数跟 --inplace 解决的是不同层面的问题,可以配合着用,但别指望它们替你解决空间问题。
--delete 是 rsync 里唯一一个会造成不可逆损失的参数,值得单独拎出来讲。
它的语义是:让目标端删掉那些源端已经没有的文件,保证两边严格一致。这个功能本身很有用,风险在于它的判断完全建立在"源端此刻的样子"上——如果源端当时不完整,rsync 会非常忠诚地把目标端删成同样不完整。
几种常见的翻车方式:源路径写错了,指向了一个空目录或者还没挂载的挂载点;源目录所在的磁盘没挂载成功,路径存在但是空的;源端是某个筛选之后的视图,被筛掉的那些文件在源端"不存在"。这几种情况下,rsync 认为这些文件在源端没有了,于是把目标端的对应文件全部删掉。删完之后才发现源端其实是有内容的,但目标端那份已经被清掉了。
所以规矩是硬的:任何带 --delete 的命令,正式执行之前必须先跑一遍 --dry-run(-n)配 -v,或者用 --itemize-changes 看清楚清单。这个动作的成本几乎为零,它不传数据,只把"如果真跑会发生什么"列出来。你要在这份清单里重点看两类行:会被删除的那些(在 -i 的输出里以 *deleted 标记),以及传输方向是不是你预期的。
另外几个能降低风险的做法:加 --max-delete 给删除数量设一个上限,超出就报错退出,相当于一根保险丝;用 --delete-after(先传完再删)而不是传的过程中就删,这样即便传输中途断了,目标端的旧数据还在,不至于两头落空;确认源端确实是完整的再动手,比如先检查源目录的文件数量跟预期是否一致。
至于误删之后能不能救回来——rsync 没有回收站,删掉就是删掉了。能不能恢复取决于你有没有别的副本、有没有文件系统层面的快照,跟 rsync 本身没关系。这也是为什么前面那句"先跑 dry-run"要当成硬性纪律而不是建议。
--link-dest 是一个能把空间账算得很漂亮的参数,但它有严格的使用前提。
它的作用是:同步的时候指定一个参照目录,凡是跟参照目录里内容相同的文件,就不真的复制,而是在目标目录里建一个指向参照目录那个文件的硬链接。效果上,每一次同步产出的都是一个"看起来完整的目录树",但实际占用的新增空间只有这次真正变化了的那部分文件。做多版本留存的时候,这个差别是数量级的。
前提有三个,少一个就会出问题。
前提一:必须在同一个文件系统内。硬链接不能跨文件系统,参照目录和目标目录不在同一个挂载点上时,硬链接建不起来,rsync 会退化成老老实实复制一份,空间账立刻回到原点。
前提二:判断依据同样是"大小 + mtime"。跟前面讲的 quick check 是同一套逻辑,所以 mtime 不可信的场景在这里同样会出问题——文件明明没变,却因为时间戳对不上被判成"变了",于是真的复制了一份。需要的话同样可以加 -c,代价同样是两边都要读一遍。
前提三:被硬链接共享的文件不能被就地修改。这是最容易踩的一条。多个目录里的路径指向同一个 inode,一旦有进程"打开原文件写入"而不是"写新文件再替换",改的是那个共享的 inode,所有指向它的历史版本会同时被改掉——历史快照当场失效。所以在使用 --link-dest 的场景里,一般不要用 --inplace(就地改写正是破坏共享的典型动作);同理,下游程序如果会就地改写这些文件,也会毁掉整条快照链。
把这三条前提检查一遍再决定用不用。用对了,空间省得非常可观;用错了,你以为自己有一串历史版本,实际上它们指向的是同一份正在被改写的数据。
-a 被当成"全都保留"来用,这是个普遍误解。它实际等于 -rlptgoD 这一串:递归、保留符号链接、保留权限、保留时间、保留属组、保留属主、保留设备文件与特殊文件。就这些,没有更多。
下面这些东西都不在 -a 里,要单独加:
-H 保留硬链接。不加的话,同一个 inode 的多个硬链接会被当成多个独立文件各传一份,既浪费传输量,又破坏了链接关系——对端拿到的目录树里,本该是同一个文件的几个路径变成了互不相干的副本。
-S 处理稀疏文件。稀疏文件里的空洞在文件系统上不占实际块,但不加 -S 的话,rsync 会把空洞展开成真实的零字节写出去,对端的磁盘占用可能暴涨。虚拟机的裸格式镜像、某些预分配的数据文件常有这种结构,这条很容易中招。
-A 保留 ACL,-X 保留扩展属性。权限体系里除了基本的属主属组读写执行之外的那些细节都在这些地方。某些安全上下文是存在扩展属性里的,丢了之后服务行为会变得莫名其妙,而排查时很难想到是同步环节出的问题。
--numeric-ids。不加的话,用户和组是按名字映射的,两端名字不一致或者对端根本没有这个用户时,属主会变成数字或者干脆错配。跨机器同步时这个几乎总是该加的。
还有一条关于权限的现实约束:-a 里的 -o 和 -g(保留属主和属组)需要目标端有足够的权限才能生效。非 root 用户跑 rsync 时这两个会静默失效,表现出来就是"加了 -a 怎么属主还是变了"。要么用有权限的账号跑,要么接受这个限制。
说白了:-a 是一份"常见组合"的快捷方式,不是"全部"的保证。同步之前先想清楚你要保留的属性清单里有什么,再对照着把缺的参数补上,比对着一个 -a 使劲要可靠得多。
我已经加了 -a,为什么还是全量传?-a 里没有任何一项能改变增量算法生效的条件,它管的是属性保留。全量传通常有三个原因:目标端压根没有这个文件(没有可比对的旧版本);mtime 变了导致文件进了流程,而块对齐又失效(比如文件是被重新生成的、或者中间插入删除了字节);或者链路/命令里实际上等于启用了 --whole-file(源和目标都是本地路径时 rsync 本来就会这么做)。对着这三条挨个排除,比反复加参数有用。
mtime 对不上,是不是必须上 --checksum?不一定,而且不建议一上来就用。先查根因:时钟同步有没有做、挂载选项有没有让时间戳偏移、有没有脚本在改时间戳、两边文件系统的时间精度是不是一致。能修根因就修根因。--checksum 是兜底手段,它的代价是两端都要把文件完整读一遍——对大文件来说这笔 I/O 很实在。只有在确实无法保证 mtime 可信、又必须保证改动不被漏掉的场景,才把它作为长期方案。
跨机房传输要不要开 -z?看两件事。数据压得动吗——文本、日志、SQL 导出这类冗余高的数据收益明显,已经压过的归档、图片、音视频、加密流几乎没收益,只烧 CPU。链路是不是瓶颈——链路窄、延迟高时,用 CPU 换带宽划算;局域网和内网这种宽链路上,压缩反而会成为新的瓶颈。两条都答"是",才开。
--delete 误删了能不能救回来?rsync 没有回收站,删掉就是删掉了,能不能恢复取决于你有没有别的副本或者文件系统层面的快照。所以重点在事前:带 --delete 的命令正式执行前必须先跑 -n(--dry-run)配合 -v 或 --itemize-changes 看清单,重点看会被删的那些行;再加 --max-delete 设一道删除数量上限当保险丝;用 --delete-after 保证先传完再删。这几步加起来成本很低,是唯一便宜的保险。
目标盘要留多大空间?默认方式下,rsync 会先在目标端写一个完整的临时文件,写完后才替换目标文件,所以在替换完成之前,磁盘上同时存在旧文件和新文件的完整副本——需要的余量大致是两者之和。空间紧张时用 --inplace 可以大幅降低占用,但代价是传输中断会就地留下一个改了一半的文件,且正在读取该文件的进程可能读到中间态。要空间还是要原子性,选一个。
大文件传到一半断了,要不要从头再来?默认是要的——中断后 rsync 会删掉那个半成品,下次重新传。想续传就加 --partial 保留半成品,下次从断点继续;--partial-dir 可以把半成品放到单独目录,避免污染目标目录。注意别乱用 --append:它假定"文件前面部分完全一致,只在末尾追加",只有纯追加场景(比如日志)才成立,用在别的地方会产出一个前旧后新、内部不一致的错误文件。--append-verify 会在末尾做一次校验,比 --append 稳,代价是要读一遍。
有没有办法先看看会动哪些文件,再决定执行不执行?有,就是这个动作本身:加 -n(--dry-run)配合 -v,或者用 --itemize-changes(-i)看出结构化的变更清单。-i 的输出里,每一行的开头会标明文件类型和变更方向,被删除的项以 *deleted 标记。这条命令不传任何数据,只做模拟,成本可以忽略。凡是涉及 --delete、涉及大量文件、或者对源端完整性没有十足把握的场景,都该先跑一遍。
本文关于 rsync 增量算法成立条件的描述——包括"先比大小与 mtime 再决定是否进入块校验"的快检机制、"接收端切块并生成弱校验与强校验集、发送端滚动匹配并生成差异流"的算法流程、"块大小由 rsync 依据文件大小动态确定而非固定值"以及各参数(--whole-file、-z、--checksum、--inplace、--delete、--link-dest、-a 及各扩展属性参数)的语义与取舍——依据的是 rsync 官方项目站点 rsync.samba.org 提供的文档与其手册页(man page)中对算法与各选项的公开说明,以及 rsync 算法原始技术报告中对滚动校验与强弱两级校验设计的论述。文中未引用任何具体环境的运行数据,全部结论均为对算法行为的定性分析。
涉及机器、端口带宽与跨机房传输的口径,请以官网实时展示为准:https://www.idc10000.net/ 。页面未明示的内容(出网带宽的独享或共享口径、是否设有月度流量上限、超出后的处理方式等),本文一律标注为需下单前向官方确认,未作任何断言。
上一篇:2026 服务器租用长期指标库 Mimir 落地全解:对象存储、租户隔离与查询路径六维对比 + 避坑避雷手册
下一篇:2026 服务器租用日志管道 Logstash 落地全解:队列背压、grok 开销与持久化六维对比 + 避坑避雷手册
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品