跳到主要内容

下载协议

本文档描述了使用内容寻址存储(CAS)重建 API 从 Xet 协议下载单个文件的完整过程。

概述

Xet 协议中的文件下载是一个两阶段过程:

  1. 重建查询:查询 CAS API 以获取文件重建元数据
  2. 数据获取:使用重建元数据下载并重新组装文件

阶段 1:调用重建 API

单个文件重建

要下载给定文件哈希的文件,首先调用重建 API 以获取文件重建。按照 api 中的步骤操作。

请注意,你至少需要一个 read 作用域的身份验证令牌,身份验证参考

提示

对于大文件,建议批量请求重建,即前 10GB,下载所有数据,然后下一个 10GB,依此类推。客户端可以使用 Range 标头指定文件数据的范围。

阶段 2:理解重建响应

重建 API 返回一个 QueryReconstructionResponse 对象,包含三个关键组件:

QueryReconstructionResponse 结构

{
"offset_into_first_range": 0,
"terms": [
{
"hash": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
"unpacked_length": 263873,
"range": {
"start": 0,
"end": 4
}
},
...
],
"fetch_info": {
"a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456": [
{
"range": {
"start": 0,
"end": 4
},
"url": "https://transfer.xethub.hf.co/xorb/default/a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
"url_range": {
"start": 0,
"end": 131071
}
},
...
],
...
}
}

字段

offset_into_first_range

  • 类型:number
  • 对于完整文件或当指定的范围起始为 0 时,这保证为 0
  • 对于范围查询,这是第一个 term(反序列化/块解压缩)中的字节偏移量,从该偏移量开始保留数据。
    • 由于请求的范围可能从块的中间开始,并且数据必须以完整块下载(因为它们可能需要反序列化),因此此偏移量告诉客户端在第一个块(或可能在第一个 term 内的多个块)中跳过多少字节。

terms

  • 类型:Array<CASReconstructionTerm>
  • 描述从哪个 xorb 下载哪些块的重建 term 的有序列表
  • 每个 CASReconstructionTerm 包含:
    • hash:Xorb 哈希(64 字符小写十六进制字符串)
    • range:Xorb 内的块索引范围 { start: number, end: number };端部独占 [start, end)
    • unpacked_length:解压缩后的预期长度(用于验证)

fetch_info

  • 类型:Map<Xorb Hash (64 字符小写十六进制字符串), Array<CASReconstructionFetchInfo>>
  • 将 xorb 哈希映射到下载其某些块所需的信息。
    • 映射到一个或多个 CASReconstructionFetchInfo 的数组
  • 每个 CASReconstructionFetchInfo 包含:
    • url:用于下载 xorb 数据的 HTTP URL,包含授权信息的预签名 URL
    • url_range (bytes_start, bytes_end):用于 Range 标头的字节范围 { start: number, end: number };端部包含 [start, end]
      • 下载此块范围时,Range 标头必须设置为 Range: bytes=<start>-<end>
    • range (index_start, index_end):此 URL 提供的块索引范围 { start: number, end: number };端部独占 [start, end)
      • 此范围指示此 fetch info term 描述此 xorb 内的哪个块索引范围

阶段 3:下载和重建文件

过程概述

  1. 按顺序处理 terms 数组中的每个 CASReconstructionTerm

  2. 对于每个 CASReconstructionTerm,使用 term 的哈希查找匹配的 fetch info

    1. CASReconstructionTerm 获取 xorb 哈希下的 fetch_info 项列表。保证 xorb 哈希作为键存在于 fetch_info 映射中。
    2. 线性迭代 CASReconstructionFetchInfo 列表,找到一个引用等于或包含 term 的块范围的项。
      • 保证存在这样的 fetch_info 项。如果不存在,服务器有错误。
  3. 使用 HTTP GET 请求下载所需数据,并且必须设置 Range 标头

  4. 反序列化下载的 xorb 数据以提取块

    1. 此系列块包含由 CASReconstructionFetchInforange 字段指定的索引处的块。修剪开头或结尾的块以匹配重建 term 的 range 字段指定的块。
    2. (仅对于第一个 term)跳过 offset_into_first_range 字节
  5. 按 term 顺序连接结果以重建文件

