Files
jabberwocky238 3b72884d9f DESIGN
2026-07-07 00:18:31 +08:00

38 KiB
Raw Permalink Blame History

etvd设计文档

分十一阶段递进实现:FUSE 拦截 -> 内存段 Diff -> cur 快照与聚合版本 -> SQLite 真实写入记录 -> KV 持久化 -> KV 事务模式 -> Prefix 策略 -> MVCC 恢复 -> etcd 协议兼容 -> QUIC mTLS Raft -> 分布式租约。


目录

  1. 第一阶段:FUSE 拦截验证
  2. 第二阶段:内存段 Diff
  3. 第三阶段:cur 快照与聚合版本
  4. 第四阶段:SQLite 真实写入记录
  5. 第五阶段:KV 持久化元数据
  6. 第六阶段:KV 事务模式
  7. 第七阶段:KV Prefix 策略层
  8. 第八阶段:基于 KV 的 MVCC 恢复 构建为lib和单独可执行器
  9. 第九阶段:etcd 协议兼容
  10. 第十阶段:QUIC mTLS Raft 算法
  11. 第十一阶段:分布式租约与条件数据同步
  12. 后续分布式扩展蓝图

第一阶段:FUSE 拦截验证

目标:让 FUSE Daemon 能挂载目录,拦截所有文件系统调用,上层程序可以正常 open / read / write / close,证明通路打通。

本阶段边界(明确不做什么)

排除项 原因
分布式共识 第一步只验证单节点文件语义是否正确
Raft 同步 元数据先放内存或简单持久化,不碰共识
节点间租约 单节点不存在并发写冲突
多节点 QUIC 同步 无集群,无需网络传输
列族分流 / 策略层持久化 前缀策略先放内存,不依赖底层存储引擎的列族能力
复杂 GC / 归档 先让系统能跑起来,后期再补空洞回收

当前唯一目标:让 SQLite 能正常读写一个挂在 FUSE 上的文件,且我们能从底层观察到每一次写入产生的「内存段 Diff」。



第二阶段:内存段 Diff

目标:在 FUSE 拦截通路打通的基础上,实现 COW 内存段 Diff 机制。任何写入都不原地覆盖物理文件,只在尾部追加新字节段,通过段链表拼接还原文件视图。本阶段聚焦写入正确性:读取通过重放历史 VersionDiff 重建段链表,功能正确但不优化性能;cur 快照加速读与聚合版本留到第三阶段。

三层架构

FUSE 拦截层

  • 挂载 /vfs,所有文件操作 100% 经过 FUSE Daemon。
  • 拦截 open、read、write、pwrite、lseek、fsync、truncate、ftruncate、close。
  • 写入拦截是核心:任何 write 不能原地覆盖物理文件已有字节,必须触发内存段 Diff 机制。

内存段 Diff 管理层

  • 物理存储是一个单全局稀疏大文件(data.raw),只允许顺序追加,绝不原地修改已有字节。
  • 每次 write 操作,新增数据追加到 data.raw 尾部,生成一个物理偏移区间,称为 Segment。
  • 一个虚拟文件的当前内容,由一组 Segment 按逻辑顺序拼接而成。
  • 一次事务(或一次 fsync 边界)产生的所有 Segment 变更,打包为一个 VersionDiff。

两层分离原则

  • data.raw 只存原始二进制字节,不存任何元数据、版本号、路径信息。
  • 元数据只存 Segment 的偏移区间和逻辑拼接顺序,不存文件内容本身。

稀疏文件物理存储

  • 单文件 data.raw,预分配为稀疏文件,支持 fallocateLinux)或 truncatemacOS)。
  • 唯一写入接口:Append,返回写入后的起始偏移和长度。
  • 读取接口:按偏移区间随机读。

三种写入场景的设计

场景一:Append 追加

用户行为示例:echo "new line" >> /vfs/log.txt

底层行为:

  1. FUSE write 拦截到追加请求。
  2. 将新内容顺序写入 data.raw 尾部,得到一个物理段,如从偏移 1M 到 1M+256 字节。
  3. 生成 VersionDiff,记录本次新增的物理段区间。
  4. 更新该虚拟文件的当前视图:原视图段链表 + 新增段。
  5. 读取时按段链表顺序拼接,用户看到的就是追加后的完整文件。

关键点:Append 是最简单场景,物理文件只做尾部追加,段链表也只做尾部追加,无需替换中间段。

场景二:随机覆盖写

用户行为示例:dd seek=4096 of=/vfs/config.cfg bs=1024 count=1

底层行为:

  1. FUSE 拦截到对文件中间偏移 4096 位置的写入。
  2. 绝不原地修改 data.raw 中 4096 位置已有的字节。
  3. 将新的 1KB 内容追加到 data.raw 尾部,得到新物理段。
  4. 生成 VersionDiff,记录新增物理段。
  5. 更新虚拟文件视图:将原视图中被覆盖的逻辑区间替换为新物理段,其余段不变。
  6. 读取时按新视图拼接,用户看到覆盖后的内容。

