RustFS 文档 文档

RustFS 元数据 — 全新构建最终设计(中文版)

RustFS 元数据 — 全新构建最终设计(中文版)

文档编号:   RUSTFS-MD-DESIGN-005(权威 / 单一事实来源)
状态:       草案
版本:       1.0.0
前提:       没有历史数据。全新构建。永不迁移。
归并:       RUSTFS-MD-DESIGN-002(内核)+ 004(无共存),并**彻底废止**
            CHG-001 与 DESIGN-003 的迁移/共存内容。
语言:       中文  (English mirror: rustfs-final-greenfield-EN.md)

1. 新前提及其删除项

没有历史数据。系统全新构建。因此:

彻底删除(不再属于任何方案):
  ✗ 一切迁移(无 rustfs-migrate 工具、无重条带化、无选项 A/B 抉择)
  ✗ xl.meta 的一切形态——运行时没有、迁移输入也没有、根本没有
  ✗ 一切遗留解码待办(O1 旧 bitrot、O5 复现旧 EcDist……)
  ✗ 任何向后兼容脚手架

结果:设计纯粹向前。系统见到的唯一数据,是它以自己的格式亲手写下的数据。

2. 原则重述:"最小改动" → "最大复用"

早先"对 RustFS chunk 改动最小"的目标,是为了不破坏运行中的数据。既无历史数据, 重述为:

复用经过验证、难做对的算法(Reed-Solomon、bitrot、磁盘 I/O、heal 重建)。 在其上构建一个干净的、公式原生的 chunk 服务。不带入任何对象存储 / xl.meta 语义。

作为库复用(不重写):    ecstore-core(RS 编解码)、bitrot 读写器、磁盘 I/O 层、
                        heal 重建数学、quorum 逻辑。

全新构建(干净、无遗留):chunk 服务外壳、on-disk chunk 格式、公式放置、KV 元数据。

永久丢弃:                SetDisks 对象语义、xl.meta、对象→集合的名称哈希、DDir、
                        EcDist 存储、按版本的对象目录布局。

由于 on-disk chunk 格式从零定义,我们将其设计为自校验(内联流式 bitrot)—— 因此元数据从不携带逐块哈希,解码路径只需布局参数。


3. 权威架构(全貌,一页)

        客户端                         无状态 MDS               事务型 KV
  ┌──────────────────┐          ┌────────────────────┐      ┌──────────────────┐
  │ 元数据 RPC stub   │──控制────▶ 文件系统语义        │──事务─▶ INOD / DENT /    │
  │ 放置引擎          │          │ inode 区段分配      │      │ VIDX / VOLU /    │
  │ 集群映射表缓存    │◀─布局────│ 放置引擎            │      │ EXTM / PEND/DELQ │
  │ chunk I/O ────────┼─数据─┐   │ (无数据路径)       │      │ (TiKV / 内嵌)    │
  └──────────────────┘      │   └────────────────────┘      └──────────────────┘
                            ▼
                   ┌──────────────────────────────────────────┐
                   │ CHUNK 服务(全新外壳)                     │
                   │  · 公式原生、按 chunk-key 寻址             │
                   │  · 自校验的 on-disk chunk 格式             │
                   │  复用:RS · bitrot · 磁盘 I/O · heal       │
                   └──────────────────────────────────────────┘

3.1 元数据 = KV 行

INOD | inode_id          → InodeAttr { size, mode, uid, gid, times, kind, nlink,
                                       body, sys_meta }
                           body = Inline(bytes)               # 仅 ≤ ~1 KB(罕见)
                                | Slice{container_id,off,len} # 小文件 → 打包
                                | OwnLayout(Layout)           # 大文件 → 自有 chunk
DENT | parent_ino | name → { child_ino, kind }
VIDX | bucket | key      → [ { version_id, inode_id, mtime, delete_marker } ]
EXTM | volume_id | extent → ExtentEntry        (块卷——唯一的索引)
VOLU | volume_id         → Volume
CMTA | container_id      → ContainerMeta { layout, capacity, used, dead_bytes,
                                           sealed }            (小文件打包)
IALOC                     → 批量 inode 计数器
PEND | file_id           → 写意图(崩溃 GC)
DELQ | file_id           → 删除墓碑(chunk GC)
MPUP/MPRT | upload_id...  → 分段暂存

