a153ca504081f19eea20a85eaee63ba3acd69283
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_*syscall(POSIX passthrough)。
- 拦截
daemon 层- 独占 SPDK 资源(
spdk_env/blobstore/spdk_thread)。 - 统一处理元数据与并发控制(path/inode/handle)。
- 接收 IPC 请求并执行实际 I/O,返回 POSIX 风格结果与 errno。
- 独占 SPDK 资源(
2.3 元数据与数据映射
- 文件数据:存储在 SPDK blob 中。
- 文件到 blob 的映射:写入真实文件的
xattr(key:user.zvfs.blob_id)。 - 运行时维护三张表:
inode_table:blob_id -> inodepath_cache:path -> inodefd_table:fd -> 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_md;sync_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.sotests/bin/hook_api_testtests/bin/ioengine_single_blob_testtests/bin/ioengine_multi_blob_testtests/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/unlinkread/write/pread/pwrite/readv/writev/pwritevfstat/lseek/ftruncatefcntl/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_BDEV:zvfs_ensure_init使用的 bdev 名称(未设置时使用config.h默认值)。SPDK_JSON_CONFIG:覆盖默认 SPDK JSON 配置路径。
6. 性能说明(仅保留趋势)
README 历史压测数据来自旧版本,不能直接当作当前版本结论;但可作为设计趋势参考:
- 目标工作负载为阻塞 API,近似
QD=1。 - 旧数据下,ZVFS 在
QD=1时约达到spdk_nvme_perf的90%~95%。- 4K:约
95 MiB/svs100 MiB/s - 128K:约
1662 MiB/svs1843 MiB/s
- 4K:约
- 相对同机
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 风险
Description
Languages
C
95.5%
Shell
3.8%
Makefile
0.7%