RustFS Chunk 格式规范 — M0(中文版)
RustFS Chunk 格式规范 — M0(中文版)
文档编号: RUSTFS-SPEC-CHUNK-001
状态: 草案——里程碑 M0 的冻结目标
版本: format_version = 1
范围: on-disk shard 文件格式、流式 bitrot、chunk↔shard 映射、chunk-key
路径方案,以及持久化的写/读协议。这是 chunk-store 与自校验完整性
模型的地基。
语言: 中文 (English mirror: rustfs-chunk-format-EN.md)
字节序: 除非另行说明,一律小端(little-endian)
1. 术语与存储层级
File(文件) POSIX 文件 / S3 对象 / 块卷 的逻辑字节。
Chunk(块) 文件的固定逻辑切片:chunk_size 字节(默认 4 MiB)。
由 (file_id, version, chunk_index) 标识。
Shard(分片) 真正存于一块磁盘上的单元。chunk 按存储策略变换为分片:
· EC(k,m) → k 个数据分片 + m 个校验分片
· Replica(r) → r 个完全相同的分片(概念上 k=1, m=0)
Shard file 保存一个分片的磁盘文件,采用本规范定义的格式。
Bitrot block 在 shard 文件内部计算一个完整性哈希的单元(默认 16 KiB)。
shard 文件的数据区是一串 bitrot block;每个在哈希表中有一个
HighwayHash-256 条目。
EC block 对原始 chunk 字节做纠删编码的单元(默认 1 MiB);每个 EC block
为每个数据分片产生一个 shard_fragment。
File ──切分──▶ Chunk ──EC 或 Replica──▶ Shard ──存为──▶ Shard 文件
│
[ Header | HashTable | Data ]
Data = bitrot block,每块带哈希
权威性说明:KV 中的 inode Layout 对放置与解码参数具有权威性。shard 文件
头部(见下)复制了关键参数,使每个分片对校验、巡检、灾难恢复自描述。若头部与
inode 不一致,即为损坏信号——正常路径上头部绝不用于覆盖放置。
2. Shard 文件布局(顶层)
shard 文件由三个连续、4 KiB 对齐的区域组成:
偏移 区域
───────────────────────────── ──────────────────────────────────────────────
0 Header(恰好 4096 字节)
4096 Hash Table(align_up(block_count*32, 4096) 字节)
4096 + hash_table_size Data(分片字节;bitrot block)
采用拆分布局(哈希与数据分离,而非交错)的理由:Data 区从 4 KiB 边界开始,
且每个 bitrot block 都是 4 KiB 的倍数,因此读取对 O_DIRECT 友好,且 shard 偏移
O 处的字节总位于文件偏移 data_start + O——随机读的偏移计算很干净。包含 O
的 bitrot block 的哈希位于 4096 + (O / bitrot_block) * 32。
3. Header(4096 字节,定长)
偏移 大小 字段 说明
─── ──── ──────────────────── ────────────────────────────────────────────
0 8 magic ASCII "RFSHARD1"
8 2 format_version (u16) = 1
10 2 flags (u16) 见 §3.1
12 4 header_crc32c (u32) 对字节 [16, 4096) 的 CRC32C(Castagnoli)
16 8 file_id (u64)
24 8 chunk_index (u64)
32 8 version (u64) 对象/文件版本(无版本时为 1)
40 2 shard_index (u16) 0..k+m-1(EC)或 0..r-1(replica)
42 2 ec_k (u16) 数据分片数(replica 为 1)
44 2 ec_m (u16) 校验分片数(replica 为 0)
46 1 ec_algo (u8) 0=无/replica,1=rs-vandermonde
47 1 csum_algo (u8) 1=HighwayHash256
48 4 bitrot_block (u32) 每个哈希块的字节数;4096 的倍数
52 8 shard_data_len (u64) 本分片数据区的逻辑字节数
60 8 chunk_logical_len(u64) 原始 chunk 的逻辑字节数
68 4 block_count (u32) = ceil(shard_data_len / bitrot_block)
72 8 created_unix_nanos(u64)
80 8 ec_block_size (u64) 对 chunk 字节的纠删块大小(如 1 MiB)
88 8 placement_seed (u64) Layout.placement_seed 的回显(交叉校验)
96 8 cluster_map_epoch(u64) 该分片放置时的 epoch
104 8 chunk_size (u64) 文件的 chunk_size(Layout 回显)
112 8 content_len (u64) chunk 载荷变换前的逻辑长度
(压缩/加密前),用于最后一个 chunk 的短尾
120 4904 reserved 写入时必须为零;读取时忽略
3.1 flags(u16,位域)
bit 0 mode_ec 1 = EC(k,m) 数据/校验分片;0 = replica 副本
bit 1 compressed 哈希前载荷已压缩(算法在 Layout 中)
bit 2 encrypted 哈希前载荷已加密(密钥引用在 Layout 中)
bit 3 last_block_short 最后一个 bitrot block 不足 bitrot_block 字节
bit 4 parity_shard 本分片为校验分片(shard_index >= ec_k)
bit 5..15 reserved(0)
变换顺序(置位时):存储载荷 = encrypt(compress(raw))。 bitrot 哈希在存储载荷上计算(变换后),因此完整性恰好保护落盘字节。
4. Hash Table(区域 2)
block_count 个条目,每个 32 字节(HighwayHash-256 摘要),按块顺序排列。
条目 i 覆盖 Data 字节 [i*bitrot_block, min((i+1)*bitrot_block, shard_data_len))。
区域以零填充至下一个 4096 边界。
4.1 HighwayHash-256 密钥(位置绑定)
每个块的哈希使用由集群 bitrot 密钥 K = (K0,K1,K2,K3)(4×u64,按集群配置)与
块位置 XOR 混合派生的 256 位密钥,使得正确的块放在错误位置时校验失败:
key0 = K0 ^ file_id
key1 = K1 ^ chunk_index
key2 = K2 ^ ((shard_index as u64) << 32 | block_index as u64)
key3 = K3 ^ version
digest_i = HighwayHash256(key=(key0,key1,key2,key3), data = data_block_i)
这把每个块绑定到 (file, chunk, shard, version, 块索引),在位腐之外还能捕获 错位的读/写。
5. Data 区(区域 3)
shard_data_len 字节,概念上是 block_count 个 bitrot block 连续排布。除最后一个
可能不足(flags.last_block_short)外,所有块均为 bitrot_block 字节。块与块之间
不填充;仅整个文件可在 EOF 处为 O_DIRECT 写入补齐到 4 KiB(超过 shard_data_len
的尾部填充字节被忽略)。
6. Chunk ↔ Shard 字节映射(EC 模式)
对 EC(k,m) 与 ec_block_size(EB),chunk 的原始字节按 EB 大小的纠删块处理;
每个 EB 被切为 k 个等长 shard_fragment:
shard_fragment = ceil(EB / k) (最后一个数据分片在需要时零填充)
定位 chunk 偏移 C(chunk 内 0 基)处的字节:
eb_index = C / EB # 哪个纠删块
off_in_eb = C % EB
data_shard = off_in_eb / shard_fragment # 哪个数据分片(0..k-1)
off_in_fragment = off_in_eb % shard_fragment
shard_offset = eb_index * shard_fragment + off_in_fragment
因此连续的 chunk 范围最多触及 k 个数据分片;一个小范围(< shard_fragment、
位于一个 EB 内)恰好触及**一个**数据分片 → 1x 读放大(随机读快路径;不碰校验、
除非所需数据分片不健康才重建)。
Replica 模式下,chunk 字节原样存于每个分片(shard_offset = chunk_offset); 读取选任一健康副本。
7. Chunk-Key 与 on-disk 路径方案
逻辑 chunk-key: (file_id, version, chunk_index)
分片标识: (file_id, version, chunk_index, shard_index)
on-disk 路径:
{disk_root}/.chunks/{p0}/{p1}/{file_id:016x}.v{version}/c{chunk_index}/{shard_index}.shard
其中:
p0 = h = xxh3_64(file_id) 的十六进制第 0 字节 # 00..ff
p1 = h 的十六进制第 1 字节 # 00..ff
→ 两级扇出(≤ 65536 个叶组),限制每目录条目数。
写入期间临时文件:
{...}/{shard_index}.shard.tmp.{random_u64:016x}
目录扇出仅以 file_id 为键(不含 chunk_index),使一个文件的所有 chunk 在磁盘上
聚集——有利于顺序巡检/heal 的局部性。
8. 持久化写协议
8.1 整分片写(新 chunk;数据集写路径)
1. 为所有块计算 bitrot 哈希(在变换后载荷上)。
2. 将 Header、Hash Table、Data 写入 {path}.tmp.{rand}。
3. fsync(tmp)。
4. rename(tmp → final) # 同一文件系统上原子
5. fsync(父目录) # 持久化 rename
8.2 部分写(块卷 / 随机原地写)
写入必须以 bitrot_block 粒度处理(读-改-写):
对每个与写范围重叠的 bitrot block B:
1. 读 B 的数据 + 其哈希条目;校验(若 B 被完全覆写则跳过校验)。
2. 将新字节应用到 B 的缓冲。
3. 为 B 重算摘要。
4. 写数据块(O_DIRECT 对齐);写 32 字节哈希条目。
5. fdatasync(或 O_DSYNC)。
数据块与其哈希条目之间的撕裂写窗口可容忍:后续读时不匹配会触发重建(EC)或 另一副本读 + 修复。块卷应使用 Replica 模式(见 RUSTFS-MD-DESIGN-005), 正是为了让部分写避免 EC 读-改-写放大。
9. 读协议(分片的校验字节范围读)
输入:分片相对范围 [O, O+L)
first = O / bitrot_block ; last = (O + L - 1) / bitrot_block
data_start = 4096 + align_up(block_count*32, 4096)
for i in first..=last:
blk = read(data_start + i*bitrot_block, this_block_len(i)) # 可 O_DIRECT
h = read_hash_entry(i) # 32 字节
if HighwayHash256(key_i, blk) != h: return BitrotError(i)
return concat(blocks)[ O - first*bitrot_block .. + L ]
发生 BitrotError 时,chunk-store 不让客户端读失败:EC 模式读其他分片重建
受影响的纠删块;Replica 模式读另一副本。坏分片入队后台 heal。
10. 默认值与可调项
参数 默认值 说明
─────────────── ─────────── ──────────────────────────────────────────────
chunk_size 4 MiB 逐文件(Layout)
ec_block_size 1 MiB EC 编码单元;chunk_size 为其倍数
bitrot_block 16 KiB 4×4 KiB;点读密集文件取**更小**(更少校验放大),
顺序取**更大**(更少哈希表开销)。
必须是 4096 的倍数。
csum_algo HighwayHash256
ec_algo rs-vandermonde
目录扇出 2 级 xxh3_64(file_id) 第 0、1 字节
默认下的开销:哈希表 = 每 16 KiB 用 32 B ≈ 分片大小的 0.195%。
11. 容器与 Slice 布局(小文件打包)
小文件(AI 样本/token;node_modules、pip/conda、构建临时——动辄数十亿个)不 作为各自的分片存储。它们以 needle 的形式打包进 容器(container),文件的 inode 持有一个 Slice 指针(见 RUSTFS-MD-DESIGN-005 §3.5)。本节定义 on-disk needle 布局、Slice 指针字段,以及一次 slice 读如何映射到 §6 的 chunk↔shard 映射与 §9 的校验读——从而让打包原样复用既有 chunk 格式、EC 与 bitrot。
11.1 容器(Container)
容器是一个大的**内部**文件(默认容量 256 MiB),位于保留命名空间。它**完全**像
一个普通大文件那样存储:
· 自己的 file_id(= container_id),
· 自己的 Layout(公式放置 + EC,或 Replica)——DESIGN-005 §3.2,
· 它的字节就是 §2 分片格式的 chunk,照常 EC / bitrot。
容器在达到容量前为追加写,封存(SEAL)后不可变(适合 EC)。容器存储策略在**容器
层级**遵循 大小×访问模式 规则(一次写入数据集打包 → EC;高 churn 临时打包 →
Replica)。这对下面的 needle 层透明。
11.2 Needle(容器内的一个小文件)
容器的逻辑字节流是一串 needle:
Needle = [ NeedleHeader (32 B) | Data (data_len 字节) | 补齐到 needle_align ]
NeedleHeader(小端,32 字节):
偏移 大小 字段 说明
0 4 magic "RFND" (0x52464E44)
4 4 flags bit0 已删除, bit1 已压缩, bit2 已加密
8 8 file_id 所属 inode(支持 KV-轻量压实与巡检)
16 4 data_len Data 的逻辑字节数
20 4 cookie 随机;必须与 Slice 指针匹配(防陈旧/防损坏)
24 4 data_crc32c 对 Data 的 CRC32C(needle 级快速校验)
28 4 reserved 0
needle_align : 默认 8 字节。needle 起点在容器逻辑流中是 needle_align 的倍数。
每个 needle 浪费 ≤ needle_align-1 字节。
对齐分层(重要):needle_align 仅用于**打包密度**。
设备 / O_DIRECT / EC-fragment 对齐完全由 chunk 层(§2/§5/§6)在读取容器字节时
处理。needle **不**需要设备对齐——因此微小文件以 8 B 粒度紧密打包,而读取在
下层 chunk 层依然对 O_DIRECT 友好。
11.3 Slice 指针(存于小文件的 inode)
inode.body = Slice {
container_id : u64, // 容器的 file_id
offset : u64, // NeedleHeader 在容器流中的字节偏移
length : u32, // = data_len(无需解析头部即可确定读取大小)
cookie : u32, // 必须等于 NeedleHeader.cookie
}
这是唯一的逐小文件数据结构,且就放在 inode 行(KV)里。没有单独的小文件索引, KV 中也没有数据本体。
11.4 Slice 读 → EC 部分读(偏移映射)
小文件读完全复用 §6 + §9;数据路径上无任何新东西:
给定 Slice{container_id, offset, length} 与已缓存的容器 Layout:
1. 要取的容器字节范围:[offset, offset + 32 + length)
(头部 + 数据;读头部以校验 magic/file_id/cookie)。
2. 把容器偏移 Oc → 容器 chunk 与 EC 分片(这就是 §6):
chunk_index = Oc / container.chunk_size
off_in_chunk = Oc % container.chunk_size
(eb_index, data_shard, shard_offset) = §6( off_in_chunk, EB, k )
3. 对容器分片发起 §9 校验字节范围读:
bitrot 校验;EC 部分读(只触及覆盖该范围的数据分片;除非所需数据分片不健康,
否则不重建)。
4. 解析 NeedleHeader;校验 magic == "RFND"、file_id == inode、cookie ==
Slice.cookie;可选校验 data_crc32c。
5. 返回 Data[0 .. length]。
放大:小于 shard_fragment(= ceil(EB/k))的 needle,常见情形下落在**一个** EC 数据
分片 fragment 内 → 1× 读(一个数据分片)。若跨 fragment 边界则触及 2 个数据分片——
仍无校验块、无重建。于是小文件读继承 LLD-002 的随机读快路径。MDS **不**在此路径上
(客户端缓存容器 Layout,且容器少而热)。
11.5 追加、封存、删除、压实
追加 : 在容器当前追加偏移预留一个 needle 槽(每次写一次原子自增;每写入者/会话
一个**打开**容器以避免跨写入者争用)。把 needle 写入打开容器的 chunk;
然后提交 inode.body = Slice{...}。数据在 inode 提交**之前**已持久(容器
chunk 写,§8)(先数据;崩溃 GC 见 DESIGN-005 §3.4)。
封存 : used ≥ 容量时标记 CMTA.sealed;容器变为不可变,作为普通一次写入 EC 文件定稿。
删除 : 仅逻辑删除——丢弃小文件 inode 并增加 CMTA.dead_bytes。封存容器**不**原地修改
(不可变 / EC)。
压实 : 当 CMTA.dead_bytes/容量 ≥ 阈值(Q6),压实器扫描容器 needle;对每个 file_id
为 F、偏移为 O 的 needle 检查 inode F:若 F.body == Slice{本容器, O, …} 则
needle **存活** → 复制进新的打开容器并事务性更新 F 的 Slice;否则**死亡** →
跳过。完成后释放旧容器。(needle 的 file_id 使这种存活判定无需反向索引。)
11.6 默认值
container_capacity 256 MiB (Q6 可调;越小压实周转越快)
container_policy 一次写入打包用 EC(k,m);高 churn 打包用 Replica
needle_align 8 字节
NeedleHeader 32 字节(定长)
压实触发 dead_bytes/容量 ≥ ~0.4(Q6 可调),相对 I/O 限速
12. 格式版本与兼容性
· 头部的 format_version 决定一切解析。读取器**必须**拒绝不理解的版本。
· 保留头部字节写入时必须为零、读取时忽略;**仅当**字段缺省(零)是合法默认时,
才允许在未来小修订中无需提升版本地增加字段。
· 对区域布局、哈希、映射的任何改动都**要求**提升 format_version 并提供新版读取器。
· 不存在从旧格式的迁移:这是全新构建系统(RUSTFS-MD-DESIGN-005)。
format_version 1 是 GA 时的第一个也是唯一格式。
13. 黄金测试向量(互通契约)
两个独立实现必须逐字节一致。参考实现生成并提交黄金向量于 testdata/chunk/,
每个向量 = { params.json, input.bin, expected.shard }。
一个向量至少必须覆盖:
G1 Replica,单个完整 bitrot block(data_len = bitrot_block)。
G2 Replica,短尾末块(data_len = 1.5 × bitrot_block)。
G3 EC(4,2) 数据分片 0,多 EC-block 的 chunk(chunk = 4 MiB,EB = 1 MiB)。
G4 EC(4,2) 校验分片(shard_index = 4),与 G3 同一 chunk。
G5 压缩 + 加密载荷(flags 位 1、2),验证 bitrot 在变换后字节上计算。
G6 位置绑定:把 G3 的一个有效块放到错误的 block_index,HighwayHash 校验**必须**
失败(负向向量)。
G7 打包(Replica 容器):向一个容器写入 3 个 needle;各自经 Slice 逐字节读回;
校验 NeedleHeader 的 magic/file_id/cookie;cookie 错误的 Slice **必须**被拒
(负向向量)。
G8 打包(EC 容器):完全落在一个 shard_fragment 内的 needle 只从**一个**数据分片
读取(EC 部分读,无重建);跨 fragment 边界的 needle 恰从**两个**数据分片读取。
13.1 结构化示例(G2,为说明用极小尺寸)
用示意性的 bitrot_block = 16 字节(真实默认为 16 KiB),Replica 模式,
shard_data_len = 24(一个完整块 + 一个 8 字节短块):
block_count = ceil(24/16) = 2
Header : 字节 [0,4096) magic="RFSHARD1", format_version=1, flags=0x0008
(last_block_short), bitrot_block=16, shard_data_len=24,
block_count=2, ec_k=1, ec_m=0, ec_algo=0, csum_algo=1, ...
HashTable: 字节 [4096,8192)(align_up(2*32=64, 4096)=4096)
[4096,4128) = HighwayHash256(key_0, data[0..16]) # 32 B
[4128,4160) = HighwayHash256(key_1, data[16..24]) # 32 B
[4160,8192) = 0x00 填充
Data : 字节 [8192, 8192+24) = 24 个载荷字节
文件为 O_DIRECT 补齐到 8192+4096(下一个 4 KiB);填充被忽略。
(真实的 32 字节摘要由参考 HighwayHash256 按 §4.1 密钥生成并提交于
expected.shard,此处无法手算。上述结构偏移是规范契约。)
14. 合规检查清单(M0 退出条件)
□ Header 编解码全部字段;读取时校验 header_crc32c。
□ 拆分布局偏移完全如 §2;Data 从 4 KiB 对齐处开始。
□ 实现 §4.1 的 HighwayHash256 密钥(位置绑定)。
□ Chunk↔shard 映射(§6)与 rustfs-placement 的预期一致。
□ 实现路径方案(§7)与原子写(§8.1)。
□ Replica 模式的部分 RMW(§8.2);撕裂写经修复容忍。
□ 校验读(§9)在不匹配时返回 BitrotError;绝不静默。
□ NeedleHeader(§11.2)编解码;校验 magic + file_id + cookie + data_crc32c;
Slice 读(§11.4)映射到 §6/§9(容器范围 → EC 部分读)。
□ 小于 shard_fragment 的 needle 触及 ≤ 2 个数据分片,健康数据下无重建;压实存活
判定经 needle.file_id ↔ inode.Slice;封存容器绝不原地修改。
□ 两个独立解码器均通过全部黄金向量 G1–G8。
□ format_version 闸门拒绝未知版本。
英文镜像:rustfs-chunk-format-EN.md。本规范是 RUSTFS-PLAN-001 的 M0 冻结产物; 它支撑 chunk-store(Track B)与 rustfs-placement。