最近在做一个跟网络存储有关的项目涉及到 CDC ,目前比较主流的算法除了前几年新提出的 FastCDC 以外就是基于拉宾卡普算法的 Rabin CDC ,其基本思想是计算拉宾指纹并根据滚动哈希进行分块。目前比较典型的成熟应用就是海文的 Seafile ,它的后端使用的是 Rabin CDC 并且网络上有很多针对其源代码的分析,例如: [1] http://www.ilovecpp.com/2019/02/03/cdc/ [2] https://blog.csdn.net/cyk0620/article/details/120813255
我也找到了一个 Github 仓库基本上是把 Seafile 的分块部分单独拿出来做了个主程序的实现: https://github.com/baixiangcpp/FileCDC
目前遇到的问题是,当我尝试将分块后的文件块再重新合并成一个文件,但发现这个文件跟原来的哈希不一致。具体来说,我使用 ubuntu 官网的 22.04 amd64 的系统镜像 iso 进行测试,源文件的 SHA256 哈希:
$ sha256sum ubuntu.iso
a435f6f393dda581172490eda9f683c32e495158a780b5a1de422ee77d98e909 ubuntu.iso
这与 Ubuntu 官网提供的 SHA256 校验值是一致的。但我尝试将分块后的块按照顺序重新合并后,得到的文件虽然大小与源文件相同,但 SHA256 哈希却变了:
$ sha256sum merged.iso
04a917c75a2d7733e0aabe6a5dbd0bbc875e8d3e63622c8d3c78b9058b92abd3 merged.iso
我自己用 C#另外实现了一遍,也是针对同一个 iso 源文件,但合并出来的文件哈希也不一样,甚至分块结果(也就是分了多少块,每一块的 SHA1 )和 C 语言的实现也不一样。
我用小黄鸭调试法检查了几遍也没能发现逻辑上的错误和问题,初步的断点调试和单元测发现拉宾指纹的计算部分 C#的运算结果和 C 语言的运算结果没有区别,但文件分块出来不一样,合并起来也不一样,完全没有排查的思路。毕竟一个大文件会分出 500+块,也很难通过断点一个个查到底是哪里出了问题。
所以现在出现的现象包括: 1. 分块后合并导致哈希不一致( C 语言实现和 C#实现都存在,但两种实现合并后的文件哈希也互不一致) 2. C#的实现和 C 语言实现两种实现计算 Rabin 指纹的结果一致,但分块结果不完全一致,具体来说,有些 C#分出来的块在 C 语言实现的结果中不存在,C 语言实现的结果一些块在 C#结果中也没有,分出来的块数也不一样。但有一些块是一样的,SHA1 和大小都一致。
所以想问问大佬们: 1. 以上出现的两种情况是正常的吗? 2. 如果不正常,是由于什么原因造成的? 3. 如何调试这样的问题?打断点一步步调试?找拉宾指纹的分割点可是一个个字节找的,这恐怕不太现实
附: 1. C 语言的完整代码是基于 https://github.com/baixiangcpp/FileCDC ,但改动了两个地方 一个是写块的函数,我把写在当前工作目录改成了卸载 chunks 文件夹中:
```c
int do_write_chunk (const unsigned char *checksum, const char *buf, int len)
{
char chksum_str[41];
int fd;
int n;
rawdata_to_hex (checksum, chksum_str, 20);
/* Don't write if the block already exists. */
if (g_access (chksum_str, F_OK) == 0)
return 0;
// Make path "/workspace/chunks/{chksum_str}"
char path[100];
sprintf(path, "/workspaces/FileCDC-master/chunks/%s", chksum_str);
fd = open (path, O_WRONLY|O_CREAT,0666);
if (fd == -1) {
printf ("Failed to open block %s.\n", path);
return -1;
}
n = write (fd, buf, len);
if (n < 0) {
printf ("Failed to write chunk %s.\n", chksum_str);
close (fd);
return -1;
}
close(fd);
return 0;
}
```
另一个是我自己加的,合并分块的函数:
```c
void file_merge_cdc(CDCFileDescriptor* file_desc)
{
int i;
int fd;
char path[100];
char *buf = malloc(file_desc->block_max_sz);
int merged_file = open("merged.iso", O_WRONLY | O_APPEND, 0666);
if(merged_file == -1)
{
printf("Failed to open merged file.\n");
return;
}
for(i = 0; i < file_desc->block_nr; i++)
{
char chksum_str[41];
rawdata_to_hex(file_desc->blk_sha1s + i * CHECKSUM_LENGTH, chksum_str, 20);
sprintf(path, "/workspaces/FileCDC-master/chunks/%s", chksum_str);
fd = open(path, O_RDONLY);
if(fd == -1)
{
printf("Failed to open block %s.\n", path);
return;
}
read(fd, buf, file_desc->block_max_sz);
close(fd);
write(merged_file, buf, file_desc->block_max_sz);
}
close(merged_file);
free(buf);
}
```
2. C#的分块实现
```c#
public class RabingCDC
{
private RabinChecksum rabinChecksum = new RabinChecksum();
private readonly int MinChunkSize = 1024 * 1024 * 6; // 6MB
private readonly int MaxChunkSize = 1024 * 1024 * 10; // 10MB
private readonly int AvarageChunkSize = 1024 * 1024 * 8; // 8MB
private LinkedList<string> fileHashed = new LinkedList<string>();
public async Task FileChunkCDC(Stream file)
{
byte[] buffer = new byte[MaxChunkSize];
int cursor = MinChunkSize - 1;
int bufferDataTail = 0;
while (true)
{
int readSize = 4096; // 4KB
if (bufferDataTail < MinChunkSize)
{
readSize = MinChunkSize - bufferDataTail + 4096;
}
else if (MaxChunkSize - bufferDataTail < 4096)
{
readSize = MaxChunkSize - bufferDataTail;
}
int readed = await file.ReadAsync(buffer, bufferDataTail, readSize);
bufferDataTail += readed;
if (bufferDataTail < MinChunkSize || cursor >= bufferDataTail)
{
// The file is less than minimum chunk size
// Write the entire file as a block
WriteBlock(buffer.Take(readed).ToArray());
bufferDataTail = 0;
cursor = MinChunkSize - 1;
break;
}
uint fingerprint = 0;
while (cursor < bufferDataTail)
{
if (cursor == MinChunkSize - 1)
{
fingerprint = rabinChecksum.GetRabingChecksum(buffer.Skip(cursor).Take(48).ToArray());
}
else
{
fingerprint = rabinChecksum.RabinRollingChecksum(fingerprint, 48, buffer[cursor - 48], buffer[cursor]);
}
if ((fingerprint & (AvarageChunkSize - 1)) == (0x0013 & (AvarageChunkSize - 1)) || cursor + 1 >= MaxChunkSize)
{
WriteBlock(buffer.Take(cursor + 1).ToArray());
// Move the rest of the buffer to the beginning
Array.Copy(buffer, cursor, buffer, 0, bufferDataTail - cursor-1);
bufferDataTail -= cursor + 1;
cursor = MinChunkSize - 1;
break;
}
else
{
cursor++;
}
}
}
}
public void WriteBlock(byte[] block)
{
var path = "D:\\Code\\temp\\cdc";
// Write block as a new file under the path, the file name is the SHA1 of the block
// Calcualte SHA1 of the block
byte[] sha1 = SHA1.Create().ComputeHash(block);
string fileName = BitConverter.ToString(sha1).Replace("-", "").ToLower();
fileHashed.AddLast(fileName);
File.WriteAllBytes(Path.Combine(path, fileName), block);
}
public async Task FileMergeCDC(string chunksFolderPath , string resultFilePath)
{
var fileStream = File.OpenWrite(resultFilePath);
foreach (var file in fileHashed)
{
var buffer = File.ReadAllBytes(Path.Combine(chunksFolderPath, file));
await fileStream.WriteAsync(buffer, 0, buffer.Length);
}
fileStream.Close();
}
}