RustFS 文档 文档

RustFS 元数据结构规范(中文版)

RustFS 元数据结构规范(中文版)

文档编号:   RUSTFS-SPEC-META-001
状态:       草案——里程碑 M0/M1 的冻结目标
版本:       struct_version = 1
范围:       对每一种元数据记录的**完整**规范定义:KV 键编码与值结构、命名空间组织、
            引用不变式、事务原子性。SPEC-CHUNK-001 的元数据对位规范。收敛并取代
            MD-DESIGN-002/004/005 中散落的元数据定义。
语言:       中文  (English mirror: rustfs-metadata-structures-EN.md)

1. 约定

· 存储:事务型 KV(TiKV / FoundationDB / 内嵌 redb/RocksDB)。
· 键:  字节串。首字节 = 表标签(§2)。键中多字节整数为大端(BE),但点查表中的
        inode/file/container id 用小端(LE),以把顺序分配的 id 均匀散布到 KV 分片。
· 值:  1 字节 struct_version(=1),其后为该结构的 MessagePack。读取器**必须**拒绝
        不支持的 struct_version。(XATR 值为不透明字节。)
· 字符串:UTF-8。路径分量不得含 '\0' 或 '/'。
· 时间:Timespec { secs: i64, nanos: u32 }(UTC)。
· 字节:长度定界的二进制。

2. KV 键空间

标签  表      键布局(1 字节标签之后)                     值(§)
────  ──────  ──────────────────────────────────────────  ─────────
0x01  INOD    inode_id : u64 LE                            InodeAttr      §5
0x02  DENT    parent_ino : u64 BE | name                   DentValue      §6
0x03  VIDX    parent_ino : u64 BE | name                   VersionList    §7
0x04  XATR    inode_id : u64 LE | xattr_name               Bytes(不透明)  §8
0x05  EXTM    volume_id : u128 BE | extent_index : u64 BE   ExtentEntry    §10.3
0x06  VOLU    volume_id : u128 LE                           Volume         §10.1
0x07  SNAP    snapshot_id : u128 LE                         Snapshot       §10.2
0x08  CMAP    epoch : u64 BE                                ClusterMap     §11
0x09  CMAPC   (单例)                                        u64 epoch      §11
0x0A  IALOC   (单例)                                        u64 计数器     §12
0x0B  PEND    file_id : u64 LE                              WriteIntent    §13.1
0x0C  DELQ    file_id : u64 LE                              DeleteTomb     §13.2
0x0D  CMTA    container_id : u64 LE                         ContainerMeta  §14
0x0E  MPUP    upload_id : u128 LE                           MultipartState §15
0x0F  MPRT    upload_id : u128 BE | part_no : u32 BE        PartInfo       §15
0x10  BUCK    bucket_ino : u64 LE                           BucketConfig   §9
0x11  STAT    inode_id : u64 LE                             DirStat        §16
0x12  QUOT    inode_id : u64 LE                             Quota          §16
0x13  IDEN    principal_id : utf8                           Identity       §17
0x14  AKEY    access_key_id : utf8                          AccessKey      §17
0x15  LOCK    inode_id : u64 LE | start : u64 BE | len:u64 BE LockEntry    §18

DENT/VIDX 用 BE parent_ino 作分组前缀 → 一个目录的子项构成连续区间(readdir = 一次有序扫描;lookup = 一次点查)。点查表用 LE id,把顺序分配散布到分片(无写热点)。


3. 标识类型与保留 inode

type InodeId = u64; type FileId = u64; type ContainerId = u64;
type VolumeId = u128; type SnapshotId = u128; type VersionId = u128; type Epoch = u64;

const NIL_INODE:    InodeId = 0;
const ROOT_INODE:   InodeId = 1;   // "/"
const FS_ROOT:      InodeId = 2;   // "/fs"       POSIX 命名空间
const BUCKETS_ROOT: InodeId = 3;   // "/buckets"  S3 桶
const VOLUMES_ROOT: InodeId = 4;   // "/volumes"  块卷
const SYS_ROOT:     InodeId = 5;   // "/.sys"     保留内部
// inode 1..15 保留并在格式化时预建;用户/容器 id 从 16 起。

文件 inode id 与容器 id 取自同一 IALOC 计数器(§12),互不冲突。


4. 命名空间组织(文件 + 对象 + 块,同一棵树)

单一全局树(这是"统一"的关键)。保留布局:
  /          ROOT_INODE
  /fs        POSIX 命名空间根
  /buckets   每个子目录 = 一个 S3 桶(其配置在 BUCK,§9)
  /volumes   每个子项 = 一个 BlockVolume inode(其数据在 VOLU/EXTM,§10)
  /.sys      保留内部(容器**无** dentry;只作为 CMTA + 公式 chunk 存在,§14)

