跳到主要内容

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)
备注

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

2. 文件信息部分

位置footer.file_info_offsetfooter.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 内的结束块索引(独占)
}
备注

请注意,在 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_offsetfooter.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)

备注

将 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, // 页脚开始的偏移量
}

内存布局

备注

字段不完全按比例

┌─────────┬─────────┬─────────┬─────────────────────────────────────────────────────────────┬─────────────────────────────────────┐
│ 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 将被拒绝

错误处理

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