关键点:随机写看似改了文件中间,实际底层全是追加,旧字节仍在原处不动,只是视图链表中不再引用它。

场景三:Truncate 截断

用户行为示例:truncate -s 2048 /vfs/cache.db

底层行为:

  1. FUSE 拦截截断请求。
  2. 纯截断不产生新物理字节,data.raw 不增长。
  3. 生成 VersionDiff,标记操作类型为 Truncate,记录目标文件尺寸 2048。
  4. 更新虚拟文件视图:遍历原段链表,只保留逻辑偏移在 2048 以内的段,裁剪掉超出部分。
  5. 被裁剪掉的旧段不再出现在当前视图,但物理字节仍在 data.raw 中,等待后期 GC 回收。

关键点:Truncate 只是修改视图链表的长度,不动物理文件。

场景四:扩展写(写操作超出当前文件末尾)

用户行为示例:dd if=/data.bin of=/vfs/target.bin bs=1024 seek=100

底层行为:

  1. FUSE 拦截到对偏移 100KB 位置的写入,当前文件只有 50KB。
  2. 将新内容追加到 data.raw 尾部,得到新物理段。
  3. 更新虚拟文件视图:原 0-50KB 段保持不变,50KB-100KB 区间逻辑上视为零填充空洞,100KB 之后挂入新物理段。
  4. 读取时遇到空洞区间返回零字节,遇到有段区间返回实际数据。

关键点:扩展写本质上是随机覆盖写的一种边界情况,文件中间可能出现未写入的空洞,读取时必须正确处理零填充。

场景五:Fsync 刷盘边界与事务圈定

用户行为示例:sqlite > BEGIN; INSERT ...; COMMIT;

底层行为:

  1. BEGIN 到 COMMIT 之间的多次 write 全部暂存在事务缓冲区,不立即 Append 到 data.raw。
  2. 缓冲期间不生成 VersionDiff,不更新 cur 快照。
  3. COMMIT(或 fsync)触发 TxnEnd,将缓冲区中所有变更一次性 Append 到 data.raw。
  4. 生成单条 VersionDiff,包含本次事务的所有新增 Segment。
  5. 更新 cur 快照,事务内所有变更对外原子可见。

关键点:事务边界决定 Diff 的打包粒度,一次事务对应一个版本,保证原子性。


第三阶段:cur 快照与聚合版本

目标:在第二阶段 diff 机制跑通的基础上,解决「读取需重放全部历史 VersionDiff 导致延迟随写入次数线性爆炸」的问题。维护当前视图快照 cur 常驻内存,直接记录文件当前生效的完整段链表,读取无需重放历史;并将物理存储拆分为多 Block,引入聚合版本优化过长的小段链表。


cur 快照与读取加速

为什么需要 cur 快照

如果每次读文件都要重放历史上所有的 VersionDiff 来重建段链表,随着写入次数增加,读取延迟会线性爆炸。因此必须维护一个「当前视图快照」,直接记录该文件当前生效的完整段链表。

cur 快照内容

  • segments:当前文件视图包含的所有物理段,按逻辑偏移排序。
  • total_size:预计算的文件总长度,避免每次 stat 都遍历求和。
  • last_version_id:最后一次更新该快照的版本号。

更新逻辑

Append 场景:原快照段链表直接追加新段,覆盖写入。 RandomWrite 场景:定位被覆盖的逻辑区间,拆分或替换对应段,生成新链表覆盖写入。 Truncate 场景:遍历原链表裁剪长度,生成短链表覆盖写入。

内存缓存

热点文件的 cur 快照常驻内存 LRU 缓存,避免频繁读取持久化存储。TxEnd 后同步更新缓存。


块增长与聚合版本

问题

如果只有一个 data.raw,随着持续追加,文件会无限增长,即使后期 GC 回收空洞,稀疏文件的逻辑大小仍然巨大,管理不便。

方案

将物理存储拆分为多个固定大小的 Block 文件:

  • block_0.raw、block_1.raw、block_2.raw ...
  • 每个 Block 上限 1GB(可配置)。
  • 当前 Block 写满后,自动创建下一个 Block,后续 Append 写入新 Block。

Block 内的 Diff 管理

每个 Block 内部仍然遵循追加写原则:

  • 同一块内的多次写入,生成多个 Segment,全部追加在该 Block 尾部。
  • 每个 Block 维护自己的段链表和 cur 快照。
  • 一个虚拟文件的视图可以跨越多个 Block,由全局段链表串联各 Block 中的物理段。

聚合版本

当一个 Block 内的 diff 次数过多(例如一个 1GB 的 Block 积累了上万个 Segment),段链表变得很长,读取时需要遍历大量小段,性能下降。

聚合版本机制:

  • 后台定期扫描高频修改的 Block。
  • 将该 Block 内某个虚拟文件的所有小段合并为少量大段,重写物理布局(复制到该 Block 的新尾部,生成合并后的 Segment)。
  • 更新 cur 快照引用新的合并段,旧小段标记为可回收。
  • 聚合后读取该文件只需访问少数几个大段,减少 IO 次数。

