一、前言

Rsync 是一个在 Linux 平台及类 Unix 平台下非常有名的应用工具,其所使用的差异数据算法是其核心。有的将 Rsync 的同步算法移为增量同步,其实这是非常不准确的。如果了解了 Rsync 的同步算法,你就不会同意这样的提法。

在 Andrew Tridgell 的博士论文中所提到的,Rsync 算法的初衷是要解决其本身的在代码上传过程当中的问题(其实很多的很有价值的发明、发现以及理论,都是这些大牛们的博士论文)。当时的情况是,其上网还是使用的几十K 的拨号线路,而他每次上传的的代码,其实只做了一点点更改。在当时的情况下,他还得将所有更改的文件重新上传一遍。因此,在这样的情况下他就考虑采用某种算法来解决只传送更改部份的数据的问题。因此也就有了后来的大名鼎鼎的 rsync 工具的产生。最终他的博士论文的名为《Efficient Algorithms for Sorting and Synchronization》。

虽然几十年过去了,当年的网络环境已经不存在了,现在的网络带宽与原来的拨号带宽已经不可同日而语。Rsync 背后的算法似乎所起的作用不那么明显了。但是新的应用的出现,像云文档同步(Dropbox,秒传等),重复数据删除等,又给了 Rsync 第二次开花结果的机会。

二、算法原理

Rsync 背后的算法其实很简单,就是要找出源文件与目标文件之间的不同之处,并将不同之处的数据(也就是差异数据)传送给目标文件。其步骤如下:

1. 计算目标文件固定块长的数据块的快慢 Hash 值。所谓快 Hash 指只有 4 个字节的摘要(有的文档称为弱 Hash,或者 Weak Digest),慢 Hash 是利用 md4,md5,sha1,sha2 等算法计算的摘要,其计算速度远慢于快 Hash。

之所以采用两个 Hash,主要目的是需要快速找到相同的数据(差异数据也就出来了)。快 Hash 用于快速查找可能相同的数据块,而慢 Hash 用于确认两块数据是否是相同或者不同。当两个数据块相同时,他们的快慢 Hash 一定是相同的。而如果两个块 Hash 值不同,那么他们对应的数据块一定不相同(但是反过来不一定),这样就不需要再进一步比较慢 Hash 值了。这样就可以节省下很多的 Hash 计算时间。

2. 将第 1 步计算的 Hash 传送到源文件,源文件也需要用相同的块长度计算两个 Hash 值。这里需要注意的是,为了最大限度地找到差异数据,源文件会对文件从开头开始,以字节为单位步进地计算相同块长度的数据的快 Hash,即相当于有一个块长度大小的数据块窗口,从开头向后移动,对在窗口中的数据,首先计算快 Hash,并在目标文件的 Hash 中查的是否有相同的快 Hash,如果有,则这块数据可能相同。则再比较慢 Hash,如果也相同,则这块数据就是相同的。而之前窗口滑过的数据,即为差异数据。如果 Hash 不同,则数据不同,则数据窗口再步进一个字节,重新执行 Hash 的计算与比较。

3. 当窗口滑过了整个文件,那么差异数据也就计算出来了。只要将这些差异数据以及相同数据块的记录传递给目标文件。目标文件根据这些差异数据以及相同数据块的记录,就能从目标文件中重构出一个与源文件相同的目标文件来。

在上面的算法中,最为关键的问题是在于 Hash 计算、查找的性能。因为在源文件中,需要对每一个字节边界的块数据计算 Hash 值,即使是快 Hash 也会造成非常大的负担。因此在 Rsync 算法中快 Hash 采用的是一种叫做 RollingHash 的算法。即这种算法除了第一次需要计算全部数据外,当窗口以步进的方式运动时,只要通过移出窗口的字节以及移入窗口的字节,即可计算出窗口中的数据的快速 Hash。而查找的性能也是影响算法应用一个关键因素。这里的问题比较好解决,即可以通过 Hash 表的方式,来实现查找。

由上面的分析也可知,同步文件时,对于文件的变更类型是不敏感的,即新旧文件之间的相同数据块不会因为在其前或者后插入数据或者删除数据而导致无法匹配。但是在块中插入数据或者删除数据,则会导致数据块匹配失败。此时,可以降低块大小来进行匹配。但是块过小,会导致传送的 Hash 表也会增加,相应增加数据传输量。但是只要块大小大于 Hash 值所需要的空间大小,其传送数据量小于整个文件尺寸的概率还是很大的。

通过上面的算法分析,只要将目标文件中快慢 Hash 传送到源文件,即可以通过 Rsync 算法来提取差异数据,因此会有两种同步方式,即:

PUSH模式:源文件发起同步,通知道目标文件将文件的 Hash 结果传递过来,然后计算差异数据,再传回目标文件。

PULL模式:目标文件发起同步,直接将 Hash 结果传送给源文件,源文件将计算结果传回目标文件。

三、块大小的选择

