Skip to main content

MDB Shard 文件格式规范

Shard 是一个序列化对象,包含文件重建信息和用于去重目的的 xorb 元数据。

Shard 格式是上传文件重建上传并传达有关 xorbs 和块信息的载体,客户端可以针对这些信息进行数据去重。

概述​

MDB(Merkle 数据库)shard 文件格式是一种二进制格式,用于存储文件元数据和内容寻址存储(CAS)信息,以实现高效的去重和检索。 本文档描述了 shard 格式的二进制布局和反序列化过程。 xet 协议的实现者在实现上传协议时必须使用 shard 格式。 Shard 格式用于 shard 上传(记录文件)和全局去重 API。

用作 API 请求和响应 Body​

Shard 格式在 shard 上传 API 中用作请求负载,在全局去重/块查询 API 中用作响应负载。

Shard 上传​

在这种情况下,shard 是一种序列化格式,允许客户端表示它们正在上传的文件。 每个文件重建映射到文件信息部分中的文件信息块。 此外,客户端创建的所有新 xorbs 的列表映射到 CAS 信息部分中的项(CAS 信息块),以便将来可以针对它们进行去重。

上传 shard 时,必须省略页脚部分。

可用于文件上传的 shard 示例可以在 Xet 参考文件 中找到。 此 shard 的也包含页脚的版本在 Xet 参考文件 中,有关更多上下文,请参见参考文件数据集的 README。

全局去重​

全局去重 API 返回的 Shard 具有空的文件信息部分,仅在 CAS 信息部分包含相关信息。 此 API 返回的 CAS 信息部分包含 xorbs,其中 CAS 信息部分中描述的 xorb 包含被查询的块。 客户端可以针对返回的 shard 的 CAS 信息部分中任何 CAS 信息块中描述的任何其他 xorbs 进行内容去重。 在 shard 中返回的其他 xorb 描述可能更有可能引用客户端拥有的内容。

可用于全局去重查询的 shard 示例可以在 Xet 参考文件 中找到。

文件结构​

Shard 文件按顺序由以下部分组成:

┌─────────────────────┐
│ Header │
├─────────────────────┤
│ File Info Section │
├─────────────────────┤
│ CAS Info Section │
├─────────────────────┤
│ Footer │
└─────────────────────┘

带字节偏移的整体文件布局​

Offset 0:
┌───────────────────────────────────────────────────────┐
│ Header (48 bytes) │ ← 固定大小
└───────────────────────────────────────────────────────┘

Offset footer.file_info_offset:
┌───────────────────────────────────────────────────────┐
│ │
│ File Info Section │ ← 可变大小
│ (Multiple file blocks + │
│ bookend entry) │
│ │
└───────────────────────────────────────────────────────┘

Offset footer.cas_info_offset:
┌───────────────────────────────────────────────────────┐
│ │
│ CAS Info Section │ ← 可变大小
│ (Multiple CAS blocks + │
│ bookend entry) │
│ │
└───────────────────────────────────────────────────────┘

Offset footer.footer_offset:
┌───────────────────────────────────────────────────────┐
│ Footer (200 bytes, sometimes omitted) │ ← 固定大小
└───────────────────────────────────────────────────────┘

常量​

  • MDB_SHARD_HEADER_VERSION: 2
  • MDB_SHARD_FOOTER_VERSION: 1
  • MDB_FILE_INFO_ENTRY_SIZE: 48 字节(每个文件信息结构的大小)
  • MDB_CAS_INFO_ENTRY_SIZE: 48 字节(每个 CAS 信息结构的大小)
  • MDB_SHARD_HEADER_TAG: 32 字节魔术标识符

数据类型​

所有多字节整数都以小端格式存储。

  • u8: 8 位无符号整数
  • u32: 32 位无符号整数
  • u64: 64 位无符号整数
  • 字节数组类型在 Rust 中表示为 [u8; N],其中 N 是数组中的字节数。
  • Hash: 32 字节哈希值,特殊的 [u8; 32]

1. 标头(MDBShardFileHeader)​

位置:文件开始(偏移 0) 大小:48 字节

struct MDBShardFileHeader {
tag: [u8; 32], // 魔术数字标识符
version: u64, // 标头版本(必须为 2)
footer_size: u64, // 页脚大小(字节),如果省略页脚则设置为 0
}