S3 key ↔ 目录映射(统一):
  桶 B 中的对象键 "a/b/c.txt"  →  路径 /buckets/B/a/b/c.txt。
  因为文件与对象共享**一棵**树,PUT "a/b/c.txt" 会创建中间 Directory inode a/、b/
  (mkdir -p),使 POSIX 挂载能看到它们;ListObjects(prefix, delimiter='/') 映射为
  对相应目录的 readdir。以 '/' 结尾的键(目录标记)→ 空 body 的 Directory inode。
  代价:在某前缀下首次 PUT 时隐式建目录(每个新分量一次 mkdir,在同一创建事务内)。
  收益:前缀列举为 O(1)/目录。

5. INOD — InodeAttr(中心记录)

struct InodeAttr {
    ino:    InodeId,
    kind:   InodeKind,        // §5.1
    size:   u64,              // 逻辑字节(目录:0)
    mode:   u16,              // 权限位 rwxrwxrwx + suid|sgid|sticky(12 位)
    uid:    u32,
    gid:    u32,
    nlink:  u32,              // 文件:硬链接数;目录:2 + 子目录数(§20)
    atime:  Timespec, mtime: Timespec, ctime: Timespec, btime: Timespec,
    body:   InodeBody,        // §5.2 — 数据如何/在何处
    vol:    Option<VolumeId>, // 当 kind==BlockVolume 时存在 → 活的 VOLU(vol)
    sys:    SysMeta,          // §5.6
    gen:    u64,              // 每次 attr/body 变更递增(缓存校验)
}

5.1 InodeKind

enum InodeKind { RegularFile=0, Directory=1, Symlink=2, BlockVolume=3 }

5.2 InodeBody

enum InodeBody {
    Empty,            // 0 字节 / 目录 / 块卷
    Inline(Bytes),    // ≤ inline_cap(§5.3);极小文件与符号链接目标
    Slice(SliceRef),  // 打包进容器的小文件(§5.4)
    Layout(Layout),   // 拥有自有公式 chunk 的大文件(§5.5)
    Segmented(SegmentList), // 分段上传完成的对象:一张 part 表(§23.1)
}
struct SegmentList { segments: Vec<Segment> }   // 连续且不相交地铺满 [0,size)
struct Segment { offset: u64, length: u64, part_file_id: FileId, layout: Layout }

Segmented 仅用于分段上传完成的对象(大且少);读取偏移 O 时二分 segments,再在该 段的 part_file_id 内按公式解析。单段上传直接收敛为 Layout。

5.3 Inline 上限(仅退化)

Inline 仅用于 ≤ ~1 KiB(符号链接目标、近空文件)。**不是**小文件方案(海量小文件
走 Slice/打包)。保持 inline 极小,确保 KV 永不持有数据本体。按后端配置;默认 1 KiB。

5.4 SliceRef(打包小文件 → 容器 needle)

struct SliceRef { container_id: ContainerId, offset: u64, length: u32, cookie: u32 }
// cookie 必须等于 NeedleHeader.cookie(SPEC-CHUNK-001 §11)。

5.5 Layout(大文件自有 chunk——纯公式)与 Policy

struct Layout {
    file_id: FileId,           // == ino
    policy:  Policy,
    stripe_unit: u32,          // EC 随机读放大控制
    chunk_size:  u64,
    placement_seed: u64,
    cluster_map_epoch: Epoch,
}
enum Policy {
    ErasureCoded { algo: u8, k: u16, m: u16, ec_block_size: u64, csum_algo: u8 },
    Replicated   { factor: u8, csum_algo: u8 },     // factor = m+1 以等同容错
}

所有 chunk 位置由 (file_id, chunk_index, version) + seed + CMAP[epoch] 计算得出 (SPEC-CHUNK-001 §6/§7);逐 chunk 零存储。对一次写入文件不可变 → 客户端整生命周期缓存。

5.6 SysMeta(S3 / 分层 / 复制 / 加密)

struct SysMeta {
    content_type: Option<String>, content_encoding: Option<String>,
    etag: Option<String>, storage_class: Option<String>,
    user_meta: BTreeMap<String,String>,   // x-amz-meta-*(小;大 → XATR)
    tags: BTreeMap<String,String>,         // S3 对象标签
    checksums: BTreeMap<u8,Bytes>,         // 算法→摘要(CRC32C/SHA256/…)
    encryption: Option<EncryptionInfo>,    // §5.7
    transition: Option<TransitionInfo>, restore: Option<RestoreInfo>,
    repl_status: Option<u8>,               // 0 pending,1 done,2 failed,3 replica
    acl: Option<String>,                   // 对象 ACL(若使用)
}
struct TransitionInfo { tier: String, status: u8, transitioned_version: Option<VersionId> }
struct RestoreInfo { ongoing: bool, expiry: Option<Timespec> }

