Rsync核心算法

Rsync是unix/linux下同步文件的一个高效算法,它能同步更新两台计算机的文件与目录,并查找文件中的不同块以减少数据传输。Rsync的一个重要特性就是只对变更部分进行传送。rsync可拷贝、显示目录属性以及拷贝文件,并可选择性的压缩以及递归拷贝。Rsync是由Andrew Tridgell发明的。

Rsync要解决差异化同步,前提就必须知道源文件和目标文件之间的差异,特别是当源文件与目标文件分别位于两台计算机上时。

Rsync的算法流程:

1) 分块CheckSum算法:将目标文件按指定块大小平均分成若干块,然后对每块计算两个CheckSum值。涉及的两个算法如下:

a) Rolling CheckSum,32位,采用Adler-32算法;比较弱,碰撞几率高;

b) MD5 HASH,128位,强CheckSum,碰撞几率低;

显然,如果Rolling CheckSum 值不同,那么两个文件块肯定不同;但是,如果Rolling CheckSum 值相同,但因其高碰撞率我们无法确定两个文件块就是相同的,这时就需要进一步比较MD5的值了,毕竟MD5的碰撞几率达到2* 因此,可以说:Adler-32用来区别不同,而MD5用来进一步确认相同。

2) 目标文件的CheckSum计算完成后,接下来同步目标端就需要将这个CheckSum列表告诉给源文件端,给其提供一个差异检查的参照物。这个列表的每一项都包含以下三个信息:Rolling CheckSum、MD5、文件块号。同步源端拿到这个列表后,对源文件进行同样的CheckSum,然后对比,就可以知道哪些文件块发生了变化。仅仅如此简单吗?我们接着看下面这两个问题:

a) 如果在源文件中间插入了一个字符,其后的文件块都将偏移一个字符,显然和目标文件已经不同了,CheckSum信息也发生了改变。但从理论上将,我们应该只传输一个字符到目标端。怎么解决呢?

b) 如果目标文件很大,对应的CheckSum列表也将很长,在对源文件进行CheckSum比较时采用线性查找,显然比较慢。又该怎么办?

3) 查找算法。同步源端拿到目标文件的CheckSum列表后,将之存到一个Hash Table中,用Rolling Checksum做Hash-Key,以便获得O(1)时间复杂度的查找性能。这个Hash Table是16bits的,所以,Hash Table的大小是2^16,对Rolling Checksum的Hash会被散列到0 到 2^16 – 1中的某个整数值。显然,Hash表中的KEY会发生碰撞,我们只需要将碰撞的Checksum做成一个链表就可以了。

4) 比对算法。这是最关键的算法,细节如下:

a) 取源文件的第一个文件块(长度设为N),即从源文件的第1个字节到第N个字节计算Rolling CheckSum,然后到HASH表中查找;

b) 如果查到了,表明源文件与目标文件中有潜在相同的文件块;继续计算MD5并比较。如果Rolling checksum和MD5都相同,可确定相同文件块存在,记下文件块号。(两次CheckSum比较的碰撞概率为2^-(32+128) = 2^-160,可以忽略);

c) 如果没查到,不用计算MD5,就可以确定两个文件块不同。后移 1个字节,取源文件中的2-N+1字节的文件块继续执行步骤a);

rsync 模块传输 rsync原理_压缩


最终,在同步源端,rsync算法可能会得到下图所示的一个数据数组。图中,红色块表示在目标端已匹配上,不用传输,而白色的地方就是需要传输的内容(注意:白色块是不定长的,红色块仅仅是一个文件块编号,由目标端提供的)。当目标端拿到这个数组后,就可以重新生成一个新的文件了。

rsync 模块传输 rsync原理_源文件_02


算法到这里就介绍完了,为了提高效率,由两个地方值得考虑:

a) 计算CheckSum时涉及文件I/O操作,文件块的大小将决定IO效率,建议去文件系统的最小分配单元。比如,FAT系统下取簇大小。

b) 源与目标两端进行数据传输时都可考虑使用压缩算法,节省网络带宽,毕竟计算机的运行速度越来越快,不再会成为文件同步的瓶颈。

Adler-32算法细节

