rsync 的实现原理:它为什么能快速同步两个文件?
在日常开发和运维中,rsync 是一个非常常见的文件同步工具。相比 cp、scp 这类直接复制工具,rsync 最大的特点是:它不一定会把整个文件重新传输,而是尽量只传输变化的部分。
这篇文章会从三个问题入手解释 rsync 的核心原理:
- rsync 如何快速判断两个文件是否需要同步?
- rsync 如何只传输文件中的差异部分?
- rolling checksum 和 strong checksum 是如何协作的?
--
1. rsync 解决的核心问题
假设我们有两个文件:
源端新文件:A
目标端旧文件:B
我们希望把目标端的 B 更新成源端的 A。
最简单的做法是直接把 A 整个传过去,覆盖 B:
scp A remote:/path/B
但是如果文件很大,比如几个 GB,而实际只改了其中几 KB,那么整体传输就很浪费。
rsync 的目标就是:
尽量复用目标端已经存在的数据,只传输真正变化的部分。
这就是 rsync 的核心价值。
2. rsync 默认如何判断文件是否一致?
很多人以为 rsync 默认会对文件内容做 hash 校验,其实不是。
默认情况下,rsync 会使用一种很快的判断方式,通常叫做 quick check。
它主要看两个元信息:
1. 文件大小 size
2. 最后修改时间 mtime
如果源端和目标端的文件大小一致,并且修改时间也一致,rsync 默认会认为这个文件没有变化,不需要同步。
所以执行:
rsync -av src/ dst/
时,大多数未变化文件会被快速跳过。因为读取文件大小和修改时间只需要访问文件元数据,不需要把整个文件读一遍。
这也是 rsync 在同步大量文件时很快的原因之一。
不过,这种判断不是严格的内容校验。如果文件内容被修改了,但文件大小没变,而且修改时间也被人为恢复成原来的值,那么默认 rsync 可能无法发现变化。
如果你希望严格按文件内容判断,可以使用:
rsync -avc src/ dst/
其中 -c 或 --checksum 会让 rsync 计算文件内容的 checksum,再判断文件是否一致。
代价是:两端都要读取完整文件内容,所以会明显增加磁盘 I/O。
简单总结:
默认模式:size + mtime,速度快,但不是严格内容校验
checksum 模式:按内容校验,更可靠,但更慢
3. rsync 真正厉害的地方:差异传输
当 rsync 判断某个文件需要更新时,它并不一定会把整个文件重新传过去。
它会尝试找出:
源端新文件 A 中,哪些内容目标端旧文件 B 已经有了?
哪些内容是目标端没有的,需要真正传输?
这个过程就是 rsync 的 delta-transfer algorithm,也就是差异传输算法。
核心思想可以概括为一句话:
目标端告诉源端:我已有这些数据块;
源端扫描新文件:发现哪些块你已经有了,我就不传,只传你没有的部分。
4. 差异传输的基本流程
假设目标端已有旧文件 B。
第一步,目标端会把旧文件 B 切成固定大小的 block:
B = [B0][B1][B2][B3][B4]...
然后目标端会为每个 block 计算两个 checksum:
B0 -> weak0, strong0
B1 -> weak1, strong1
B2 -> weak2, strong2
...
这里有两种 checksum:
weak checksum:弱校验,也就是 rolling checksum
strong checksum:强校验,用来最终确认块是否真的一致
目标端不会把整个旧文件传给源端,而是只把这些 block 的 checksum 列表发给源端。
接下来,源端拿到这些 checksum 后,开始扫描源端的新文件 A。
5. rolling checksum 是什么?
rolling checksum 可以理解成一种适合“滑动窗口”的弱校验。
普通 hash 的问题是:如果窗口移动 1 个字节,通常要重新计算整个窗口的 hash。
例如窗口大小是 4KB:
窗口 1:[第 0 字节 ... 第 4095 字节]
窗口 2:[第 1 字节 ... 第 4096 字节]
两个窗口只差了一个字节,但普通 hash 通常需要重新读完整个 4KB 来计算。
rolling checksum 的优势是:它可以基于上一个窗口的结果,快速算出下一个窗口的校验值。
可以粗略理解为:
新 checksum = 旧 checksum - 离开窗口的字节 + 进入窗口的字节
真实算法会比这个更复杂,但直觉就是这样。
所以 rolling checksum 非常适合源端扫描新文件:
A = abcdefghijklmnop
[----] 窗口 1
[----] 窗口 2
[----] 窗口 3
窗口每次向后滑动 1 个字节,都可以快速得到新的 weak checksum。
6. strong checksum 是什么?
rolling checksum 虽然快,但它是弱校验,可能发生碰撞。
也就是说,两个不同的数据块,有可能算出相同的 weak checksum。
如果 rsync 只依赖 rolling checksum,就可能把两个不同的数据块误认为相同,导致最终同步出来的文件内容错误。
所以 rsync 还需要 strong checksum。
strong checksum 的作用是:
当 rolling checksum 发现一个“疑似匹配”时,再用 strong checksum 做最终确认。
它比 rolling checksum 更可靠,但计算成本也更高。
因此,rsync 不会对每一个滑动窗口都计算 strong checksum,而是只在 weak checksum 命中时才计算。
7. rolling checksum 和 strong checksum 如何协作?
这部分是 rsync 算法的核心。
目标端旧文件 B 已经被切成多个 block,并且每个 block 都有一对 checksum:
B0 -> weak0, strong0
B1 -> weak1, strong1
B2 -> weak2, strong2
...
源端扫描新文件 A 时,会不断移动一个大小相同的窗口。
对于每个窗口,先计算 rolling checksum:
window = A[pos : pos + block_size]
weak = rolling_checksum(window)
然后拿这个 weak checksum 去目标端发来的 checksum 表中查找。
情况一:weak checksum 没有命中
如果没有命中,说明这个窗口大概率不是目标端已有的任何 block。
于是源端继续向后滑动 1 个字节:
当前位置没有匹配
=> 窗口后移 1 字节
=> 继续计算 rolling checksum
这一步很快,因为 rolling checksum 可以增量计算。
情况二:weak checksum 命中
如果 weak checksum 命中了某个旧 block,比如 B7:
A 的当前窗口 weak checksum == B7 的 weak checksum
这说明当前窗口“可能”和旧文件里的 B7 相同。
但这只是疑似匹配。
接下来,源端会对当前窗口计算 strong checksum:
strong = strong_checksum(A[pos : pos + block_size])
然后和 B7 的 strong checksum 比较:
如果 strong == strong7
=> 基本确认 A 的当前窗口和 B7 是同一段内容
确认匹配之后,源端就不需要发送这段数据本身了。
它只需要告诉目标端:
复制你本地旧文件里的第 7 个 block
也就是发送一条类似这样的指令:
copy block B7
对于无法匹配的内容,源端才会发送原始字节数据,也就是 literal data。
最终,目标端收到的是一串构造指令:
literal "abc"
copy B7
copy B8
literal "xyz"
copy B12
...
目标端根据这些指令,把旧文件已有的 block 和新传输的 literal data 组合起来,重建出新的文件 A。
8. 为什么不只用 strong checksum?
因为源端扫描新文件时,不是只看固定块边界,而是要在新文件的所有可能偏移位置上寻找匹配。
如果文件很大,每个偏移位置都计算 strong checksum,成本会非常高。
rolling checksum 的价值就在于:
它可以非常便宜地扫描大量候选位置。
所以它适合做第一层过滤。
只有当 rolling checksum 命中时,rsync 才会计算 strong checksum。
这样可以避免大量不必要的强校验计算。
9. 为什么不只用 rolling checksum?
因为 rolling checksum 是弱校验,存在碰撞风险。
如果只靠它判断两个 block 是否相同,就可能出现误判。
strong checksum 的作用就是降低误判概率,保证最终重建出来的文件尽可能可靠。
所以二者的分工是:
rolling checksum:负责快速发现疑似匹配
strong checksum:负责确认疑似匹配是否真的成立
可以把它理解成一个两阶段过滤器:
第一阶段:rolling checksum 快速筛选候选块
第二阶段:strong checksum 精确确认候选块
这就是 rsync 能同时做到“快”和“可靠”的原因。
10. 一个简化例子
假设目标端旧文件 B 被切成 4 个 block:
B = [B0][B1][B2][B3]
目标端计算并发送:
B0 -> weak0, strong0
B1 -> weak1, strong1
B2 -> weak2, strong2
B3 -> weak3, strong3
源端新文件 A 内容中,前面插入了一小段新数据,但后面大部分内容和旧文件一样:
A = [新数据][B1][B2][B3]
源端扫描 A 时,会发现:
开头的新数据无法匹配旧 block
=> 需要发送 literal data
后面的窗口匹配到 B1
=> 发送 copy B1
继续匹配到 B2
=> 发送 copy B2
继续匹配到 B3
=> 发送 copy B3
最终源端只需要发送:
literal "新数据"
copy B1
copy B2
copy B3
目标端就可以根据旧文件 B 和这些指令重建出新文件 A。
这就是为什么在大文件只改动一小部分时,rsync 的传输量会远小于直接复制整个文件。
11. 本地判断两个文件是否完全一致,应该用什么?
如果只是想在本机判断两个文件是否完全一致,最直接的方式其实不是 rsync,而是 cmp:
cmp -s file1 file2 && echo same || echo diff
cmp 会按字节比较两个文件。
几种方式的区别:
stat:
只看元数据,例如 size、mtime,最快,但不严格
cmp:
逐字节比较,适合判断两个本地文件是否完全一致
sha256sum:
计算完整 hash,适合生成指纹、跨机器保存和比对
rsync:
适合同步目录或跨机器增量同步
如果只是同步目录:
rsync -av src/ dst/
如果你怀疑文件的修改时间不可靠,希望按内容判断:
rsync -avc src/ dst/
如果只是想预览哪些文件会变化:
rsync -avcni src/ dst/
其中:
-a:归档模式,保留权限、时间等信息
-v:显示详细信息
-c:按 checksum 判断内容变化
-n:dry-run,只预演,不真正修改
-i:输出变更摘要
12. 总结
rsync 的实现原理可以分成两层:
第一层是快速判断文件是否需要处理。
默认情况下,rsync 使用:
文件大小 size + 修改时间 mtime
来快速跳过未变化文件。
第二层是对需要更新的文件做差异传输。
它使用:
rolling checksum + strong checksum
来寻找源端新文件中哪些数据块已经存在于目标端旧文件中。
两者的协作方式是:
rolling checksum 负责快速扫描和发现候选块;
strong checksum 负责确认候选块是否真的相同。
找到匹配块后,源端不需要发送真实数据,只需要发送类似:
copy block N
这样的引用指令。
找不到匹配的部分,才发送原始数据。
因此,rsync 能够在文件变化很小的情况下,大幅减少传输量。
一句话概括:
rsync 默认靠 size + mtime 快速判断文件是否变化;
真正同步变化文件时,靠 rolling checksum 找候选块,靠 strong checksum 确认匹配,只传输目标端缺失的数据。
这就是 rsync 高效同步的核心原理。