跨 Block 读取

读取虚拟文件时,按全局段链表定位每个 Segment 所属的 Block,依次从对应 Block 读取字节,拼接成完整视图。


第四阶段:SQLite 真实写入记录

目标:让 SQLite 在 /vfs 上正常运行,底层能精确记录每一次事务产生的字节段 Diff,证明 COW 机制对真实数据库工作负载有效。

验证目标

证明 SQLite 能无感知地运行在我们的 VFS 上,且我们能在底层精确追踪到每一次事务产生的字节段变更。

验证步骤

  1. 启动 FUSE Daemon,挂载 /vfs。
  2. 让 SQLite 打开 /vfs/test.db 数据库文件。
  3. 执行 CREATE TABLE 和 INSERT 操作。
  4. 观察底层产生的 VersionDiff
    • 应该能看到 SQLite 写入的页大小(通常是 4KB)对应的 Segment。
    • 随机覆盖写场景:UPDATE 修改某行时,SQLite 会覆盖数据库文件的特定页,底层应产生 RandomWrite 类型的 Diff。
  5. 执行 SELECT,验证读取时能正确拼接段链表,SQLite 能正常读出数据。
  6. 执行 BEGIN ... COMMIT,验证事务边界是否能正确圈定一组 Diff。

验收标准

  • SQLite 能完成建表、插入、查询、更新,无报错。
  • 底层能打印出每一次写入对应的 Segment(物理偏移、长度、操作类型)。
  • 随机写后,旧页的数据段仍保留在物理存储中,但不在当前视图链表里。
  • Truncate / Vacuum 操作能正确缩小文件视图。

第五阶段:KV 持久化元数据

目标:把内存中的路径到段链表的映射,持久化到 KV 存储中。KV key 与 VFS 虚拟路径百分之百对应,元数据 value 中记录保存策略、实际位置与预期位置。

KV key 与 VFS 路径一一对应

  • KV 存储中的 key 直接使用 VFS 虚拟路径,不做任何编码转换或哈希映射。
  • 例如 VFS 路径 /vfs/config/app.cfg,对应 KV key 就是 /vfs/config/app.cfg。
  • 这种一一对应关系保证:给定任意 VFS 路径,能直接定位到其元数据;给定任意 KV key,也知道它代表哪个虚拟文件。
  • 目录层级天然反映为 KV 的 key 层级,前缀扫描即可列出目录内容。

三层 key 逻辑

当前视图链

  • key 为虚拟路径本身,value 为 cur 快照(段链表 + total_size + last_version_id)。
  • 读取文件时直接查此 key,无需遍历历史日志。

历史快照

  • key 格式为 {path}@{version_id}value 为对应版本的完整段链表。
  • 仅当保存策略为保留历史时才写入。

全局段引用索引

  • key 格式为 ref:{block_id}:{offset}value 为 (path, version_id)。
  • GC 扫描时据此判断哪些物理段仍在被引用。

元数据 value 中的保存信息

每个虚拟路径的元数据 value 中必须包含以下字段:

保存策略

  • strategylocal / region / global,决定该文件的 Diff 是否走 Raft 同步。
  • region_nodes:当 strategy 为 region 时,列出预期同步的目标节点列表。

预期保存位置

  • expected_nodes:根据策略计算出的应该保存该文件元数据的节点集合。
  • global 策略下为全集群节点;region 策略下为指定子集;local 策略下仅为本机。

实际保存位置

  • actual_nodes:记录当前哪些节点已经确认持有该文件的最新元数据。
  • 写入时本地节点首先更新,Raft 同步完成后逐步更新其他节点的确认状态。
  • 用于判断某节点是否滞后、是否需要触发补同步。

元数据与数据的分流原则

  • 元数据(KV 内部)100% 全部走 Raft 强一致同步,所有节点持有的元数据视图必须一致。
  • 真实数据(VFS 上的 value 字节)是条件性的:按保存策略决定是否需要跨节点同步。
  • local 策略的数据只写本机物理存储;region/global 策略的数据通过 QUIC 异步传输到其他节点。

内存优先到持久化的演进

内存优先

  • 第一阶段元数据全部放内存:虚拟路径到 cur 快照的映射表、历史 VersionDiff 列表、全局段引用索引。

简化持久化(可选)

  • 如需进程重启后恢复,可将内存表序列化为单个 JSON / MessagePack 文件落盘:
    • meta.json:记录所有虚拟文件的 cur 快照段链表和保存策略。
    • 启动时加载,运行时全内存操作,定期快照落盘。

排除持久 KV 引擎的原因

  • 第一阶段不需要前缀策略的持久化,策略树纯内存维护即可。
  • 不需要列族、WAL、复杂压缩,简单文件序列化足够。
  • 避免引入外部引擎带来的编译依赖和调优成本。

第六阶段:KV 事务模式

目标:在 KV 持久化之上实现显式事务边界。业务层或管理员可手动调用 TxnBegin / TxnEnd / TxnRollback,将多次 VFS 写入聚合为单个 COW 版本,而不是每次 write 都产生独立 Diff。

