main
- store: data.raw append-only / read-by-offset / fsync - meta: Segment/VersionDiff/FileView/FileMeta + segment-list update (append/overwrite/truncate/extend) + in-memory directory tree - fuse_ops: all callbacks route through COW backend; per-write Segment logging (op/phys_off/log_off/size) - tests/phase2.sh: 5 write scenarios + sqlite + dir ops, 24/24 pass Metadata in-memory (restart-lost; persistence phase 5); data.raw retains superseded bytes pending GC. cur cache / aggregation / multi-block land in phase 3. Co-Authored-By: Claude <noreply@anthropic.com>
etvd — 第二阶段:内存段 Diff(COW append-only 后端)
对应
docs/DESIGN.md第二阶段。在第一阶段 FUSE 拦截通路基础上,把透传后端替换为 COW append-only 后端:写入只追加到data.raw尾部、以 Segment 拼接还原视图,验证五种写入场景并从底层观测每一次 write 的 Segment。
这一步做了什么
- 第一阶段的 FUSE 拦截层保留,后端由「透传真实目录」改为 COW append-only:
- 物理存储是单个稀疏文件
data.raw,唯一写接口是 Append(顺序追加,绝不原地改字节)。 - 每次 write 把新字节追加到
data.raw尾部,得到一个物理段 Segment(phys_off/phys_len/log_off)。 - 一个虚拟文件的当前内容由一组 Segment 按逻辑顺序拼接;段间 gap 即空洞,读返回零。
- 读取按当前视图段链表从
data.raw取字节拼接。
- 物理存储是单个稀疏文件
- 全自管内存 VFS:目录树 + 文件元数据全部内存维护,
data.raw只存文件字节。getattr/readdir/mkdir/unlink/rename全走内存。 - 即时写 + 当前视图:write 立即 Append + 生成 Segment/VersionDiff + 增量更新当前视图;read 走当前视图;fsync 对
data.raw落盘并标记版本边界。事务暂存圈定留到第六阶段。 - 底层观测:每次 write 打印
[etvd] write <path> op=<Append|Overwrite|Truncate|Extend> phys=<off>+<len> off=.. size=.. data=<hex>,满足「从底层观察每一次写入产生的 Segment」。 - 元数据纯内存,进程重启丢失(第五阶段 KV 持久化);
data.raw残留旧字节待后续 GC。
三层结构
上层程序 (bash / cat / dd / sqlite3)
│ 标准 Linux 文件 API
▼
FUSE 拦截层 (src/fuse_ops.cpp) ── 每次调用先记日志,再走 COW 后端
│
▼
内存元数据层 (src/meta.cpp) ── 段链表(当前视图)+ 目录树 + history,shared_mutex 保护
│
▼
物理存储层 (src/store.cpp) ── data.raw,Append-only / 按偏移读
依赖
Linux + /dev/fuse、libfuse3-dev、pkg-config、g++(C++17)、make、sqlite3(测试用)。
sudo apt-get install -y libfuse3-dev pkg-config g++ make sqlite3
构建
make
产出可执行文件 etvd_fuse。
运行
./etvd_fuse mnt -o backend=./backend
-o backend=DIR:存放data.raw的目录(默认./backend,不存在会自动创建)。-f前台运行(已强制开启);-o default_permissions(已强制开启)。- 日志输出到 stderr。
卸载
fusermount3 -u mnt
验收测试
bash tests/phase2.sh
脚本覆盖五种写入场景 + SQLite + 目录操作,并校验底层日志的 Segment 观测,末尾打印 RESULT: pass=N fail=M。
五种写入语义
| 场景 | 行为 | 日志可见 |
|---|---|---|
| 追加写 | echo >> |
write op=Append phys=<尾部off>+<len> |
| 随机覆盖写 | dd seek= / SQLite UPDATE 改页 |
write op=Overwrite phys=<新off>+<len>(旧字节仍在 data.raw,仅视图摘出) |
| 截断缩小 | truncate -s N |
truncate size=N,视图裁剪,不产生新物理字节 |
| 截断扩大 / 扩展写 | truncate -s M / dd seek=超末尾 |
write op=Extend,中间空洞读返回零 |
| Fsync 边界 | SQLite COMMIT | fsync datasync=..,对 data.raw 落盘 |
实现要点
- 分层:
store(物理 Append/Read/Sync)+meta(段链表算法 + 目录树 + history)+fuse_ops(回调)。meta方法不加锁,由fuse_ops经Meta::Lock()(shared_mutex)在外层界定读写锁;write 持写锁跨Store::Append,保证段落盘与视图更新原子。 - 段链表更新:write 区间
[off,off+len)把相交旧段拆成前缀/后缀片段、丢弃被覆盖中段、插入新段,保持升序不重叠,gap 即空洞。truncate 缩小裁剪段、扩大只增长total_size。 - 即时写 + 当前视图:当前视图(
FileView)是 cur 的朴素前身;第三阶段才在其上做 LRU 缓存 / 聚合版本 / 多 Block 分块。history(VersionDiff列表)保留供观测与未来 MVCC,不参与读路径。 - 全自管内存 VFS:根
/在Meta构造时建立;路径经Parent/Base拆分管理父子;rename 目录时遍历子树改 key 前缀。 - fsync 真落盘:fsync 回调
::fsync(data.raw fd),SQLite 依赖其持久性。 - 锁:
lock回调对F_GETLK返回F_UNLCK、F_SETLK返回 0,SQLite fcntl 建议锁单进程正常运行。真实互斥留第十一阶段。 - default_permissions:getattr 返回内存元数据的 mode/uid/gid,内核据此校验;以当前用户身份运行,文件 0644 / 目录 0755。
- 重启语义:
data.raw的 append 游标从文件大小恢复(继续追加、不覆写旧字节),但内存元数据丢失,旧字节成孤儿待 GC。 - 未实现 op(readlink / symlink / fallocate / xattr)保持 NULL,FUSE 返回
-ENOSYS。
第二阶段边界(明确不做)
cur 的 LRU 缓存 / 聚合版本 / 多 Block 分块(第三阶段)、KV 持久化(第五阶段)、事务 TxnBegin/End(第六阶段)、Prefix 策略(第七阶段)、MVCC 恢复(第八阶段)、etcd 协议(第九阶段)、QUIC mTLS Raft(第十阶段)、分布式租约(第十一阶段)、GC 空洞回收、冷数据归档、symlink / 设备文件。
文件
src/store.h/.cpp 物理存储:data.raw Append / Read / Sync
src/meta.h/.cpp 内存元数据:Segment/VersionDiff/FileView/FileMeta + 段链表算法 + 目录树
src/fuse_ops.h/.cpp FUSE3 回调:日志 + 走 COW 后端 + ops 表
src/main.cpp 参数解析、绝对路径、打开 data.raw、初始化 Meta、挂载入口
src/logger.h 线程安全操作日志 + hex 预览
tests/phase2.sh 五种场景 + SQLite 验收脚本
Makefile pkg-config fuse3
Description
Languages
C++
87.3%
Shell
11.3%
Makefile
1.4%