详细下载过程

下载重建

  • 使用重建 API 下载给定文件的重建对象
file_id = "0123...abcdef"
api_endpoint, token = get_token() # 遵循身份验证说明
url = api_endpoint + "/reconstructions/" + file_id
reconstruction = get(url, headers={"Authorization": "Bearer: " + token})

# 将重建分解为组件
terms = reconstruction["terms"]
fetch_info = reconstruction["fetch_info"]
offset_into_first_range = reconstruction["offset_into_first_range"]

将 Term 匹配到 Fetch Info

对于 terms 数组中的每个 CASReconstructionTerm

  • fetch_info 映射中查找 term 的 hash 以获取 CASReconstructionFetchInfo 列表
  • 找到一个 CASReconstructionFetchInfo 条目,其中 fetch info 的 range 包含 term 的 range
    • 线性搜索 CASReconstructionFetchInfo 数组,找到 CASReconstructionFetchInfo 的范围块({ "start": number, "end": number })的 start <= term 的范围 start 且 end >= term 的范围 end 的元素。
    • 服务器应该保证匹配,如果没有匹配,此下载被视为失败,服务器出错。
for term in terms:
xorb_hash = term["hash"]
fetch_info_entries = fetch_info[xorb_hash]
fetch_info_entry = None
for entry in fetch_info_entries:
if entry["range"][start] <= term["range"]["start"] and entry["range"]["end"] >= term["range"]["end"]:
fetch_info_entry = entry
break
if fetch_info_entry is None:
# 错误!

步骤 2:下载 Xorb 数据

对于每个匹配的 fetch info:

  1. 向 fetch info 条目中的 url 发出 HTTP GET 请求
  2. 包含 Range 标头:bytes={url_range.start}-{url_range.end}
for term in terms:
...
data_url = fetch_info_entry["url"]
range_header = "bytes=" + fetch_info_entry["url_range"]["start"] + "-" + fetch_info_entry["url_range"]["end"]
data = get(data_url, headers={"Range": range_header})

反序列化下载的数据

下载的数据采用 xorb 格式,必须反序列化:

  1. 解析 xorb 结构:数据包含带标头的压缩块
  2. 解压缩块:每个块都有一个标头,后跟压缩数据
  3. 提取字节索引:跟踪块之间的字节边界以进行范围提取
  4. 验证长度:解压缩后的长度必须匹配 term 中的 unpacked_length

注意:反序列化过程取决于 Xorb 格式

for term in terms:
...
chunks = {}
for i in range(fetch_info_entry["range"]["start"], fetch_info_entry["range"]["end"]):
chunk = deserialize_chunk(data) # 假设 data 是一个向前推进的读取器
chunks[i] = chunk
# 此时数据应该完全消耗

步骤 4:提取 Term 数据

从反序列化的 xorb 数据中:

  1. 使用 term 的 range 识别需要哪些块
  2. 仅提取由 range.startrange.end-1(端部独占)指定的块
  3. 如果处理部分文件下载,应用任何范围偏移
file_chunks = []
for term in terms:
...
for i in range(term["range"]["start"], term["range"]["end"]):
chunk = chunks[i]
# 偏移可能捕获多个块,因此我们可能需要跳过整个块
if offset_into_first_range > len(chunk):
offset_into_first_range -= len(chunk)
continue
if offset_info_first_range > 0:
chunk = chunk[offset_into_first_range:]
offset_info_first_range = 0

file_chunks.push(chunk)

步骤 5:拼接结果

将所有块写入输出文件或缓冲区。

如果指定了范围,则需要将总数据截断为请求的字节数。 当指定了范围但范围不在块边界上结束时,请求范围的最后一个字节将在最后一个块的中间。 客户端从 offset_into_first_range 知道数据的开始,然后可以使用指定范围的长度来知道结束偏移。

with open(file_path) as f:
for chunk in file_chunks:
f.write(chunk)

范围下载

对于部分文件下载,重建 API 支持范围查询:

  • 在重建请求中包含 Range: bytes=start-end 标头
  • offset_into_first_range 字段指示你的范围在第一个 term 内的起始位置
  • 内容的结尾需要截断以适合请求的范围。
    • 除非请求的范围超过总文件长度,在这种情况下返回的内容会更短,不需要截断。