核心设计

事务会话

  • TxnBegin(path):为指定虚拟路径创建一个事务会话,分配唯一 txn_id。
  • 会话期间,所有对该路径的 write / truncate 操作不立即 Append 到物理存储,也不更新 cur 快照。
  • 变更内容暂存在内存缓冲区,记录待追加的 Segment 列表和待裁剪的 truncate 尺寸。

写入聚合

  • 事务内允许多次写入,缓冲区累积所有变更。
  • 例如 SQLite 一个事务内可能写入十几个页,全部缓冲在内存。
  • 随机覆盖写场景下,缓冲区可合并重叠区间的写入,只保留最终生效的字节,减少冗余物理段。

TxnEnd

  • 将缓冲区中所有变更一次性 Append 到 data.raw 尾部。
  • 生成单条 VersionDiff,包含本次事务的全部新增 Segment 和最终文件尺寸。
  • 原子更新 cur 快照,对外可见。
  • 若策略允许,同时写入 hist 历史快照。
  • 事务内所有变更要么全部生效,要么全部不生效。

TxnRollback

  • 直接丢弃内存缓冲区,无需回收物理空间(因为尚未真正 Append)。
  • 虚拟文件保持事务开始前的状态,无任何痕迹。
  • 轻量回滚,不涉及磁盘 IO。

手动调用触发 COW

  • 管理员或业务进程可通过专用接口(如 UnixSocket、ioctl、xattr 扩展)主动调用 TxnBegin / TxnEnd。
  • 这种模式下,FUSE 层的 write 操作被挂起,直到显式 TxnEnd 才统一落盘。
  • 适用于需要精确控制版本边界的场景,例如数据库事务、批量配置文件更新。

与 FUSE 的集成

  • 默认模式下,每次 fsync 或文件 close 自动触发隐式 TxnEnd,兼容普通程序无感知使用。
  • 显式事务模式下,fsync 仅刷缓冲区到稳定存储,不生成新版本,直到 TxnEnd 才提交。
  • 两种模式可共存,单文件级别通过 xattr 或 open flag 切换。

第七阶段:KV Prefix 策略层

目标:在 KV 存储之上实现前缀策略树,支持按路径前缀统一配置同步策略、MVCC 开关、共享字段等。路径匹配采用最长前缀优先。

核心设计

前缀策略树

  • 内存中维护 Trie / Radix Tree,按路径层级插入前缀规则。
  • 每个前缀节点可绑定:同步策略(local / region / global)、MVCC 开关(keep_version / discard_old)、区域节点列表、共享字段。
  • 查询时按虚拟路径做最长前缀匹配,返回该路径生效的策略集合。

KV 中的策略存储

  • 策略规则本身作为特殊 key 存入 KVpolicy:{prefix} -> 策略配置值。
  • 启动时从 KV 加载所有策略规则,重建内存 Trie。
  • 运行时策略变更实时写回 KV,保证重启后恢复。

共享字段

  • 同一前缀下的多个文件可共享公共元数据字段,减少重复存储。
  • 例如 /vfs/config/ 下所有文件共享同一个版本保留策略,无需每个文件单独记录。

xattr 动态覆盖

  • 单个文件可通过 xattr 设置 user.vfs.sync 和 user.vfs.version 覆盖前缀默认策略。
  • 动态覆盖仅影响该文件,不改变前缀规则本身。

第八阶段:基于 KV 的 MVCC 恢复

目标:利用 KV 中保存的历史快照,实现对单个路径的任意版本回滚恢复,不是全系统恢复,而是精确到某个 path 的某个历史版本。

核心设计

历史快照存储

  • KV 中按路径 + 版本号存储历史视图:hist:{path}:{version_id} -> 该版本对应的完整段链表。
  • 仅当 MVCC 开关为 keep_version 时才生成历史快照,discard_old 不保留历史。
  • 每次 TxnEnd 产生新版本前,把旧版本的 cur 快照复制一份存入 hist key。

单路径版本恢复

  • 恢复粒度精确到单个虚拟路径,不影响其他文件。
  • 恢复操作:读取 hist:{path}:{target_version} 得到历史段链表,覆盖写入 cur:{path},该文件立即回到目标版本视图。
  • 恢复不产生新物理字节,只修改元数据引用,轻量瞬时完成。

恢复后的写入

  • 恢复后的文件继续接受新写入,新写入基于恢复后的版本生成新的 Diff,历史版本链继续延伸。
  • 恢复操作本身可视为一个特殊版本节点,支持再次回滚到恢复之前的状态。

与 GC 的协同

  • 历史快照持有旧段引用,GC 不会回收这些段。
  • 若管理员决定删除某路径的某个历史版本,只需删除对应的 hist key,该版本引用的段失去引用后由 GC 后续回收。

附录:内存段 Diff 与 Git 的本质区别

