rsync 常被概括成一句话,说它“只发送差异部分”。这句话成立,却还太松。真正让 rsync 长久有生命力的东西,被这句口号盖住了。它的真正形状,是一台分成三层的窄机器:先做文件列表比较,尽量跳过不用动的文件;再对真正需要更新的文件做块级匹配,尽量复用目标端已经存在的内容;最后再用一条进程流水线,把文件选择、差分构造与写入重叠起来。[1][2][3]

rsync 官方文档到今天仍然把读者引回最早那份技术报告和后来的实现概览,这本身就是一个信号。[4] 这个工具之所以撑到现在,靠的并非一套时髦传输层,也并非一层特别圆润的交互界面,靠的是它一直诚实地面对带宽、时延和磁盘 I/O 这几件事。顺着这个角度去读,rsync 许多乍看古怪的行为,就会一点点归位。

配图说明:题图没有用终端截图,改选 Andrew Tridgell 的真实肖像照片。这一选择更贴合本文重心,因为这里要讲的并非某个命令示例,更在于 rsync 最初那套设计逻辑。真正的故事落在架构上:廉价的文件分流、滚动块搜索,以及一条专门为慢链路保持效率而搭起来的流水线。[5]

第一层过滤器,并非滚动校验和

很多人提到 rsync 时,会下意识觉得它默认就会对每个文件做滚动校验和扫描。事情并非这样。项目自己的实现概览写得很清楚,generator 进程会先把共享文件列表与本地目录树对照起来,逐个检查文件能不能直接跳过。[2] 在最常见的模式里,只要修改时间和大小都没有变化,文件就会被跳过;只有在显式指定 --checksum 时,rsync 才会在传输前做文件级校验和比较。[2][3]

这一层非常重要,因为它让昂贵的部分始终并非常态。文件列表本身已经带着路径、属主、模式、权限、大小和修改时间;若调用者明确要求,也可以附带校验和。[2] 对于一棵大目录树来说,只要大部分文件没有变化,rsync 的第一重胜利其实来自一个更朴素的判断:这些文件根本不用进入块级匹配阶段。

man page 在这里划出了一条很有用的边界。它把传输前“这个文件需不需要更新”的判断,与传输后“重建出来的文件是否正确”的校验明确分开。[3] 两者很容易在口头解释里被混成一件事,实际上解决的是两类问题。前者关心避免多余工作,后者关心工作已经发生之后,结果是否正确。

也正因此,rsync 比它的名声显得更不神秘。很多时候它首先是一台认真处理文件列表的机器,然后才是一台差分机器。滚动校验和负责出名,跳过逻辑负责完成大量日常劳动。

第二层才是搜索问题,而且它是对着 basis file 展开的

一旦某个文件不能被跳过,rsync 就会切换工作方式。目标端已经存在的那份副本,会成为这次传输的 basis file,接收端会为这份 basis file 生成块校验和,再把这些校验和发给发送端。[2] 到了这一步,架构就进入 1996 年技术报告描述的那套模式:面对一条低带宽、高时延、双向通信链路,在不把两份文件放到同一台机器上的前提下,找出源文件里哪些部分与目标文件已经相同,只把无法匹配的那一部分送过去。[1]

技术报告里最关键的一步,是把匹配拆成廉价的弱校验和与更昂贵的强校验和。[1] 弱校验和之所以叫“滚动”,就在于发送端可以在源文件上按字节滑动时,快速更新当前块的校验值,而不需要每前进一步就整块重算一次。[1] 官方那篇实践概览把这个过程说得更像实现:为当前位置生成一个校验值,去 generator 发来的那组块校验和里查找;若没有命中,就把当前无法匹配的字节追加进 literal data,再向前移动一个字节继续查。[2]

当弱校验和命中之后,rsync 也不会立刻相信它。技术报告说明,强校验和只在候选命中时才会被计算,因为它更贵,应该留给廉价过滤器已经说出“这里也许有戏”的地方。[1] 因而,发送端真正做的是一层层筛选:先用滚动校验和当成快速移动的索引,再用强校验和剔除误报,最后把新文件描述成两部分的组合,一部分是无法复用的 literal data,一部分是对 basis file 已有块的引用。[1][2]

这套设计会自然推出两件事。第一,文件越相似,rsync 越能发挥力量,因为可复用块越多,这种搜索越值得。[1] 第二,rsync 和今天那类内容寻址对象存储的局部缩小版属于两种东西。它做的是针对某一份现成 basis file 的局部复用。目标端那份旧副本,既是传输终点,也是算法本身的一部分。

第三层常被忘掉,而它恰好是 rsync 最像系统设计的地方