5.7 EncryptionInfo(SSE)

struct EncryptionInfo {
    algo: u8,                  // 0 none,1 AES-256-GCM,2 AES-256-CTR,3 SSE-KMS
    key_ref: Option<String>,   // KMS 密钥 id / keyring 引用(**绝不**存密钥本身)
    iv: Bytes,                 // 逐对象 nonce/IV
    dek_wrapped: Option<Bytes>,// 信封加密:被包裹的数据加密密钥
}
// 存在 ⇔ chunk 的 SPEC-CHUNK-001 flags.encrypted 置位;bitrot 在密文(变换后)上计算。

6. DENT — DentValue

struct DentValue { child_ino: InodeId, kind_tag: u8 }  // kind_tag = InodeKind 判别

键 0x02 | parent_ino(BE) | name。readdir = 对 0x02|parent 范围扫描;lookup = 一次点查。


7. VIDX — VersionList(S3 版本;可选)

DENT.child_ino 永远指向当前版本 inode(快路径);VIDX 记录完整版本链(仅当开 版本且有历史时)。

struct VersionList { latest: u32, entries: Vec<VersionEntry> }  // 新在前
struct VersionEntry { version_id: VersionId, inode_id: InodeId, mtime: Timespec,
                      flags: u8 /*bit0 delete_marker, bit1 is_latest*/ }

8. XATR — 扩展属性

键 = 0x04 | inode_id(LE) | xattr_name        值 = 原始字节(不透明)

小的 S3 user-meta 内联于 SysMeta;任意/大的 POSIX xattr 用 XATR。 listxattr(ino) = 对 0x04 | inode_id 范围扫描。


9. BUCK — BucketConfig

// 键 = 0x10 | bucket_ino(LE)   (即 /buckets/<name> 目录 inode)
struct BucketConfig {
    bucket_ino: InodeId, name: String, owner: String, created: Timespec,
    versioning: u8,                       // 0 关闭,1 开启,2 暂停
    object_lock: Option<ObjectLock>,
    lifecycle:  Vec<LifecycleRule>,
    policy:     Option<String>,           // JSON 桶策略
    cors:       Option<String>,
    default_enc: Option<EncryptionInfo>,  // 新对象的 SSE 默认
    tags:       BTreeMap<String,String>,
    quota_bytes: Option<u64>,
}
struct ObjectLock { mode: u8, retain_days: u32, legal_hold_default: bool }
struct LifecycleRule { id: String, prefix: String, expire_days: Option<u32>,
    transition_days: Option<u32>, transition_tier: Option<String>,
    noncurrent_expire_days: Option<u32> }

桶就是 /buckets 下的一个 Directory inode;BUCK 保存其 S3 专属配置。


10. 块卷

10.1 VOLU — Volume

struct Volume {
    id: VolumeId, name: String, ino: InodeId, capacity: u64,
    block_size: u32, chunk_size: u64, provisioning: u8 /*0 精简,1 厚*/,
    state: u8 /*0 可用,1 已挂载,2 快照中,3 删除中*/,
    replica_factor: u8, csum_algo: u8, created: Timespec, snapshots: Vec<SnapshotId>,
}

卷区段公式放置:chunk_key = f(volume_id, extent_index, version)。

10.2 SNAP — Snapshot

struct Snapshot { id: SnapshotId, volume_id: VolumeId, created: Timespec,
                  capacity: u64, cow_version: u64 }

10.3 EXTM — ExtentEntry(稀疏区段映射——唯一的枚举索引)

// 键 = 0x05 | volume_id(BE) | extent_index(BE)
struct ExtentEntry { version: u64 /*COW→选 chunk_key*/, flags: u8 /*0 dirty,1 cow_shared*/ }

缺失的区段 ⇒ 未写 ⇒ 读零(精简)。对 cow_shared 区段写入会递增 version → 新 chunk_key → 写入 → 更新条目。


11. CMAP / CMAPC — 集群映射表

struct ClusterMap { epoch: Epoch, sets: Vec<ErasureSet> }
struct ErasureSet { id: u32, weight: f64, failure_domain: u32, members: Vec<DiskRef> }
struct DiskRef { node_id: u32, disk_id: u32, endpoint: String, status: u8 } // 0 up,1 healing,2 down

CMAPC(单例)= 当前 epoch。仅拓扑变化时改变。极小;客户端与 MDS 缓存(共享 rustfs-placement)。


12. IALOC — id 分配器

