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。二者均单事务。
22.4 unlink / rmdir 与"已打开但被删"(POSIX 孤儿)
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-- 。
22.5 hardlink / symlink
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 的元数据定义。