内存布局:

┌────────────────────────────────────────────────────────────────┬───────────┬───────────┐
│ tag (32 bytes) │ version │ footer_sz │
│ Magic Number Identifier │ (8 bytes) │ (8 bytes) │
└────────────────────────────────────────────────────────────────┴───────────┴───────────┘
0 32 40 48

反序列化步骤:

  1. 读取 32 字节的魔术标签
  2. 验证标签匹配 MDB_SHARD_HEADER_TAG
  3. 读取 8 字节的版本(u64)
  4. 验证版本等于 2
  5. 读取 8 字节的 footer_size(u64)
note

序列化时,footer_size 必须是组成页脚的字节数,如果省略页脚则为 0。

2. 文件信息部分​

位置:footer.file_info_offset 到 footer.cas_info_offset 或直接在标头之后

此部分包含 0 个或多个文件信息(File Info)块的序列,每个块至少包含一个标头和至少 1 个数据序列条目,以及可选的验证条目和元数据扩展部分。 文件信息部分在到达书挡条目时结束。

整个部分中的每个文件信息块都是文件重建到二进制格式的序列化。 对于每个文件,有一个 FileDataSequenceHeader,对于每个 term,有一个 FileDataSequenceEntry,可选地有一个匹配的 FileVerificationEntry,并且在末尾还有一个可选的 FileMetadataExt。

Shard 文件信息部分可以连续包含多个文件信息块,在完成读取 1 个文件描述的所有内容后,下一个立即开始。 如果在读取下一部分的标头时,读取器遇到书挡条目,这意味着文件信息部分结束;你已经读取了此 shard 中的最后一个文件描述。

文件信息部分布局​

无可选组件:

┌─────────────────────┐
│ FileDataSeqHeader │ ← 文件 1
├─────────────────────┤
│ FileDataSeqEntry │
├─────────────────────┤
│ FileDataSeqEntry │
├─────────────────────┤
│ ... │
├─────────────────────┤
│ FileDataSeqHeader │ ← 文件 2
├─────────────────────┤
│ FileDataSeqEntry │
├─────────────────────┤
│ ... │
├─────────────────────┤
│ Bookend Entry │ ← 全 0xFF 哈希 + 零
└─────────────────────┘

包含所有可选组件:

┌─────────────────────┐
│ FileDataSeqHeader │ ← 文件 1(标志指示验证 + 元数据)
├─────────────────────┤
│ FileDataSeqEntry │
├─────────────────────┤
│ FileDataSeqEntry │
├─────────────────────┤
│ ... │
├─────────────────────┤
│ FileVerifyEntry │ ← 每个 FileDataSeqEntry 一个
├─────────────────────┤
│ FileVerifyEntry │
├─────────────────────┤
│ ... │
├─────────────────────┤
│ FileMetadataExt │ ← 每个文件一个(如果设置了标志)
├─────────────────────┤
│ FileDataSeqHeader │ ← 文件 2
├─────────────────────┤
│ ... │
├─────────────────────┤
│ Bookend Entry │ ← 全 0xFF 哈希 + 零
└─────────────────────┘

FileDataSequenceHeader​

struct FileDataSequenceHeader {
file_hash: Hash, // 32 字节文件哈希
file_flags: u32, // 指示后续条件部分的标志
num_entries: u32, // FileDataSequenceEntry 结构的数量
_unused: [u8; 8], // 保留空间 8 字节
}

文件标志:

  • MDB_FILE_FLAG_WITH_VERIFICATION (0x80000000 或 1 << 31):具有验证条目
  • MDB_FILE_FLAG_WITH_METADATA_EXT (0x40000000 或 1 << 30):具有元数据扩展

给定 file_data_sequence_header.file_flags & MASK(按位与)操作,如果结果 != 0,则效果为真。

Memory Layout:

┌────────────────────────────────────────────────────────────────┬──────────┬───────────┬────────────┐
│ file_hash (32 bytes) │file_flags│num_entries│ _unused │
│ File Hash Value │(4 bytes) │(4 bytes) │ (8 bytes) │
└────────────────────────────────────────────────────────────────┴──────────┴───────────┴────────────┘
0 32 36 40 48

FileDataSequenceEntry​

每个 FileDataSequenceEntry 是 1 个 term,本质上是文件重建 term 的二进制序列化。