键 0x0A(单例)→ u64 `next`。每个无状态 MDS 用一次 CAS 预留一**段** `batch`
(如 16384)id,随后在内存分发。文件 inode id 与容器 id 取自这一计数器(不冲突)。
id 从 16 起。

13. GC 行

13.1 PEND — WriteIntent

// 键 = 0x0B | file_id(LE)
struct WriteIntent { layout: Layout, started: Timespec }

在新 OwnLayout 文件首个 chunk 写入前写入;commit_write 时删除。巡查器删除任何 inode 从未提交的陈旧 PEND 的公式定位 chunk。

13.2 DELQ — DeleteTombstone

// 键 = 0x0C | file_id(LE)
struct DeleteTombstone { layout: Layout, size: u64, deleted: Timespec }

OwnLayout 文件 nlink→0 时由 unlink 写入。GC 计算 ceil(size/chunk_size) 个 chunk 并逐个按公式删除,再移除 DELQ。打包(Slice)文件无需 DELQ——unlink 递增 CMTA.dead_bytes;压实回收(§14)。


14. CMTA — ContainerMeta(小文件打包)

// 键 = 0x0D | container_id(LE)
struct ContainerMeta {
    container_id: ContainerId, layout: Layout, capacity: u64, used: u64,
    dead_bytes: u64, sealed: bool, created: Timespec,
}

容器是内部大文件(公式 chunk,SPEC-CHUNK-001 §11),无 inode、无 dentry。 追加/封存/删除/压实见 SPEC-CHUNK-001 §11.5。


15. MPUP / MPRT — 分段上传(S3)

// 键 = 0x0E | upload_id(LE)
struct MultipartState { bucket_ino: InodeId, key: String, started: Timespec,
                        init_meta: SysMeta, policy: Policy /*目标对象策略*/ }
// 键 = 0x0F | upload_id(BE) | part_no(BE)
struct PartInfo { size: u64, etag: String, checksums: BTreeMap<u8,Bytes>,
                  part_file_id: FileId, layout: Layout }  // 自包含暂存

每个 part 以自己的 part_file_id 自包含暂存(UploadPart,§23.1);最终偏移在 Complete 前未知。Complete 由各 part 构建 Segmented body(不重条带化)——完整状态机见 §23.1。 Abort/过期 GC 全部暂存 part_file_id(§23.2)。


16. 用量与配额

// STAT — 递归子树统计(咨询性)
// 键 = 0x11 | inode_id(LE)
struct DirStat { files: u64, dirs: u64, bytes: u64, updated: Timespec }
// QUOT — 子树配额(桶或目录)
// 键 = 0x12 | inode_id(LE)
struct Quota { max_bytes: Option<u64>, max_inodes: Option<u64> }

DirStat 惰性维护(递归统计严格一致代价高):后台聚合器或最终一致计数。S3 桶用量读 桶 inode 的 DirStat。配额检查可用(略陈旧的)DirStat——咨询性,绝不门禁正确性(I13)。


17. 身份与访问(最小;完整 IAM 在上层)

// IDEN — 键 = 0x13 | principal_id(utf8)
struct Identity { principal_id: String, display: String, created: Timespec, groups: Vec<String> }
// AKEY — 键 = 0x14 | access_key_id(utf8)
struct AccessKey { access_key_id: String, secret_hash: Bytes, principal_id: String,
                   created: Timespec, disabled: bool }

S3/STS 鉴权:AKEY→principal,再据 BUCK.policy + 对象 SysMeta.acl 授权。 角色/联合/内联策略不在 v1 核心范围。


18. LOCK — POSIX 咨询锁(v1 可选)

// 键 = 0x15 | inode_id(LE) | start(u64 BE) | len(u64 BE)
struct LockEntry { owner: u64 /*客户端/会话*/, kind: u8 /*0 读,1 写*/, acquired: Timespec }

事务性;冲突由 MDS 解决。默认暂缓(AI 训练极少需要字节范围锁);定义该记录以使 schema 完整。


19. 可变文件、稀疏与 truncate 语义

OwnLayout 文件**不**假设只追加:
· 稀疏    : 从未写过的 chunk_index **不存在**;读取在 `size` 内返回零。
            现存 chunk ⊆ [0, ceil(size/chunk_size))。
· 增长/追加 : 写过 EOF 抬高 size;新 chunk 按公式;commit_write 更新 size/mtime/gen;
            Layout 不变。
· 就地覆写 : 覆写目标 chunk(EC ⇒ 以 bitrot_block 粒度 RMW → 可变/随机写文件**应**
            用 Replicated 策略,DESIGN-005 Q4)。
· truncate 缩到 N : size=N;完全超过 N 的 chunk 按公式删除;边界 chunk 的有效尾部由
            分片头部的 chunk_logical_len 界定(SPEC-CHUNK-001 §3)——其后字节读为零。