维度 Git 本方案
存储粒度 完整文件 blob,diff 是字节差值压缩包 物理文件只存原始字节,diff 只存新增偏移坐标
CPU 开销 需要计算字节差异 写入时直接追加分配新 offset,无需差分计算
磁盘开销 每条提交生成完整文件副本 多版本共享底层物理字节,仅新增少量偏移元数据
在线支持 无在线 fd、租约、truncate 原生支持 完整覆盖追加、随机覆盖、截断三类标准文件操作
块管理 无固定块大小概念 明确 1GB Block 边界,支持聚合版本优化

第九阶段:etcd 协议兼容

目标:对外暴露 etcd v3 API 语义,上层工具(etcdctl、Kubernetes 等)可无缝接入。内部读取不走传统 KV 查询,而是直接从 VFS 文件流中获取数据。

协议兼容层

Key 到虚拟路径的映射

  • etcd 的 key 空间中以 /vfs/ 为前缀的 key,视为虚拟文件路径。
  • etcd Put 操作对应向 /vfs/{key} 写入文件内容。
  • etcd Get / Range 操作对应读取 /vfs/{key} 的文件内容,返回 value 为文件字节流。

写入路径

  • 客户端通过 etcd v3 API 发起 Put / Txn 请求。
  • 协议兼容层拦截 /vfs/ 前缀的 key,将 value 转化为文件写入操作。
  • 走 FUSE 拦截层 -> 内存段 Diff -> Append 到物理存储,产生 VersionDiff。
  • 元数据按策略写入 KV 持久层。

读取直接从 VFS 拿流

  • 传统 etcd 读取是查内存 B+ 树或 BoltDB,返回 value。
  • 本方案中,/vfs/ 前缀的 Get / Range 不走 KV 值查询,而是直接定位到该路径的 cur 快照。
  • 按 cur 快照中的段链表,从 Block 文件中顺序读取字节流,拼接后作为 value 返回给客户端。
  • 大文件读取天然支持流式返回,无需一次性加载完整 value 到内存。

事务语义

  • etcd Txn 的 compare 操作针对 /vfs/ 路径时,比较的是文件的版本号(last_version_id)或内容摘要。
  • etcd Txn 的 success / failure 操作中的 Put / Delete,映射为文件的写入 / 删除操作。
  • 事务边界与 TxnBegin / TxnEnd 对齐,保证原子性。

Watch 机制

  • 对 /vfs/ 路径建立 Watch 时,监听该路径的 cur 快照版本变化。
  • 任何写入导致 cur 快照更新,触发 Watch 事件推送。
  • 用于 runner 任务下发、配置热更新等场景。

第十阶段:QUIC mTLS Raft 算法

目标:实现基于 QUIC + mTLS 的 Raft 共识算法,替代 etcd 原生的 gRPC over TCP 传输层。节点间所有通信(Raft 日志、大文件字节、心跳探活)统一走 QUIC 连接,通过 mTLS 完成双向身份鉴权。

核心设计

QUIC 作为统一传输层

  • 节点间不建立传统 TCP 连接,全部使用 QUIC 长连接。
  • 一条 QUIC 连接内部通过不同 stream 隔离多类流量:Raft 日志流(低延迟、高优先级)、大文件字节同步流(高吞吐、可阻塞)、心跳探活流(轻量、定时)。
  • QUIC 内置连接迁移和 0-RTT 握手,节点 IP 变化时连接不中断。

mTLS 双向鉴权

  • 复用 etcd 的证书体系:CA 根证书、peer 证书、client 证书。
  • 每个节点启动时加载自己的 TLS 私钥和证书,加入集群前必须完成证书校验。
  • QUIC 握手阶段交换证书,握手失败则拒绝加入集群,防止非法节点接入。

Raft 日志传输

  • Leader 将 VersionDiff 序列化为 Raft 日志条目,通过 QUIC stream 发送给 Follower。
  • Follower 收到后写入本地 WAL,应用后再回复 ACK。
  • 相比原生 etcd 的 gRPCQUIC stream 级流量控制避免头阻塞,Raft 日志不受大文件同步影响。

大文件字节同步

  • Raft 只同步偏移元数据,真实字节通过独立 QUIC stream 异步传输。
  • 节点收到 Raft MetaDiff 后,发现本地缺少某些 Segment,向持有节点发起 QUIC PULL 请求。
  • 支持断点续传:stream 中断后根据已确认 offset 恢复,不重新传输完整文件。

与 etcd 协议兼容的衔接

  • 对外仍暴露 etcd v3 API,客户端无感知。
  • 内部 Peer 通信全部替换为 QUIC mTLS,不依赖 etcd 原生的 gRPC 实现。
  • 复用 etcd 的 Raft 状态机逻辑(选举、日志复制、快照、成员变更),只替换网络传输层。

第十一阶段:分布式租约与条件数据同步

目标:实现 FD 级别的分布式写租约,保证同一时刻只有一个节点能写入指定文件。同时实现条件同步机制:默认元数据全集群 Raft 同步,但实际的 VFS value 数据可按策略决定是否跨节点复制。

分布式租约

FD 打开即持有写租约,仅持有租约节点可写入,解决分布式文件并发写互斥。

