Files
zvfs/README.md

6.4 KiB
Executable File
Raw Blame History

ZVFS

ZVFS 是一个基于 SPDK Blobstore 的轻量级用户态文件系统原型, 通过 LD_PRELOAD 拦截常见 POSIX 文件 API/zvfs 路径下的文件 I/O 转换为 Blob I/O。

目标是让上层应用尽量少改动地复用阻塞式文件接口,同时接近 SPDK 在低队列深度QD≈1场景的性能上限。

1. 项目结构

zvfs/
├── src/
│   ├── hook/           # POSIX API hook 层open/read/write/...
│   ├── fs/             # inode/path/fd 运行时元数据管理
│   ├── spdk_engine/    # SPDK Blobstore 封装
│   ├── common/         # 对齐与缓冲区工具函数
│   ├── config.h        # 默认配置JSON、bdev、xattr key 等)
│   └── Makefile        # 产出 libzvfs.so
├── tests/
│   ├── hook/           # hook API 语义测试
│   ├── ioengine_test/  # Blob 引擎单元测试
│   └── Makefile
├── scripts/            # db_bench/hook 测试辅助脚本
├── spdk/               # SPDK 子模块
└── README.md

2. 核心架构

2.1 分层

当前实现:

App (open/read/write/fstat/...)
  -> LD_PRELOAD Hook (src/hook)
  -> ZVFS Runtime Metadata (src/fs)
  -> SPDK Engine (src/spdk_engine)
  -> SPDK Blobstore
  -> bdev (Malloc/NVMe)

目标架构Daemon + IPC

App (multi-process, e.g. PostgreSQL)
  -> LD_PRELOAD Hook Client
  -> IPC (Unix Domain Socket)
  -> zvfs daemon
     -> metadata manager
     -> SPDK worker threads
     -> SPDK Blobstore / bdev

2.2 目标架构简版HOOK 层 + daemon 层)

  • HOOK 层
    • 拦截 /zvfs 路径的 POSIX API 并同步发起 IPC 请求。
    • 维护本地最小状态(如 fd -> remote_handle_id)。
    • 对非 /zvfs 路径继续透传到 real_* syscallPOSIX passthrough
  • daemon 层
    • 独占 SPDK 资源(spdk_env/blobstore/spdk_thread)。
    • 统一处理元数据与并发控制path/inode/handle
    • 接收 IPC 请求并执行实际 I/O返回 POSIX 风格结果与 errno。

2.3 元数据与数据映射

  • 文件数据:存储在 SPDK blob 中。
  • 文件到 blob 的映射:写入真实文件的 xattrkey: user.zvfs.blob_id)。
  • 运行时维护三张表:
    • inode_tableblob_id -> inode
    • path_cachepath -> inode
    • fd_tablefd -> open_file

2.4 当前实现的 I/O 路径要点

  • blob_read/blob_write 统一走按 io_unit_size 对齐的 DMA 缓冲。
  • 非对齐写会触发读改写RMW先读对齐块再覆盖局部写回。
  • readv/writev 在 hook 层会做聚合,减少多次 I/O 提交。
  • fsync/fdatasync 对 zvfs fd 调用 blob_sync_mdsync_file_range 在 zvfs 路径直接返回成功。

3. 构建

下面命令以仓库根目录为 /home/lian/try/zvfs 为例。

3.1 初始化并构建 SPDK

git submodule update --init --recursive
cd spdk
./scripts/pkgdep.sh
./configure --with-shared
make -j"$(nproc)"

3.2 构建 ZVFS 与测试

cd /home/lian/try/zvfs
make -j"$(nproc)"
make test -j"$(nproc)"

产物:

  • src/libzvfs.so
  • tests/bin/hook_api_test
  • tests/bin/ioengine_single_blob_test
  • tests/bin/ioengine_multi_blob_test
  • tests/bin/ioengine_same_blob_mt_test

4. 运行与验证

4.1 Hook API 语义测试

mkdir -p /zvfs
cd /home/lian/try/zvfs
LD_PRELOAD=$PWD/src/libzvfs.so ZVFS_TEST_ROOT=/zvfs ./tests/bin/hook_api_test

覆盖点包括:

  • open/openat/rename/unlink
  • read/write/pread/pwrite/readv/writev/pwritev
  • fstat/lseek/ftruncate
  • fcntl/ioctl(FIONREAD)
  • fsync/fdatasync

4.2 SPDK 引擎测试

cd /home/lian/try/zvfs
SPDK_BDEV_NAME=Malloc0 ./tests/bin/ioengine_single_blob_test
SPDK_BDEV_NAME=Malloc0 ./tests/bin/ioengine_multi_blob_test
SPDK_BDEV_NAME=Malloc0 ./tests/bin/ioengine_same_blob_mt_test

5. 关键环境变量

  • SPDK_BDEV_NAME:选择后端 bdev默认 Malloc0)。
  • ZVFS_BDEVzvfs_ensure_init 使用的 bdev 名称(未设置时使用 config.h 默认值)。
  • SPDK_JSON_CONFIG:覆盖默认 SPDK JSON 配置路径。

6. 性能说明(仅保留趋势)

README 历史压测数据来自旧版本,不能直接当作当前版本结论;但可作为设计趋势参考:

  • 目标工作负载为阻塞 API近似 QD=1
  • 旧数据下ZVFS 在 QD=1 时约达到 spdk_nvme_perf90%~95%
    • 4K95 MiB/s vs 100 MiB/s
    • 128K1662 MiB/s vs 1843 MiB/s
  • 相对同机 O_DIRECT 路径,旧数据写带宽约有 2.2x~2.3x 提升。
  • 非对齐写存在 RMW吞吐明显下降旧数据常见接近对齐写的一半

如果需要用于对外汇报,请重新在当前 commit 与固定硬件环境下复测。

7. 当前限制

  • 仅拦截 /zvfs 路径。
  • mmap 对 zvfs fd 当前返回 ENOTSUP(建议上层关闭 mmap 读写)。
  • dup/dup2/dup3 对 zvfs fd 当前返回 ENOTSUP
  • rename/zvfs 与非 /zvfs 路径返回 EXDEV
  • fallocate(FALLOC_FL_PUNCH_HOLE) 未实现。

8. 后续建议

  • 补齐 mmap 路径mmap table + 脏页回写)。
  • 完善多线程/高并发下的语义与压测基线。
  • 增加版本化 benchmark 报告,避免 README 中历史数据失真。

9. Blob Store 血泪教训

Owner Thread 绑定

blobstore内部负责并发控制让所有metadata操作都在一个线程上执行回调固定绑定给创建blobstore的线程。所以多线程模型下不是send给谁谁就能poll到回调的。

正确架构:

metadata thread
    spdk_bs_load()
    resize
    delete
    sync_md

worker thread
    blob_io_read
    blob_io_write

spdk_for_each_channel() Barrier

某些 metadata 操作非常慢:

resize
delete
unload
snapshot

这些操作内部会调用spdk_for_each_channel()

语义:在所有 io_channel 所属线程执行 callback

类似

for each channel:
    send_msg(channel->thread)

问题1持有 Channel 的 Thread 不 poll

如果所属线程不poll就会卡住。

问题2线程退出 Channel 没有释放

永远卡住

IO 操作的回调行为与 metadata 操作不同

spdk_blob_io_read / spdk_blob_io_write 的回调,是通过传入的 io_channel 投递的,回调回到分配该 channel 的 thread。

超时任务

设置超时就免不了超时后回调成功执行,超时后回调仍会触发,存在 UAF 风险