· truncate 增长 : size=N,不写 chunk → 空洞稀疏(读零)。
· chunk path 中的 `version` : S3 版本写入与 COW 用**对象版本**;普通 POSIX 就地写保持
            同一 version 并覆写。新 S3 版本创建**新** inode(自有 file_id)→ 自有 chunk
            路径,旧版本数据不受影响。

20. 引用完整性与不变式

I1  每个 DENT.child_ino / VIDX.entries[].inode_id 引用一个存活的 INOD。
I2  文件:nlink == 指向它的 DENT 数。目录:nlink == 2 + 子目录数。
I3  Directory ⇒ body==Empty;Symlink ⇒ body==Inline(target);
    BlockVolume ⇒ body==Empty ∧ vol==Some(v) 且 VOLU(v) 存活。
I4  body==Slice(s) ⇒ CMTA(s.container_id) 存活;读取交叉校验 s.cookie==
    NeedleHeader.cookie ∧ NeedleHeader.file_id==ino。
I5  body==Layout(L) ⇒ L.file_id==ino;chunk 按公式存在(或 PEND 在途)。
I6  VIDX(p,n) 存在 ⇒ 开版本 ∧ 当前项.inode == DENT(p,n).child_ino。
I7  EXTM 稀疏:缺失区段读为零。
I8  container_id 与文件 inode id 不冲突(共享 IALOC)。
I9  gen 在每次 attr/body 变更时递增。
I10 S3 对象 /buckets/B/<key> 是 RegularFile(键以 '/' 结尾则为 Directory);中间分量
    为 Directory inode(隐式 mkdir -p)。
I11 BUCK(bucket_ino) 存在 ⇔ bucket_ino 是 /buckets 的直接子 Directory。
I12 OwnLayout 可稀疏:现存 chunk ⊆ [0, ceil(size/chunk_size))。
I13 DirStat/Quota 为咨询性、可滞后;门禁**策略**,绝不门禁正确性。
I14 SysMeta.encryption 存在 ⇔ 该对象 chunk 的 flags.encrypted 置位。
I15 任何目录都不是自身的祖先;rename **必须**拒绝把目录移入其自身子树(防环)。
I16 rmdir / 目录-rename-覆盖 要求目标目录为空(其前缀下无 DENT 行)。
I17 nlink==0 但 open_count>0 的 inode 为**孤儿**:已从树摘除,保活至最后一次 close
    (无 DENT;chunk GC 推迟到最终 close)。
I18 硬链接只指向同命名空间内的 RegularFile inode;绝不指向 Directory(防环),也不
    跨 文件/对象/卷 保留根。
I19 在已返回名称无并发 rename 的游标扫描下,readdir 对每个存活项恰返回一次;按名序。
I20 分段上传完成的对象 body=Segmented(单段则为 Layout);各段连续且不相交地铺满
    [0,size);每个 part_file_id 的 chunk 按公式存在。
I21 删除标记(VIDX 项,delete_marker=1)⇒ 该名无 DENT;键在 POSIX/ListObjectsV2 中
    不存在,但在 ListObjectVersions 中存在。
I22 中止/过期一个分段上传会移除 MPUP + 其全部 MPRT,并 GC 每个暂存 part_file_id
    (无遗留暂存 chunk)。

21. 事务原子性分组

每项都是恰好一次可串行化 KV 事务;数据在任何元数据提交事务之前已在 chunk 服务器持久。

create(parent,name,attr)      { put INOD; put DENT; bump parent.mtime,gen }
mkdir(parent,name)            { put INOD(dir,nlink=2); put DENT; parent.nlink++;
                                bump parent.mtime,gen }
hardlink(ino,parent,name)     { ino.nlink++ , ino.ctime,gen; put DENT }
symlink(parent,name,target)   { put INOD(Symlink, body=Inline(target)); put DENT }
unlink_ownlayout(parent,name) { del DENT; ino.nlink-- ; if 0 { del INOD; put DELQ } }
unlink_packed(parent,name)    { del DENT; del INOD; CMTA.dead_bytes += hdr+len }
rmdir(parent,name)            { (空) del DENT; del INOD; parent.nlink-- ; mtime,gen }
rename(sp,sn,dp,dn)           { del DENT(sp,sn); put DENT(dp,dn); 处理目标覆盖 +
                                目录移动 nlink 修正; ino.ctime,gen; sp/dp mtime }
create_packed(parent,name,sr) { CMTA.used += hdr+len(预留,写入者打开容器);
                                put INOD(body=Slice); put DENT }
                                // 在 needle 于容器中持久之后