核心逻辑

  • open 拿到 FD 后,当前节点向 Raft 申请该虚拟路径的写租约。
  • Lease 绑定 FD 生命周期:所有 FD 全部 release 关闭后,租约自动释放。
  • 集群全局单一租约,同一时刻仅一个节点持有。
  • 只有持有 Lease 的节点,FUSE 才允许执行 write / ftruncate;其他节点写入直接返回 EBUSY。

租约数据分层

  • 全局持久租约:存入持久 KV 并走 Raft 同步,所有节点可见谁持有锁,主键为虚拟路径。
  • 本机内存 FD 计数:仅当前节点内存维护,记录该节点打开了多少个 FD,不参与集群同步。

生命周期

  • open 时查询全局 Lease,无租约或已过期则向 Raft 提交抢占 Diff,Raft 多数派成功后本地引用计数加一。
  • write / ftruncate 前校验:若 holder_node 不是本机,直接拒绝。
  • release 时引用计数减一,归零后向 Raft 提交释放 Diff,清除全局 holder_node。
  • 超时兜底:每个 Lease 携带过期时间,持有节点定时心跳续租,宕机后心跳停止,其余节点后台扫描过期 Lease 自动释放。

只读优化

  • O_RDONLY 打开不抢占租约,任意节点可并行读,不阻塞。
  • O_WRONLY / O_RDWR 打开必须抢占写租约,互斥。
  • 同一节点多进程多 FD 打开同一文件:只占用一份全局租约,内存 FD 计数叠加,全部 close 才释放。

条件数据同步

默认行为:元数据全 Raft,value 本机保存

  • 元数据(KV 中的 cur 快照、hist 历史、ref 引用索引)100% 走 Raft 强一致同步,所有节点持有的元数据视图必须完全一致。
  • 默认情况下,实际的 VFS value 数据(Block 文件中的原始字节)只保存在写入节点的本地物理存储上,不主动复制到其他节点。
  • 其他节点通过 Raft 同步的元数据知道该文件存在、知道段链表偏移,但本地 Block 文件中可能没有对应的物理字节。

条件同步触发

  • region / global 策略下,写入节点在 TxnEnd 后主动将本次新增的 Segment 通过 QUIC stream 推送到目标节点。
  • 目标节点接收后 Append 到本地 Block 文件,更新本地 actual_nodes 记录。
  • 推送完成后,目标节点可以本地读取该文件,无需跨节点实时拉取。

默认读取行为:本地无数据则失败

  • 如果某节点收到元数据但本地 Block 文件中缺少对应物理字节,默认情况下读取直接返回失败(如返回 EIO 或专用错误码)。
  • 不自动触发网络拉取,避免跨节点读取带来的高延迟,确保本地读性能可预测。
  • 该节点只能等待条件同步或管理员手动触发数据迁移后,才能本地读取。

远程读取开关

  • KV 元数据 value 中增加 remote_read 布尔字段,默认 false。
  • 当 remote_read 显式置为 true 时,该文件允许在本地无数据的情况下,通过 QUIC 向持有节点实时拉取 Segment 并返回给读取方。
  • 拉取过程中可边收边返回,实现流式远程读取,不等待完整文件落地。

公网与内网场景区分

  • 公网场景:remote_read 保持默认 false。跨公网延迟高、带宽不稳定,不允许实时远程读取,必须通过条件同步预先把数据复制到目标节点后才能读。
  • 内网场景:管理员可将 remote_read 置为 true。内网带宽充足、延迟低,允许实时远程读取,简化数据分布管理,减少冗余副本。
  • 该开关按文件粒度配置,不同路径可独立选择策略。

本地策略短路

  • local 策略的文件完全不触发任何跨节点数据同步,元数据也不走 Raft,仅本机保存。
  • 本地文件不占用分布式租约,本机无锁自由读写。

同步状态记录

  • 每个虚拟路径的元数据 value 中包含 actual_nodes 字段,记录哪些节点已确认持有该文件的完整物理数据。
  • 集群管理员或自动化工具可查询同步状态,判断是否需要触发补同步或调整策略。

后续分布式扩展蓝图

单节点验证通过后,系统按以下蓝图扩展为完整分布式版本化 VFS 底座。

1. 五层分层架构

第一层 用户应用层

  • 上层程序、bash、dd、tail、SQLite 直接操作 /vfs/ FUSE 挂载目录,无侵入,标准 Linux 文件 API。
  • 业务主动调用 TxnBegin / TxnEnd 划定版本单元,一次数据库事务对应一个文件版本。

第二层 内核 VFS + FUSE Daemon 拦截层

  • 前缀策略路由:内存 Trie 前缀匹配树,长路径优先覆盖。每条路径绑定同步策略(local / region / global)和 MVCC 版本策略(keep_version / discard_old)。
  • COW 自定义 MVCC:底层只允许 Append,写入全部 COW 在尾部新增字节段。版本不复制完整文件,仅新增变更片段,读取时拼接 offset 链表还原视图。
  • 事务与同步调度:对外暴露 TxnBegin / TxnEnd / TxnRollback。Begin 创建内存缓冲区缓存偏移,End 打包为单条元数据 Diff 按策略分流。