KV 只存元数据——绝不存文件数据本体。 Inline 仅保留给退化的 ≤ ~1 KB 情形, 不是小文件方案(见 §3.5)。AI 数据集与代码/依赖树(node_modules、pip/conda) 天然有亿万级小文件——它们的元数据(inode + dentry 行)在 KV 中可扩展,正如 3FS 所证;但它们的数据绝不能 inline。

3.2 布局 = 纯公式(每文件一份描述符,内联于 inode)

struct Layout {
    file_id: u64, ec: EcParams, stripe_unit: u32, chunk_size: u64,
    placement_seed: u64, cluster_map_epoch: u64,
}
// 所有位置均计算得出,逐 chunk 零存储:
//   set    = HRW(file_id, chunk_index, seed, cluster_map[epoch])
//   path   = deterministic(file_id, version, chunk_index)
//   dist   = deterministic(file_id, chunk_index)
//   hashes = 内联于分片(自校验)

3.3 数据面 = 客户端 ↔ chunk 直连(MDS 永不在路径上)

open():  客户端一次性取得 Layout + 集群映射 epoch → 缓存(对一次写入文件布局不可变
         → 在文件生命周期内缓存)
read():  客户端本地计算位置 → ReadChunk(chunk_key, off, len) 直连 chunk 服务器。
         零 MDS 流量。
write(): 客户端 → WriteChunk 直连 chunk 服务器(RS 编码在服务内部进行);
         随后 MDS CommitWrite(size, mtime)——仅元数据。

3.4 保持简单的干净原语

极小文件(≤~1KB) → inode.body = Inline(罕见;仅退化优化)。
小文件            → 打包进容器(§3.5)——亿万级小文件的解法。
                    inode.body = Slice{container_id, off, len}。
大文件            → inode.body = OwnLayout(§3.2 的公式路径)。
版本              → VIDX;每个版本是自己的 inode + 确定性 chunk 路径。
分段              → 暂存 parts;Complete 时写入 formula chunk(仅当 part 边界不对齐
                    时重条带化——少见)。
块卷              → EXTM 区段映射表(精简/COW),唯一的枚举索引。
崩溃一致性        → 先数据,后 KV 提交;PEND 写意图 + GC 巡查器。

3.5 小文件打包(一等公民——亿万级小文件)

AI 数据集(数百万微小样本/token)与代码/依赖树(node_modules、pip/conda、 构建产物)动辄数亿到数十亿小文件。把它们的数据塞进 KV 是致命的;为每个微小 文件单独做 EC/副本分片又会吃掉每分片的固定开销底。解法是打包,且与公式布局 天然契合:

容器(Container) = 一个大的内部对象(如 256 MiB),位于保留的内部命名空间。它在
                   内部就是一个普通大文件 → 自己的 OwnLayout、公式放置、EC。
                   完全复用大文件 chunk 路径。

写              = 把小文件字节追加进该写入者当前打开的容器;预留
                  (container_id, offset, length);提交 inode,body =
                  Slice{container_id, offset, length}。容器达到上限即封存
                  → 封存后不可变 → 适合 EC。

读              = inode → Slice{container_id, offset, length} → 容器的 Layout
                  (可缓存;容器极少且热)→ 计算 chunk 位置 → 直接向 chunk
                  服务器做字节范围读。MDS 不在数据路径上。小切片落在一个 EC
                  数据分片 fragment 内 → EC 部分读 ~1x 放大(复用 LLD-002 随机读)。

删除/GC         = 给切片打墓碑;按容器记 dead_bytes(CMTA)。后台压实器把稀疏
                  容器里的存活切片重打包进新容器、再释放旧容器(log-structured GC)。

规模核算:10 亿 × 32 KiB 文件 → 数据打包进约 13 万个容器(各 256 MiB),高效 EC (~1.5x,而非小文件分片底);元数据约 10 亿 inode + dentry 行在 KV(~几百 GB, 分布式——KV 的本职)。这就是 Haystack / SeaweedFS 模型,适配到我们的公式布局。

代价(已接受,见 PLAN-001 Track B):压实 GC;批量快速建小文件的客户端批处理; 每容器 append 偏移预留(用每写入者/会话一个打开容器来限制争用)。


4. Chunk 服务:复用算法,构建干净外壳

需记录的一个设计决策:

决策:构建一个**全新的**薄 ChunkStore,直接调用被复用的库,而非改造既有的、
      面向对象的 SetDisks。