commit_write(ino,size,mtime)  { ino.size=size; mtime; gen++ ; del PEND(ino) }
                                // 在 chunk 持久之后
truncate(ino,N)               { ino.size=N; mtime,gen++ ; 入队 trim > N 的 chunk }
setattr(ino, …)               { 改字段; gen++ }
put_object_s3(B,key,attr)     { mkdir -p 隐式目录(各 put INOD+DENT,父 nlink/mtime);
                                put 对象 INOD+DENT; 若开版本则 upsert VIDX } // 一次事务翻转
create_bucket(name,owner)     { 在 /buckets 下 put Directory INOD; put DENT; put
                                BUCK; BUCKETS_ROOT.nlink++ }
delete_bucket(name)           { (空) del BUCK; del DENT; del INOD; BUCKETS_ROOT.nlink-- }

SSI 冲突由 MDS 重试。跨目录 rename 是一次全局事务。


22. 命名空间操作语义(精确行为)

本节钉死 §21 简写所省略的边界行为。

22.1 名称规则

· 名称为 1..=255 字节 UTF-8,无 '\0'、无 '/'。"." 与 ".." 保留,从不作为 DENT 行存储
  (结构性解析)。
· **大小写敏感**(POSIX)。S3 键按字节精确;相同字节映射到相同 DENT。不做归一化。
· schema 不限制总路径长度;客户端可施加 PATH_MAX。

22.2 lookup / 解析

resolve(path):按 '/' 切分;从相应根(ROOT/FS_ROOT/BUCKETS_ROOT)起,对每个分量 c:
  get DENT(cur, c) → child_ino;cur = child_ino。
  "." → cur;".." → 父(由遍历器跟踪;ROOT 的父是 ROOT)。
  符号链接分量:读 body=Inline(target);解析 target(限 SYMLINK_MAX = 40 跳;超出 →
  ELOOP)。
  分量缺失 → ENOENT。非末尾分量非 Directory → ENOTDIR。

22.3 rename(src_parent, src_name → dst_parent, dst_name)——完整语义

前置 / 错误(在单一事务内检查):
  · src 必须存在(ENOENT)。
  · 若 src 是 Directory 且 dst_parent 是 src 或 src 的后代 → EINVAL(防环;不变式
    I15)。后代检查从 dst_parent 向上走到 ROOT 找 src 的 inode;受树深界定。
  · 父目录设置 sticky 位(mode & 01000)则把 rename/删除限制为条目属主或目录属主
    (POSIX)——开启 POSIX 权限时强制。
覆盖已存在的 dst:
  · dst 是 RegularFile/Symlink:原子**替换**。旧 dst inode nlink-- ;为 0 → del INOD +
    (OwnLayout 则 DELQ | Slice 则 CMTA.dead_bytes)。
  · dst 是 Directory:仅当 dst 为空(I16)**且** src 也是 Directory 才允许 → 否则
    EISDIR/ENOTDIR/ENOTEMPTY。空 dst 目录被移除(dst_parent.nlink--)。
  · src 与 dst 在 文件↔目录 意义上类型不符 → 按 POSIX ENOTDIR / EISDIR。
效果(**一次**事务):
  del DENT(src_parent,src_name); put DENT(dst_parent,dst_name)=src_child;
  若跨父移动 Directory:src_parent.nlink-- , dst_parent.nlink++(被移目录的 '..'
    重指;目录 inode 自身 nlink 不变);
  src_child.ctime,gen++ ; src_parent.mtime,gen ; dst_parent.mtime,gen ;
  按上文处理被覆盖的 dst inode;若为开版本桶,据此更新两个名称的 VIDX(对有版本对象
  的 rename 作用于**当前**版本;S3 无原生 rename——此路径仅 POSIX/rename)。
RENAME_EXCHANGE / RENAME_NOREPLACE(renameat2):EXCHANGE 原子交换两个已存在 DENT
  (二者都须存在);NOREPLACE 在 dst 存在时 EEXIST。二者均单事务。
unlink(parent,name):
  · 目标不得为 Directory(→ EISDIR;用 rmdir)。
  · del DENT;target.nlink-- 。
  · 若 nlink == 0:
       若 open_count == 0  → 立即 del INOD + DELQ(OwnLayout)/ CMTA.dead_bytes(Slice)。
       若 open_count  > 0  → **孤儿**:保留 INOD(无 DENT),置墓碑标志;删除推迟到
                             最终 close(I17)。经打开句柄的读写继续有效。
  open_count 是 MDS 经 open/close RPC 跟踪的客户端会话状态(每 inode 一张轻量打开表,
  **非**持久租约)。MDS 故障转移后,打开表由客户端重开/keepalive 重建;过宽限期后无
  存活打开者的孤儿由巡查器回收(处理崩溃客户端)。