第三层 魔改 etcd Raft 共识层

  • 复用 etcd 成熟 Raft 集群、mTLS 鉴权、事件广播基建。
  • Raft 仅传输极小 offset 元数据 Diff,不传输完整文件。带宽开销极低。
  • 全局单调版本号由 Raft 日志序列号提供,天然解决多节点并发写入冲突。

第四层 元数据存储层

  • 放弃列族分流方案,采用自研前缀策略树管理。
  • 持久 KV 只分三层 Key 逻辑:当前视图链 cur、历史快照 hist、全局段引用 ref。
  • 真实业务字节不存入此层,仅存储虚拟文件元索引。

第五层 底层物理存储层

  • 单块或多块全局预分配稀疏大文件,唯一写入方式是 Append 追加。
  • 后台常驻 GC 协程扫描废弃 offset 空洞,裁剪稀疏文件释放磁盘。
  • 长期冷版本分片异步归档至次级存储,主文件仅保留活跃数据段。

2. 三大核心差异化设计

自定义字节段 MVCC

  • 不同于 MinIO 对象 MVCC(每次更新复制完整文件),本架构仅新增变更字节段,复用全部未修改内容。
  • 不同于 RocksDB 原生 KV MVCC(整条 value 多版本),本架构粒度下放到文件内部字节偏移,版本逻辑由 FUSE 业务层控制,可按路径开关版本留存。

三级分层同步策略

  • local:仅本机,不走 Raft,低延迟,适合临时日志和单机 SQLite。
  • region:选择性副本同步,适合机房批量运维日志,仅同步指定节点。
  • global:全集群 Raft 强一致,适合网关配置、集群拓扑,完整历史版本可回滚。

兼容实时流式 IO

  • 原生 etcd 仅支持整块 Put,无 fd、lseek、tail 增量读取。
  • 本架构底层是追加式大文件,天然支持长文本流式分段写入、实时日志滚动读取。

3. Raft 共识层与 etcd 协议兼容

复用 etcd 成熟 Raft 集群基建,彻底替换原生 BoltDB 存储层,同时保持与 etcd 协议的兼容性。

etcd 协议兼容

  • 对外暴露 etcd v3 API 语义,上层工具(etcdctl、Kubernetes 等)可无缝接入,将 /vfs/ 前缀下的 key 视为虚拟文件路径。
  • 内部节点间 Peer 通信不采用 etcd 原生 gRPC over TCP,而是统一使用 QUIC + mTLS 传输。
  • 复用 etcd 的证书体系(CA、peer cert、client cert)做双向身份鉴权,节点加入集群前必须完成证书校验。
  • QUIC 连接同时承载两类流量:Raft 元数据日志通道(轻量、低延迟)和大文件字节同步通道(流式、高吞吐),通过不同 stream 隔离。

元数据 Diff 同步

  • Raft 日志内容只包含 VersionDiff 级别的偏移元数据:路径、新增物理段区间、操作类型、截断尺寸、版本策略。
  • 绝不传输任何文件二进制内容,大块字节走独立 QUIC 异步 stream,与 Raft 元通道完全解耦。
  • 全局单调版本号直接复用 Raft 日志索引 Index,天然时序唯一。

分流规则

  • local 策略:完全跳过 Raft,仅写入本机元存储。
  • region / global 策略:Diff 提交 Raft 多数派,全集群同步版本索引。

集群事件广播

  • 复用 etcd watch 机制,节点监听 /vfs/runner/task/ 等路径变更,触发本地批量 bash 执行。

5. QUIC 大文件异步流式同步

底层大文件分片走独立 QUIC 连接,与 Raft 元通道完全解耦。

连接管理

  • 节点间基于 etcd 节点发现维护 QUIC 长连接池,复用 etcd mTLS 证书。

分片策略

  • 每个 Block 文件按固定大小逻辑分片,每片有独立 checksum。
  • 断点续传:stream 中断后根据已确认 offset 恢复传输。

拉取与推送

  • 拉取触发:节点收到 Raft MetaDiff 后,发现本地缺少某些 ChunkRef,向 leader 或指定节点发起 PULL 请求。
  • 推送触发:region 策略下,节点主动向目标节点推送本地新 Append 的字节段。

流式写入

  • QUIC 接收端直接 Append 到本地 Block 文件,不缓冲完整文件,边收边落盘,内存友好。
  • 传输失败不影响 Raft 共识,仅影响数据可用性,节点可稍后重拉。

6. 三级分层同步策略

local 本地缓存通路

  • 临时日志、单机 SQLite,无集群同步,低延迟。
  • 完全跳过 Raft,直接写本地 Block 文件和元存储。
  • 本地路径不走分布式租约,本机无锁自由读写。

global 全局强一致通路

  • 网关配置、集群拓扑、任务定义,全节点 Raft 同步,完整历史版本可回滚。
  • Diff 打包为 Raft Propose,走 etcd Raft 多数派确认。
  • 异步提交:写入本地存储成功后即返回上层,Raft 同步在后台完成。