struct FileDataSequenceEntry {
cas_hash: Hash, // term 中的 32 字节 Xorb 哈希
cas_flags: u32, // CAS 标志(保留供将来使用,设置为 0)
unpacked_segment_bytes: u32, // 解包时的 term 大小
chunk_index_start: u32, // term 在 Xorb 内的起始块索引
chunk_index_end: u32, // term 在 Xorb 内的结束块索引(独占)
}
note

请注意,在 FileDataSequenceEntry 中描述块范围时,使用起始包含但结束独占的范围,即 [chunk_index_start, chunk_index_end)

内存布局:

┌────────────────────────────────────────────────────────────────┬─────────┬─────────┬─────────┬─────────┐
│ cas_hash (32 bytes) │cas_flags│unpacked │chunk_idx│chunk_idx│
│ CAS Block Hash │(4 bytes)│seg_bytes│start │end │
│ │ │(4 bytes)│(4 bytes)│(4 bytes)│
└────────────────────────────────────────────────────────────────┴─────────┴─────────┴─────────┴─────────┘
0 32 36 40 44 48

FileVerificationEntry(可选)​

Shard 上传必须设置验证条目。

要为 shard 上传生成验证哈希,请阅读有关验证哈希的部分。

struct FileVerificationEntry {
range_hash: Hash, // 32 字节验证哈希
_unused: [u8; 16], // 保留(16 字节)
}

内存布局:

┌────────────────────────────────────────────────────────────────┬────────────────────────────────┐
│ range_hash (32 bytes) │ _unused (16 bytes) │
│ Verification Hash │ Reserved Space │
└────────────────────────────────────────────────────────────────┴────────────────────────────────┘
0 32 48

当 shard 具有验证条目时,所有文件信息部分必须具有验证条目。 如果 shard 中只有部分文件具有验证条目,则认为 shard 无效。 在这种情况下,每个 FileDataSequenceEntry 将有一个匹配的 FileVerificationEntry,其中 range_hash 使用该块范围的块哈希计算。

对于任何文件,第 n 个 FileVerificationEntry 与第 n 个 FileDataSequenceEntry 相关,并且像 FileDataSequenceEntries 一样,如果有验证条目,将有 file_data_sequence_header.num_entries 个验证条目(跟随 num_entries 数据序列条目)。

FileMetadataExt(可选)​

对于通过 shard 上传 API 上传的 shard,每个文件都需要此部分。

每个文件信息块只有 1 个 FileMetadataExt 实例,当存在时,它是该文件信息块的最后一个组件。

sha256 字段是所描述文件内容的 32 字节 SHA256。

struct FileMetadataExt {
sha256: Hash, // 32 字节 SHA256 哈希
_unused: [u8; 16], // 保留(16 字节)
}

内存布局:

┌────────────────────────────────────────────────────────────────┬────────────────────────────────┐
│ sha256 (32 bytes) │ _unused (16 bytes) │
│ SHA256 Hash │ Reserved Space │
└────────────────────────────────────────────────────────────────┴────────────────────────────────┘
0 32 48

文件信息书挡​

文件信息部分的结束由书挡条目标记。

书挡条目为 48 字节长,前 32 字节全为 0xFF,后跟 16 字节全为 0x00。

假设你尝试反序列化 FileDataSequenceHeader,其文件哈希全为 1 位,则此条目是书挡条目,下一个字节开始下一部分。

由于文件信息部分紧跟在标头之后,客户端可以跳过反序列化页脚以知道它开始反序列化此部分的位置。 文件信息部分从标头之后立即开始,并在到达书挡时结束。

反序列化步骤:

  1. 寻址到 footer.file_info_offset
  2. 读取 FileDataSequenceHeader
  3. 检查 file_hash 是否全为 0xFF(书挡标记)- 如果是,停止
  4. 读取 file_data_sequence_header.num_entries × FileDataSequenceEntry 结构
  5. 如果 file_flags & MDB_FILE_FLAG_WITH_VERIFICATION != 0:读取 file_data_sequence_header.num_entries × FileVerificationEntry
  6. 如果 file_flags & MDB_FILE_FLAG_WITH_METADATA_EXT != 0:读取 1 × FileMetadataExt
  7. 从步骤 2 重复,直到找到书挡

3. CAS 信息部分​