下载单个 term 数据时:

客户端必须包含使用 url_range 字段的值形成的 Range 标头,以指定它们正在访问的 xorb 的确切数据范围。不指定此标头将导致授权失败。

Xet 全局去重要求仅授予对授权范围的 xorbs 访问权限。 不指定此标头将导致授权失败。

性能注意事项

  • 范围合并:多个 term 可能共享相同的 fetch info 以提高效率,因此单个 fetch info 可能大于任何 1 个 term,可用于满足多个 term。 考虑仅下载一次此类内容并重用数据。
  • 并行下载:Term 可以并行下载,但必须按顺序组装
    • 在具有快速寻址的文件系统上,在不同线程中打开输出文件并在不同偏移处写入内容可能是有利的
  • 缓存:客户端应考虑缓存下载的 xorb 范围以避免冗余请求
  • 重试逻辑:为瞬态故障实现指数退避

缓存建议

  1. 缓存重建对象可能无效
    1. fetch_info 部分提供短期过期的预签名 URL,因此客户端不应在短期过期后缓存 URL
    2. 要获取这些 URL 以访问数据,无论如何都需要再次调用重建 API
  2. 按范围缓存块,而不仅仅是单独缓存
    1. 如果你需要来自 xorb 的块,很可能需要另一个,因此将它们缓存在一起
  3. 缓存有助于下载相似内容。如果你总是下载不同的内容,可能不值得缓存数据

更复杂的 QueryReconstruction 示例

以下是一个序列化的 QueryReconstructionResponse 结构示例,显示了文件重建如何在多个 xorbs 之间工作:

{
"offset_into_first_range": 0,
"terms": [
{
"hash": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
"unpacked_length": 263873,
"range": {
"start": 1,
"end": 4
}
},
{
"hash": "fedcba0987654321098765432109876543210fedcba098765432109876543",
"unpacked_length": 143890,
"range": {
"start": 0,
"end": 3
}
},
{
"hash": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
"unpacked_length": 3063572,
"range": {
"start": 3,
"end": 43
}
},
],
"fetch_info": {
"a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456": [
{
"range": {
"start": 1,
"end": 43
},
"url": "https://transfer.xethub.hf.co/xorb/default/a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130721%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130721T201207Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=d6796aa6097c82ba7e33b4725e8396f8a9638f7c3d4b5a6b7c8d9e0f1a2b3c4d",
"url_range": {
"start": 57980,
"end": 1433008
}
}
],
"fedcba0987654321098765432109876543210fedcba098765432109876543": [
{
"range": {
"start": 0,
"end": 3
},
"url": "https://transfer.xethub.hf.co/xorb/default/fedcba0987654321098765432109876543210fedcba098765432109876543?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130721%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130721T201207Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=d6796aa6097c82ba7e33b4725e8396f8a9638f7c3d4b5a6b7c8d9e0f1a2b3c4d",
"url_range": {
"start": 0,
"end": 65670
}
}
]
}
}

此示例显示了需要以下内容的文件重建:

  • 来自第一个 xorb 的块 [1, 4)(约 264KB 的未打包数据)
  • 来自第二个 xorb 的块 [0, 2)(约 144KB 的未打包数据)
  • 来自第一个 term 的同一 xorb 的块 [3, 43)(约 3MB 的未打包数据)

fetch_info 提供从每个 xorb 下载所需块数据所需的 HTTP URL 和字节范围。fetch_info 和 term 部分中提供的范围始终是端部独占的,即 { "start": 0, "end": 3 } 是索引 0、1 和 2 处的 3 个块的范围。 fetch_info 项的 url_range 键下提供的范围用于在下载块范围时形成 Range 标头。 "url_range"{ "start": X, "end": Y } 创建 Range 标头值 bytes=X-Y

当从 xorb a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456 下载和反序列化块时,我们将拥有索引 [1, 43) 处的块。 我们只需要使用 [1, 4) 处的块来满足第一个 term,然后使用块 [3, 43) 来满足第三个 term。 请注意,在此示例中,索引 3 处的块使用了两次!这是去重的好处;我们只需要下载块内容一次。

序列图