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算法可能会得到下图所示的一个数据数组。图中,红色块表示在目标端已匹配上,不用传输,而白色的地方就是需要传输的内容(注意:白色块是不定长的,红色块仅仅是一个文件块编号,由目标端提供的)。当目标端拿到这个数组后,就可以重新生成一个新的文件了。
算法到这里就介绍完了,为了提高效率,由两个地方值得考虑:
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.lib实现Adler-32、MD5、Hash Search等核心算法;
Zip.lib实现数据压缩和解压缩,可直接借鉴开源库;
Network.lib实现网络传输部分,保证数据完整性。