假设一段数据长度为N,每个字节依次为D1、D2 、...、Dn,那么Adler-32的计算公式如下:
A = 1 + D1 + D2 + ... + Dn (mod 65521)
    B = (1 + D1) + (1 + D1 + D2) + ... + (1 + D1 + D2 + ... + Dn) (mod 65521)
      = n×D1 + (n-1)×D2 + (n-2)×D3 + ... + Dn + n (mod 65521)
    Adler-32(D) = B × 65536 + A

很容易,我们就可以得到C的代码:

uint32  adler32(const uchar *buffer, uint32 length) 
{
    uint32 A = 1;
    uint32 B = 0;

    /* Process each byte of the data in order */
    uint32 i = 0;
    for (i = 0; i < length; ++i)
    {
        A = (A + buffer[i]) % 65521;
        B = (B + A) % 65521;
    }

    return (B << 16) | A;
}

每计算一个字节都做一次取模运算似乎没必要吧!两个字节相加怎么也不可能超过65521啊!于是,进一步优化代码,如下:
#define NMAX 5552

uint32  adler32_fast(const uchar* buffer, uint32 length)
{
    uint32 A = 1;
    uint32 B = 0;
    uint32 n = 0;

    /* do length NMAX blocks -- requires just one modulo operation */
    while (length >= NMAX)
    {
        length -= NMAX;
        n = NMAX;          /* NMAX is divisible by 16 */
        do
        {
            A += *buffer++;
            B += A;
        } while (--n);

        A %= MOD_ADLER32;
        B %= MOD_ADLER32;
    }

    /* do remaining bytes (less than NMAX, still just one modulo) */
    if (length)
    {   
        while (length--)
        {
            A += *buffer++;
            B += A;
        }

        A %= MOD_ADLER32;
        B %= MOD_ADLER32;
    }

    /* return recombined sums */
    return A | (B << 16);
}

NMAX声明为5552有什么依据呢?
来看一下公式中的B = n×D1 + (n-1)×D2 + (n-2)×D3 + … + Dn + n (mod 65521)。最坏情况下每个字节都为255,那么必须B = 255×n×(n+1)/2 + (n+1) ×(65521-1) <= 2^32-1,以保证B(32位无符号整形)不越界。
另外,在rsync中每步进一个字节有没有必要重新对整个文件块N个字节计算一次Adler-32值?
还是看一下公式:

A  = 1 + D1 + D2 + ... + Dn     
A’ = 1 + D2 + D3 + ... + Dn + Dn+1
   = A –D1 + Dn+1

B = n×D1 + (n-1)×D2 + (n-2)×D3 + ... + Dn + n
B’= n×D2 + (n-1)×D3 + ... + 2×Dn + 1×Dn+1 + n
  = B - n×D1 + D2 + D3 +... + Dn + Dn+1
  = B - n×D1 + A’- 1

如此以来,每步进一个字节只需操作两个字节的数据,大大减少了运算量。

A’= A –D1 + Dn+1
B’= B - n×D1 + A’- 1

MD5简介

MD5全名Message Digest Algorithm 5(即消息摘要算法第五版)为计算机安全领域广泛使用的一种散列函数,用以提供消息的完整性保护。MD5的前身有MD2、MD3和MD4。MD5的作用是让大容量信息在用数字签名软件签署私人密钥前被”压缩”成一种保密的格式(就是把一个任意长度的字节串变换成一定长的十六进制数字串)。
MD5以512位分组来处理输入的信息,且每一分组又被划分为16个32位子分组,经过了一系列的处理后,算法的输出由四个32位分组组成,将这四个32位分组级联后将生成一个128位散列值。
在MD5算法中,首先需要对信息进行填充,使其位长对512求余的结果等于448。因此,信息的位长(Bits Length)将被扩展至N*512+448,N为一个非负整数,可以是零。填充的方法如下,在信息的后面填充一个1和无数个0,直到满足上面的条件时才停止用0对信息的填充。然后,在这个结果后面附加一个以64位二进制表示的填充前信息长度。经过这两步的处理,现在的信息的位长=N*512+448+64=(N+1)*512,即长度恰好是512的整数倍。这样做的原因是为满足后面处理中对信息长度的要求。
具体细节和代码可百度。

rsync实现流程

rsync 模块传输 rsync原理_目标文件_03


Rsync.lib实现Adler-32、MD5、Hash Search等核心算法;

Zip.lib实现数据压缩和解压缩,可直接借鉴开源库;

Network.lib实现网络传输部分,保证数据完整性。