region 区域选择性副本通路

  • 机房批量运维日志,仅同步到 RegionNodes 指定的节点子集。
  • 走轻量级 gossip 或自定义 topic,不经过完整 Raft 多数派。
  • 适合大带宽、低一致性要求的日志汇聚场景。

7. 内置批量 bash 执行引擎

去中心化 Ansible ad-hoc,无中控单点。

下发逻辑

  • CLI 或内网接口将 bash 任务写入 /vfs/runner/task/ 全局同步路径。
  • 任务内容包含 ID、节点标签选择器、脚本、超时、环境变量。

执行逻辑

  • 全网节点通过 FUSE 监听路径变更,匹配 NodeSelector 与本地标签后,本地 fork 子进程执行。
  • 纯一次性执行,无持续 reconcile 收敛逻辑,失败不重试。

日志落盘

  • 脚本输出直接 COW 写入本机 VFS Block 文件,offset 元数据按策略同步集群。
  • stdout / stderr 流式落盘,实时可读。

资源隔离

  • 通过 cgroup 或 setrlimit 限制任务 CPU、内存、执行时间,防止单任务耗尽节点资源。

8. SQLite 与事务边界

SQLite 不是独立的分布式模块,而是 VFS 的标准消费者之一,与 bash、普通程序使用同一套文件 API。

事务映射

  • 业务层在 BEGIN 时调用 TxnBegin,创建事务缓冲区。
  • SQL 读写全部走标准文件 API,数据暂存事务缓冲,不生成版本。
  • COMMIT 时调用 TxnEnd,将缓冲中的变更一次性 Append 到物理存储,产生一条 VersionDiff。
  • ROLLBACK 时调用 TxnRollback,直接丢弃缓冲,无需回收物理空间。

后台操作过滤

  • Vacuum、Checkpoint 等 SQLite 后台无事务裸写,可配置过滤不生成正式版本,减少冗余版本。

分布式自然达成

  • SQLite 的分布式能力完全由底层 VFS 提供:Diff 走 Raft 同步,租约保证单写节点,SQLite 本身无感知。
  • 数据库文件可按业务需求配置为 local 策略(单机缓存)或 global 策略(集群同步)。

9. GC 与空洞回收

引用扫描

  • 后台协程遍历全部 cur 当前视图链表和 hist 历史快照,收集所有被引用的物理区间。
  • 对比 Block 文件全部已占用区间,找出无任何引用的空白段。

物理裁剪

  • 对稀疏文件执行 fallocate punch hole 释放空洞对应的物理块(Linux)。
  • macOS 不支持 punch hole 时,采用复制存活数据到新文件并原子替换的策略。

触发策略

  • 定时(如每小时)、定量(空洞比例超过阈值)、或手动触发。
  • discard_old 策略的临时文件,旧段无快照引用,会快速被回收。
  • keep_version 策略的配置文件,历史快照持续引用旧段,永久保留。

并发安全

  • GC 期间允许新 Append(新数据在文件尾部,不影响已扫描区间),天然安全。
  • 禁止原地修改的底层原则保证 GC 不会误删正在写入的数据。

10. 冷数据归档

归档标准

  • 版本最后访问时间超过 N 天,或版本号远小于当前版本,判定为冷数据。

归档流程

  • 扫描冷版本,将其引用的 ChunkRef 对应字节段复制到归档存储(本地二级目录、S3、MinIO、NFS 等插件化目标)。
  • 更新元数据记录归档位置,主文件对应区间 punch hole 变为可 GC 空洞。

按需回温

  • 读取归档版本时,从次级存储拉取字节段回主文件或缓存区,重建可读视图。

与 GC 协同

  • 归档完成后,主文件对应区间失去引用,变为空洞,由 GC 后续回收物理空间。

十、修订记录

日期 版本 说明
2026-07-06 v0.1 初始架构拆解,17 个 Phase,涵盖五层架构
2026-07-06 v0.2 聚焦单节点验证,删除所有代码块,删除 RocksDB 依赖,新增块增长与聚合版本设计
2026-07-06 v0.3 重构为七阶段递进结构:FUSE -> Diff -> SQLite -> KV -> Prefix -> MVCC -> etcd 协议兼容,写入场景扩展为五种
2026-07-06 v0.4 增加第五阶段 KV 事务模式(TxnBegin / TxnEnd / TxnRollback),支持手动调用聚合多次 VFS 写入为单事务
2026-07-06 v0.5 重构为十阶段递进结构,新增第九阶段 QUIC mTLS Raft、第十阶段分布式租约与条件数据同步;KV key 与 VFS 路径一一对应;元数据 value 包含保存策略、预期位置、实际位置
2026-07-06 v0.6 第十阶段读取行为调整:默认本地无数据则读取失败,不自动网络拉取;增加 remote_read 开关区分公网/内网场景
2026-07-07 v0.7 拆分原第二阶段:内存段 Diff(写入 COW 机制)独立为第二阶段,cur 快照加速读 + 块增长 + 聚合版本独立为第三阶段;后续阶段顺延,整体由十阶段改为十一阶段