rmdir(parent,name):目标须为**空** Directory(其前缀下无 DENT,经 1 行范围扫描检查)
  → 否则 ENOTEMPTY。del DENT + del INOD;parent.nlink-- 。
hardlink(target_ino, parent, name):target **必须**是 RegularFile(I18);绝不是
  Directory。put DENT(parent,name)=target_ino;target.nlink++ ;target.ctime,gen。
  硬链接共享一个 inode(一份 body/Layout/Slice)→ 所有名称见同一数据与同一 `gen`。
symlink(parent, name, target_path):put INOD(kind=Symlink, body=Inline(target));
  put DENT。target 是未解释字节串,在 lookup 时解析(§22.2);允许悬空目标(POSIX)。

22.6 readdir 游标(稳定分页)

readdir(dir, cursor, limit):
  范围扫描 DENT 键 [0x02|dir|cursor.last_name+0x00 , 0x02|dir|0xFF…),
  返回至多 `limit` 个 (name, child_ino, kind_tag);下一游标 = 最后返回的 name。
  · 游标是最后的**名称**(非偏移)→ 对并发插入/删除稳定(telldir/seekdir 语义):
    游标之后的新建会出现;已越过的名称不会重现。
  · readdirplus 额外返回子 InodeAttr(每项一次额外 get,或批量)。按名序(UTF-8 字节
    字典序)。
  · S3 ListObjectsV2 把 marker/continuation-token 映射到此名称游标;delimiter='/' 把
    子 Directory 名作为 CommonPrefixes 返回(在分隔符层停止的 readdir——§4)。

22.7 atime / mtime / ctime 规则

· mtime:数据或目录项变化(写、建/删子项)。
· ctime:任何元数据变化(mode/owner/nlink/rename)或数据变化。
· atime:读访问;**惰性**更新(relatime 式:仅当 atime < mtime 或超过窗口),以避免
  每次读一次元数据写——对读密集 AI 负载至关重要。可配置(数据集挂载默认 noatime)。

23. S3 对象语义

23.1 分段上传——状态机与 part↔chunk 组装

状态:Initiated → Uploading → Completed | Aborted | Expired。

CreateMultipartUpload(B,key,meta):
  分配 upload_id(u128);put MPUP{bucket_ino,key,started,init_meta,policy}。
  此时无最终 inode——最终偏移在 Complete 前未知(取决于更低 part_no 的尺寸,且仅在
  Complete 才定稿,part 还可被重传)。

UploadPart(upload_id, part_no, data):
  **自包含**暂存:分配 part_file_id(IALOC);用 MPUP.policy 把 `data` 写为 part_file_id
  下的公式 chunk;put MPRT{size,etag=md5,checksums,part_file_id,layout}。重传同 part_no
  则替换(旧 part_file_id → DELQ)。

CompleteMultipartUpload(upload_id, 有序 [(part_no, etag)]):
  校验 etag、顺序、最小尺寸(除末段 ≥ 5 MiB)。
  part 的最终偏移 = 前序各 part 尺寸之和(现已知)。
  构建 body = Segmented([ Segment{offset,length,part_file_id,layout} … ])
    —— **不**复制数据、**不**重条带化(Complete 为 O(#parts) 元数据)。
  分配最终 inode;body = Segmented(单段则该 part 的 Layout);etag = "{md5(各 part
  md5 拼接)}-{N}"。
  **一次**事务:put 最终 INOD;put DENT(+ 隐式 mkdir -p,§4);若开版本则 upsert VIDX;
  删除 MPUP + 全部 MPRT。暂存 part chunk 原地成为对象 chunk(Segmented 现拥有 part_file_id)。

part↔chunk 对齐:因 part 自包含暂存且对象用 Segmented body,part 无需对齐 chunk_size——无重条带化。读偏移 O 时二分 segments,再在该段 part_file_id 内按公式 解析(SPEC-PLACEMENT-001)。可选后台重条带化可把热的 Segmented 对象收敛成单一 Layout; 非必需。

23.2 UploadPartCopy / ListParts / Abort

UploadPartCopy(upload_id, part_no, src, range):
  服务端:读 src 字节范围 → 暂存为一个 part(如 UploadPart),不经客户端往返。若 src/dst
  共享放置且范围 chunk 对齐,该复制**可**为仅元数据的 chunk 引用(引用计数 CoW)——
  优化;默认为数据复制。
ListParts(upload_id):对 0x0F|upload_id 范围扫描 MPRT → (part_no,size,etag),按 part_no
  游标分页。
AbortMultipartUpload(upload_id):del MPUP + 全部 MPRT;DELQ 每个暂存 part_file_id →
  chunk GC。幂等。