原因:SetDisks 把 EC + 分布 + xl.meta + 对象级 quorum/锁 耦合在一起。既无遗留数据,
      改造它会把对象存储假设带入 chunk 层。一个组合 ecstore-core(RS)、磁盘层、
      bitrot/heal 模块的全新 ChunkStore 更干净、且公式原生。

代价:比改造多一些新代码——但这是彻底迭代的选择,避免继承我们永不使用的对象语义。

ChunkStore 接口(按 chunk-key 寻址、字节范围、自校验):
  write_chunk(chunk_key, offset, data, sync) -> ()
  read_chunk(chunk_key, offset, length)      -> bytes   (校验内联 bitrot)
  delete_chunk(chunk_key)                    -> ()
  // RS 编解码、分片放置(按公式)、bitrot、heal:复用库

5. 构建计划(无迁移——只是构建)

B1  rustfs-kv:TransactionalKv trait + 内嵌后端(redb/RocksDB)+ TiKV。
B2  rustfs-inode:InodeAttr、Layout、DentValue、Volume、ExtentEntry 类型。
B3  rustfs-placement:共享 HRW 放置 + 集群映射表(MDS 与客户端共用)。
B4  rustfs-mds:无状态服务、inode 区段分配器、文件系统语义事务、
                gRPC(命名空间 + OpenLayout + GetClusterMap + CommitWrite + 卷)。
B5  chunk-service:复用 RS/bitrot/磁盘/heal 的全新 ChunkStore;自校验 on-disk
                   chunk 格式;chunk-key 寻址。
B5b packing:容器管理(append/seal/Slice)、小文件打/解包、压实 GC。
                   复用 B5(容器即大的内部文件)。
B6  rustfs-client / FUSE:放置引擎、集群映射缓存、直连 chunk I/O、
                   预读(顺序)+ 随机读路径(LLD-002);
                   客户端把小文件写批量化为容器 append。
B7  S3 + 块(iSCSI/NVMe-oF)前端,构建于同一 MDS + ChunkStore 之上。
B8  HA:将 MDS 指向 TiKV → 免费获得分布 + HA。无自研 Raft。

没有接缝/双写/切换/迁移的阶段——这些概念已不存在。


6. 剩下的少数真问题

Q1  KV 后端:TiKV(Rust 原生、集群、推荐)对比 内嵌 redb/RocksDB(单机)。
    先交付内嵌,再为 HA 加 TiKV。
Q2  inline 上限(仅退化的 ≤~1 KB——海量小文件走打包 §3.5,**不** inline)。
    inline 保持极小,使 KV 永不持有数据本体。
Q3  定义 on-disk chunk 格式:stripe_unit、内联 bitrot 块大小、chunk-key 路径方案。
    (自由选择——无遗留约束。)
Q4  按 大小 × 访问模式 定默认存储策略:
      极小 ≤~1KB → Inline;小 → 打包容器(§3.5);大 ≥~1MiB → EC OwnLayout;
      随机/部分写(块卷、可变文件)→ 副本(副本数 = m+1,各档容错一致)。
Q5  ChunkStore:确认"全新外壳"决策 vs 改造 SetDisks(§4)。
Q6  容器大小 + 打包局部性(按写入者/时间,或可选按目录子树)+ 压实触发阈值
    (dead_bytes 比例)。

这些都是向前的设计选择,而非兼容性约束。


7. 小结

前提     : 无历史数据 → 全新构建 → 无迁移、无 xl.meta、处处皆无。
原则     : 复用经验证的 RS/bitrot/磁盘/heal;构建干净的公式原生外壳。
元数据   : 事务型 KV 之上的无状态 MDS;INOD/DENT/VIDX 行。KV 只存元数据,绝不存
           文件数据本体。
布局     : 纯公式;逐 chunk 零元数据;分片自校验。
小文件   : 打包进大容器(inode 存 Slice 指针);亿万级小文件可扩展;KV 永不 inline
           数据本体。
数据面   : 客户端 ↔ chunk 直连;MDS 永不在数据路径上。
Chunk    : 全新薄 ChunkStore,复用硬算法;无对象/xl.meta。
HA       : TiKV 提供分布 + 一致性;无自研共识。
共存/迁移: 无。系统只见自己的格式。

这就是简化的终态:单一原生模型,经验证的数学作为库复用,毫无历史包袱。


英文镜像:rustfs-final-greenfield-EN.md。权威设计;废止 RUSTFS-CHG-001 与 RUSTFS-MD-DESIGN-003 的迁移/共存内容。