最早那份技术报告里,pipelining 只占很短一节,实践概览却把实现上的后果说得更直观。[1][2] 当文件列表在两端共享之后,rsync 会沿着这样一条流水线工作:

generator -> sender -> receiver

generator 负责文件级逻辑并决定哪些文件可以跳过;sender 读取文件索引与块校验和集合,构造差分数据流并继续往后推;receiver 则把更新写入磁盘。[2] 这几个进程彼此独立运行,真正让它们停下来的,只会是 CPU、磁盘,或者流水线本身的阻塞。[2]

如果脑子里只有“运行一次 rsync,得到一份拷贝”这类图景,就很容易把这一层忽略掉。可从架构角度看,rsync 并非一个在所有步骤之间来回切换的单进程。它是一股被分段安排的流,让文件判断、块匹配与落盘能够重叠起来。技术报告对这种安排的收益说得很直接:当要复制的不止一个文件时,如果一边有进程在发送校验和,另一边同时有进程在接收并重建差异内容,链路的双向利用率就会大幅提高,时延也会被更好地摊开。[1]

这一点也解释了,为什么 rsync 在真实链路里经常比简化版介绍更可靠。算法当然重要,进程模型也同样重要。即便块匹配方法再漂亮,如果所有阶段都必须一个彻底做完,下一个才能启动,整体表现仍会打折。rsync 的一部分性格,正是来自它拒绝把所有工作彻底串行化。

传输边界远比“SSH 还是 daemon”更具体

man page 把远程传输分成两类:通过远程 shell,例如 SSH;或者直接连到 rsync daemon,通常是 TCP 873 端口。[3] 这看上去像一个非常简单的二选一,真正有意思的地方则在于,把它读成一条架构边界会更清楚。

远程 shell 模式,把 rsync 放成另一个安全连接内部被拉起的端点进程。daemon 模式,则把 rsync 变成带有命名模块与 socket 级协议行为的服务。[2][3] man page 还特意说明,“server”这个词不等于 daemon,因为远端完全会只是一个由 shell transport 拉起的进程。[3] 实践概览又补了一层很细却很重要的理解:在远程 shell 会话里,对 rsync 本身来说,面对的是一对管道,网络这一层被藏在更外侧了。[2]

这条分界线说明了 rsync 真正愿意接手什么。它愿意接手文件列表、basis file 匹配与重建,却不坚持接手周边的信任模型和访问模型。若你希望 shell 与主机认证仍由外层系统负责,就用 SSH;若你需要 rsync 原生的模块和服务行为,就用 daemon。两者之间甚至还有混合路径,即通过 remote shell 启动一个单次使用的 daemon 风格服务器进程。[3]

同样因为这种克制,块级差分并非任何时候都会上场。man page 明说,--whole-file 会关闭 delta-transfer algorithm,而在链路带宽高于磁盘带宽时,这么做反而会更快,尤其当所谓“磁盘”本身又是一层网络存储的时候。[3] 这也说明 rsync 对自己的块匹配算法保持克制,只在架构判断认为值得的时候使用它。

rsync 今天仍然在教人的,其实是怎样缩窄问题

rsync 最强的一课,超过了“滚动校验和很聪明”这层意思。它更像一堂关于问题缩窄方式的课。先决定哪些文件值得被注意;再围绕一份确定存在的 basis file 做局部复用;然后把工作排成流水线,尽量减少 CPU、磁盘和链路彼此等待的时间。[1][2][3]

这也是为什么到了 2026 年,rsync 仍然像一件认真的系统设计作品。它之所以可靠,原因在于它很清楚自己的力气该花在哪里,也很清楚何时应该避免工作。一旦把这些边界看清,rsync 就不会再像一条莫名存活下来的旧 Unix 咒语。它更像一台分阶段推进、直到今天仍然知道何时该少做一点的机器。

来源

  1. Andrew Tridgell 与 Paul Mackerras,《The rsync algorithm》:最初的技术报告,说明了 basis file 匹配、滚动校验和、强校验和,以及面向低带宽高时延链路的流水线。
  2. Joe Mack,《How Rsync Works》:官方实践概览,解释 rsync 的角色划分、文件列表构建、generator/sender/receiver 流水线、basis file 与块校验和流向。
  3. rsync(1) 手册页:quick check 行为、remote shell 与 daemon 两种传输方式,以及 --whole-file 这条边界。
  4. rsync 官方文档索引:项目自己给出的入口页,集中链接 man page、技术报告、论文与实现概览。
  5. 本文题图所用肖像照片的 Wikimedia Commons 文件页《File:Andrew Tridgell.jpg》。