在上面的 Rsync 算法中分析中我们可以知道,无论文件是在开始,中间或者结尾插入数据,对于差异计算结果的影响是比较小的(相比相同偏移的的块比较方式)。同时,除了 Hash 的计算与查找的问题需要特别考虑外,影响 Rsync 效率的还有一个块大小的选择。一般来说,大文件应该有相应较大的块,因为大文件比较起来,全面更改的可能性要小得多。反之,小文件应该有较小的块。在 Rsync 论文中,块大小的选择是:

(24*size)1/2

其中 size 是文件的大小(以字节为单位)。

我所实现的 xdeltalib 所采用的块长度的决定算法是:

(log2(size))*(size)1/3

两个块计算公式对应的图形如下:

由上图可以看出,他们两个计算出来的块大小有相似的倾向值,但对数公式所产生的块大小递增较慢,实践中有更好的同步效果(更小的块有更多的可能找到相同的块)。

四、同步时的场景及问题

1. 在存储容易受限的系统上如何处理?

当前,同步算法在构造新文件时,首先是生成一个临时文件,并根据客户端计算返回的值,或者从旧文件中复制相同的块数据,或者将返回的差异数直接写入到临时文件中。完成后,将旧文件删除,并将临时文件更名为旧文件的文件名。

但是在当前,用户的应用大量地迁移到移动环境中,在这样的环境下,用户的设备上的存储空间是有限的,有可能导致系统无法提供足够的临时空间来完成同步,从而导致同步的失败。即使有足够的空间,如果有很多的数据块并没有移动在旧文件中的位置,则通过临时文件的方式构建新文件,也会导致很多的额外的数据迁移。

另外,由于移动设备中多是 SSD 设备或者 Flash 存储介质,通过构造临时文件的方式来实现同步,会导致多余的写操作,进而可能减少存储设备的寿命。

因此,在这样的场景中,我们需要一种就地(in-place)同步文件的方法。方法的核心是,如果一块数据在同步的过程中会被破坏,则需要将这块数据首先进行处理。也就是需要对从客户諯发送过来的相同的数据块的处理顺序进行重新排序,并先处理相同的数据块,最后将差异数据写入到文件中。如果文件大小有变小,则需要对新文件进行截断。

但是,这种方法有一个缺陷,即可能会导致传输数据量增大。因为如果在上面所述的计算中,如果出现数据块循环依赖,则需要将这个数据块以差异数据的方式进行传输,从而导致数据传输量的增大。

对于数据块处理顺序的计算,可以在新文件端进行处理,也可以在旧文件端进行处理。一个可用的建议是,如果旧文件集中存储,放在新文件端处理比较适宜。如果需要减少新文件端的计算量,则可以放在旧文件端处理。但是实际上,这个计算工作量一般不大,因此放在哪一端进行处理,要具体结合存储平台的实际情况。

2. 如果更改间隔小于块大小,会有什么问题?

如果文件的更改间隔小于块大小,则有可能导致文件中即使有大量的相同数据,也可能因为每个块中更改了一两个字节而导致无法匹配相同的数据块,导致同步效率的严重降低。但是这种情况下,我们可以不断减小块的大小并不断重复相同的同步过程来提交同步效率。直到最小的同步块出现。这种方式我们称为多轮同步。

一般来说,对于小文件采用多轮同步并没有多大的意义。因为小文件变动比率(变动的大小/总大小)一般比较高,更小的块,并不能提高同步的效率,相反会导致性能的下降。而针对大文件,多轮同步的情况下,一般可以取得比单轮同步好得多的效率。

五、Xdeltalib 的特点

Xdeltalib 是笔者用 C++ 实现的差异数据提取库,其核心就是 Rsync 的算法。有一个压缩工具称为 xdelta,但是其所使用的算法原理跟本库完全不同。Xdeltalib 库的特点有如下几个:

1. 完全用 C++ 写成,可以集中到 C++ 项目中,并充分利用 C++ 的优势。

2. 支持多平台,在 Windows 与 Linux 中经过严格测试,也可以整合到 Unix 平台中。

3. 代码经过特别优化,差异算法及数据结构经过精心设计,增加了执行性能。

4. 支持 in-place 同步算法,可以应用到各种平台中,包括移动平台、服务器环境以及 PC 环境。

5. 支持可配置的 multi-round (多轮)同步算法,提高同步效率,同时提高了集成平台的可配置性。

6. 集成网络数据传输功能,减少了用户整合的工作量,加快整合进度。

7. 支持可配置的或者默认的线程数,充分利用多核优势,提高了执行性能。

8. 采用消费者与生产者模型提交与处理任务,充分利用并发优势。

9. 一库多用途,即可用于传统的文件数据同步,也可用于其他差异算法可应用的场景。

10. 良好的平台适应性。通过特别的设计,提供在各种存储平台的应用,如单设备环境,云存储环境,以及分存式存储环境。

11. 完备的文档、支持与快速响应。

六、Xdeltalib 的可应用场景。

基于以上的分析跟说明,xdeltalib 可应用的场景或者业务如下:

1. 传统的快速文件数据同步,包括本地或者远程。

2. 基于源端重复数据删除。

3. 各种云存储或者网盘产品,由于数据的同步与去重。

等等。

七、Xdeltalib 库代码

可以从 https://github.com/yeyouqun/xdeltalib 下载实现及测试代码。