23.3 版本与删除标记(精化 §7 VIDX)

BUCK.versioning ∈ {disabled, enabled, suspended}。
PUT(enabled)        : 新 inode + 新 version_id;VIDX 头部置最新;DENT.child_ino → 新
                      inode(旧版本保留)。
PUT(disabled/susp.) : 覆盖单一 "null" 版本;旧 inode → unlink(末引用则 DELQ/CMTA)。
DELETE 当前(enabled): 插入 删除标记 VersionEntry(delete_marker=1, inode=NIL)为最新;
                      **移除 DENT**(POSIX 见名消失;ListObjectsV2 略去)。GET 当前 →
                      DENT 缺失 → VIDX 最新为删除标记 → 404 NoSuchKey。键仍现于
                      ListObjectVersions。
DELETE 版本 V       : 移除该 VIDX 项;若其 inode 引用归零 → del INOD + DELQ/CMTA。删除
                      最新的删除标记会恢复前一版本 → 重加 DENT(成为当前)。
GET 版本 V          : VIDX → entry.inode_id → INOD(绕过 DENT)。
POSIX 视图          : FUSE 挂载见**当前**非删除标记版本;版本历史是仅经 S3 API 的概念。

23.4 生命周期(对 BUCK 规则的后台 worker)

生命周期扫描器(rustfs-scanner)遍历每个桶并评估 BUCK.lifecycle:
  Expiration(days)       : 当前版本超 days → DELETE(开版本则删除标记;否则硬删)。
                           非当前过期 → 丢弃旧 VIDX 项 + GC 其 inode/chunk。
  Transition(days, tier) : 把数据移到远端层;置 SysMeta.transition;远端副本持久后
                           释放本地 chunk(DELQ);GET 已分层对象触发恢复(RestoreInfo)。
  AbortIncompleteMultipart(days) : MPUP 超 days → Abort(§23.2)。
每个动作是单次元数据事务(+ 分层的异步数据搬迁)。时序为咨询性/最终生效。

24. 规模

InodeAttr 150–300 B · DentValue 20–40 B · ExtentEntry ~16 B · BucketConfig 较小。
10 亿文件 ≈ (250+30) B × 1e9 ≈ ~280 GB 元数据 + KV 索引 → 适配分布式 KV。亿万级
小文件(AI 数据集、node_modules、pip/conda)是常态:其**数据**打包(CMTA),其
**元数据**就是这些行。KV 永不持有数据本体。

25. 编解码版本

每个值(除不透明 XATR)= struct_version(u8=1) || MessagePack(struct)。
增加带合法 None/零默认的**可选**字段 → 不升版本。删除/改类型字段或改键布局 → 升
struct_version 并提供读取器。全新构建:struct_version 1 是 GA 时第一个也是唯一。

26. 合规检查清单(M0/M1 退出)

□ 所有键编码(§2)精确;按表 LE/BE;readdir = 范围扫描。
□ 命名空间根 + 保留 inode 预建;S3 key↔目录映射含隐式 mkdir -p;
  ListObjects(delimiter) ↔ readdir;目录标记键。
□ InodeAttr + InodeKind + InodeBody(Empty/Inline/Slice/Layout/Segmented)往返。
□ Policy(EC|Replica)+ Layout 编解码;放置与 chunk-store 一致。
□ DentValue、VersionList、BucketConfig、EncryptionInfo、Volume、Snapshot、
  ExtentEntry、ContainerMeta、ClusterMap、WriteIntent、DeleteTombstone、Multipart、
  SegmentList、DirStat、Quota、Identity、AccessKey、LockEntry 全部往返。
□ 稀疏/truncate/追加/就地 语义(§19);稀疏读零。
□ 命名空间语义(§22):名称规则;解析含 symlink ELOOP/ENOTDIR;rename 覆盖/目录空/
  防环/EXCHANGE/NOREPLACE;unlink + 打开孤儿(I17);rmdir 非空;hardlink 仅文件;
  readdir 名称游标稳定;relatime。
□ S3 语义(§23):分段 Initiate/Upload/Complete(Segmented,不重条带化)/Abort/
  ListParts/UploadPartCopy;版本 + 删除标记(移除 DENT、ListObjectVersions);
  生命周期 过期/分层/中止未完成。
□ 不变式 I1–I22 强制;nlink 规则(文件 vs 目录)。
□ 事务分组(§21)为单 SSI 事务;先数据顺序。
□ id 分配器:批量区段预留;文件/容器 id 不冲突。
□ struct_version 闸门拒绝未知版本。

英文镜像:rustfs-metadata-structures-EN.md。SPEC-CHUNK-001 的规范元数据对位; 收敛 MD-DESIGN-002/004/005 的元数据定义。