位置:footer.cas_info_offset 到 footer.footer_offset 或直接在文件信息部分书挡之后

此部分包含 CAS(内容寻址存储)块信息。每个 CAS 信息块通过首先具有 CASChunkSequenceHeader 来表示 xorb,该标头包含组成此块的后续 CASChunkSequenceEntries 的数量。CAS 信息部分在到达书挡条目时结束。

CAS 信息部分布局​

┌─────────────────────┐
│ CASChunkSeqHeader │ ← CAS 块 1
├─────────────────────┤
│ CASChunkSeqEntry │
├─────────────────────┤
│ CASChunkSeqEntry │
├─────────────────────┤
│ ... │
├─────────────────────┤
│ CASChunkSeqHeader │ ← CAS 块 2
├─────────────────────┤
│ CASChunkSeqEntry │
├─────────────────────┤
│ ... │
├─────────────────────┤
│ Bookend Entry │ ← 全 0xFF 哈希 + 零
└─────────────────────┘

反序列化步骤:

  1. 寻址到 footer.cas_info_offset
  2. 读取 CASChunkSequenceHeader
  3. 检查 cas_hash 是否全为 0xFF(书挡标记)- 如果是,停止
  4. 读取 cas_chunk_sequence_header.num_entries × CASChunkSequenceEntry 结构
  5. 从步骤 2 重复,直到找到书挡

CASChunkSequenceHeader​

struct CASChunkSequenceHeader {
cas_hash: Hash, // 32 字节 Xorb 哈希
cas_flags: u32, // CAS 标志(保留供以后使用,设置为 0)
num_entries: u32, // 此 Xorb 中的块数
num_bytes_in_cas: u32, // 此 Xorb 中所有原始块字节的总大小
num_bytes_on_disk: u32, // 上传时序列化后 xorb 的长度
}

内存布局:

┌────────────────────────────────────────────────────────────────┬─────────┬─────────┬─────────┬─────────┐
│ cas_hash (32 bytes) │cas_flags│num_ │num_bytes│num_bytes│
│ CAS Block Hash │(4 bytes)│entries │in_cas │on_disk │
│ │ │(4 bytes)│(4 bytes)│(4 bytes)│
└────────────────────────────────────────────────────────────────┴─────────┴─────────┴─────────┴─────────┘
0 32 36 40 44 48

CASChunkSequenceEntry​

每个 CASChunkSequenceHeader 都有一个 num_entries 数字字段。 此数字是应该反序列化的 CASChunkSequenceEntry 项的数量,这些项与此 CAS 信息块描述的 xorb 相关联。

struct CASChunkSequenceEntry {
chunk_hash: Hash, // 32 字节块哈希
chunk_byte_range_start: u32, // CAS 块中的起始位置
unpacked_segment_bytes: u32, // 解包时的大小
_unused: [u8; 8], // 保留空间 8 字节
}

内存布局:

┌────────────────────────────────────────────────────────────────┬─────────┬─────────┬─────────────────┐
│ chunk_hash (32 bytes) │chunk_ │unpacked │ _unused │
│ Chunk Hash │byte_ │segment_ │ (8 bytes) │
│ │range_ │bytes │ │
│ │start │(4 bytes)│ │
│ │(4 bytes)│ │ │
└────────────────────────────────────────────────────────────────┴─────────┴─────────┴─────────────────┘
0 32 36 40 48

CAS 信息书挡​

CAS 信息部分的结束由书挡条目标记。

书挡条目为 48 字节长,前 32 字节全为 0xFF,后跟 16 字节全为 0x00。

假设你尝试反序列化 CASChunkSequenceHeader,其哈希全为 1 位,则此条目是书挡条目,下一个字节开始下一部分。

由于 CAS 信息部分紧跟在文件信息部分书挡之后,客户端可以跳过反序列化页脚以知道 CAS 信息部分开始的位置,它从文件信息部分书挡之后立即开始,并在到达下一个书挡时结束。

4. 页脚(MDBShardFileFooter)​

note

将 shard 序列化为 shard 上传 API 的 body 时,不得包含页脚。

位置:文件末尾减去 footer_size 大小:200 字节

struct MDBShardFileFooter {
version: u64, // 页脚版本(必须为 1)
file_info_offset: u64, // 文件信息部分的偏移量
cas_info_offset: u64, // CAS 信息部分的偏移量
_buffer: [u8; 48], // 保留空间(48 字节)
chunk_hash_hmac_key: Hash, // 块哈希的 HMAC 密钥(32 字节)
shard_creation_timestamp: u64, // 创建时间(自纪元以来的秒数)
shard_key_expiry: u64, // 过期时间(自纪元以来的秒数)
_buffer2: [u8; 72], // 保留空间(72 字节)
footer_offset: u64, // 页脚开始的偏移量
}

内存布局:

note

字段不完全按比例

┌─────────┬─────────┬─────────┬─────────────────────────────────────────────────────────────┬─────────────────────────────────────┐
│ version │file_info│cas_info │ _buffer (reserved) │ chunk_hash_hmac_key │
│(8 bytes)│offset │offset │ (48 bytes) │ (32 bytes) │
│ │(8 bytes)│(8 bytes)│ │ │
└─────────┴─────────┴─────────┴─────────────────────────────────────────────────────────────┴─────────────────────────────────────┘
0 8 16 24 72 104

┌─────────┬──────────┬─────────────────────────────────────────────────────────────────────────────┬─────────┐
│creation │shard_ │ _buffer (reserved) │footer_ │
│timestamp│key_expiry│ (72 bytes) │offset │
│(8 bytes)│ (8 bytes)│ │(8 bytes)│
└─────────┴──────────┴─────────────────────────────────────────────────────────────────────────────┴─────────┘
104 112 120 192 200

反序列化步骤:

  1. 寻址到 file_size - footer_size
  2. 按顺序将所有字段读取为 u64 值
  3. 验证版本等于 1

页脚字段的使用​

file_info_offset 和 cas_info_offset​

这些偏移量允许你寻址到 shard 数据缓冲区以到达这些部分,而无需线性反序列化。

HMAC 密钥保护​

如果 footer.chunk_hash_hmac_key 非零(作为来自全局去重 API 的响应 shard),CAS 信息部分中的块哈希使用 HMAC 保护:

  • 存储的块哈希是 HMAC(original_hash, footer.chunk_hash_hmac_key)
  • 要检查你拥有的数据块是否与 shard 中列出的块匹配,请为你的块哈希计算 HMAC(chunk_hash, footer.chunk_hash_hmac_key) 并在 shard 结果中搜索。 如果你找到匹配(matched_chunk),那么你知道你的块的原始块哈希与 matched_chunk 相同,你可以通过引用 matched_chunk 所属的 xorb 来去重你的块。

Shard 密钥过期​

Shard 密钥过期是接收到的 shard 被视为过期的 64 位 unix 时间戳(通常在 shard 发送回来后的几天或几周内)。

在此过期时间过去后,客户端应认为此 shard 已过期,不应使用它来去重数据。 引用此 shard 引用的 xorbs 的上传可能会被服务器拒绝。

完整反序列化算法​

// ** 选项 1,线性读取,流式 **
// 假设 shard 是一个可读的文件类对象,读取器位置在 shard 开始处
// 1. 读取并验证标头
header = read_header(shard)

// 2. 读取文件信息部分
file_info = read_file_info_section(shard) // 读取到文件信息书挡

// 3. 读取 CAS 信息部分
cas_info = read_cas_info_section(shard) // 读取到 CAS 信息书挡

// 4. 读取页脚
footer = read_footer(shard)

// shard 读取器现在应该在 EOF


// ** 选项 2,读取页脚并寻址 **
// 假设 shard 是一个可读可寻址的文件类对象
// 1. 读取并验证标头
seek(start of shard)
header = read_header(shard)

// 2. 读取并验证页脚(需要偏移量)
seek(end of shard minus header.footer_size)
footer = read_footer(shard)

// 3. 读取文件信息部分
seek(footer.file_info_offset)
file_info = read_file_info_section(shard) // 直到 footer.cas_info_offset

// 4. 读取 CAS 信息部分
seek(footer.cas_info_offset)
cas_info = read_cas_info_section(shard) // 直到 footer.footer_offset

版本兼容性​

  • 标头版本 2:当前格式
  • 页脚版本 1:当前格式
  • 不同版本的 Shard 将被拒绝

错误处理​

  • 始终验证魔术数字和版本
  • 检查偏移量是否在文件边界内
  • 验证书挡标记在预期位置存在