Files
zvfs/README.md
2026-03-30 21:17:25 +08:00

10 KiB
Executable File
Raw Blame History

ZVFS

透明用户态 POSIX 文件系统,基于 SPDK Blobstore。

ZVFS 是一个 透明用户态文件系统原型,通过 LD_PRELOAD 劫持 POSIX I/O 将应用程序的文件数据路径从 Linux 内核 I/O 栈重定向到 SPDK 用户态 NVMe 存储路径

目标是在 零业务代码修改 的情况下,为数据库与向量检索系统提供更低延迟的存储访问。

目前已在 PostgreSQL + pgvector 场景完成功能验证。


测试方案

git clone http://gitlab.0voice.com/lianyiheng/zvfs.git
cd zvfs
git submodule update --init --recursive

cd spdk
sudo ./scripts/pkgdep.sh
sudo ./configure --with-shared
sudo make -j
sudo ./scripts/setup.sh

cd ..
make && make test

su root

# 测试
mkdir -p /zvfs/zvfs_sys_fio
mkdir -p /tmp/zvfs_sys_fio 

SPDK_JSON_CONFIG="$PWD/src/zvfsnvme.json" ./src/daemon/zvfs_daemon
ZVFS_LD_PRELOAD_VALUE="$PWD/src/libzvfs.so" ./scripts/run_fio_matrix.sh

# 结果:
results/fio/..../summary.md

设计思路

大多数用户态文件系统(如 FUSE需要修改应用或挂载文件系统。用户态文件系统如果要通过VFS需要多一到两次额外的用户态/内核态切换。ZVFS 的目标是对应用完全透明:应用按正常方式调用 POSIX API底层存储路径被悄悄替换掉。 核心决策是控制面与数据面分离:

控制面复用 Linux VFS目录树、权限、inode 生命周期全部由 Linux 管理,文件到 blob 的映射通过 xattruser.zvfs.blob_id持久化无需额外的映射数据库。 数据面走 SPDKread/write 等数据路径绕过内核,经 IPC 送到 ZVFS daemon再通过 SPDK Blobstore 直接访问 NVMe。

Application (PostgreSQL / RocksDB)
         │  POSIX API
         ▼
  LD_PRELOAD Hook Layer
         │  Unix Domain Socket
         ▼
     ZVFS Daemon
    ┌────┴────┐
    │         │
Metadata   IO Workers
 Thread    (SPDK pollers)
    │         │
    └────┬────┘
         ▼
   SPDK Blobstore
         │
       NVMe SSD

SPDK 需要使用轮询模式最好能独占CPU core且metadata最好由同一个 spdk thread 管理,不适合嵌入任意应用进程。因此 daemon 统一持有所有 SPDK 资源,多个客户端进程共享同一个 daemon通过 Unix Domain Socket 通信。


🧠 系统架构

架构设计关键点:

  • 同步阻塞语义

  • 零侵入接管应用 I/O

    • 使用 LD_PRELOAD 拦截 POSIX API
    • 不需要修改应用代码
  • 控制面复用 Linux

    • ZVFS 不重新实现目录树,而是复用 Linux VFS。目录 / 权限 / inode 生命周期由 Linux VFS 管理。
    • 文件与 blob 的映射通过:xattr: user.zvfs.blob_id
  • SPDK 资源集中管理

    • 文件内容存储在 SPDK Blobstore。直接访问 NVMe。
    • SPDK 对 metadata 操作有 单线程要求,因此 daemon 设计为:
    • metadata 操作create / resize / delete
    • data IOread / write
  • POSIX 语义兼容

    • 用户态文件系统需要正确模拟 Linux FD 语义:dup dup2 dup3 fork close_range
    • 保证多个 fd 指向同一文件句柄时语义一致。

📦 构建

git clone ...
git submodule update --init --recursive

cd spdk
./scripts/pkgdep.sh
./configure --with-shared
make -j

cd ..
make -j

▶️ 运行

启动 daemon

./src/daemon/zvfs_daemon

运行测试:

LD_PRELOAD=./src/libzvfs.so ./tests/bin/hook_api_test

🔬 已实现功能

打开/关闭/删除

open open64 openat openat64 fopen fopen64 
creat creat64 
fclose close close_range 
dup dup2 dup3 fork
unlink unlinkat remove rename renameat

读写层

read pread pread64 readv preadv preadv64 preadv2 
write pwrite pwrite64 writev pwritev pwritev64 pwritev2 
fread_unlocked fread fscanf

偏移/空间管理层

lseek lseek64 
truncate truncate64 ftruncate ftruncate64 fallocate posix_fallocate

元数据层

stat stat64 fstat fstat64 lstat lstat64 fstatat fstatat64 statx

同步/控制层

fsync fdatasync sync_file_range
fcntl fcntl64 ioctl

🚀 性能

测试环境VMware 虚拟机 + 模拟 NVMe单线程阻塞 I/O。

VMware 模拟 NVMe 无法体现 SPDK 轮询模式对中断驱动 I/O 的延迟优势, 以下数据用于评估 hook 层与 IPC 的额外开销,不代表真实硬件上的性能对比。

fio 4Kpsync30s

测试口径:

  • ioengine=psync
  • direct=1
  • iodepth=1
  • bs=4K
  • time_based=1
  • runtime=30
  • size=512M
  • sys: 普通文件路径
  • zvfs: LD_PRELOAD=./src/libzvfs.so

prepare_fill 顺序写带宽

sys ZVFS
带宽 10.92 MiB/s 14.41 MiB/s
disk util 99.68% 5.49%

randread_4k

sys ZVFS
IOPS 3118.31 3685.21
吞吐 12.18 MiB/s 14.40 MiB/s
avg clat 318.31 µs 268.91 µs
disk util 99.77% 0.52%

randwrite_4k

sys ZVFS
IOPS 2883.24 3816.78
吞吐 11.26 MiB/s 14.91 MiB/s
avg clat 344.20 µs 259.53 µs
disk util 99.80% 3.97%

randrw_4k50/50

sys ZVFS
读 IOPS 1614.29 2652.07
写 IOPS 1605.60 2637.78
读 avg clat 306.56 µs 184.11 µs
写 avg clat 309.72 µs 189.44 µs
disk util 99.87% 2.98%

WRITE 请求端到端延迟分解(单位 µs

sudo env \
  ZVFS_TRACE_LATENCY=1 \
  LD_PRELOAD="$PWD/src/libzvfs.so" \
  fio ./zvfs_fio_test/zvfs/randwrite_4k.fio 2> /tmp/zvfs.write.trace.log

基于 /tmp/zvfs.write.trace.log107946WRITE trace 样本统计,下面按调用栈层级展开平均耗时。由于四舍五入,父子项相加会有 ±1 µs 误差。

total 256
├─ c2s 41
│  ├─ send 7
│  └─ server_rx_wait 34
├─ server 154
│  ├─ rx_dispatch 0
│  ├─ dispatch_spdk 5
│  ├─ spdk 138
│  │  ├─ phase1 0
│  │  └─ phase2 138
│  └─ reply_q 10
│     ├─ spdk_post 0
│     └─ cq_wait 10
│        ├─ kick 1
│        ├─ wake_sched 8
│        └─ wake_to_tx 0
└─ s2c 60
   ├─ resp_wait 60
   └─ parse 0

现在一次 WRITE 平均大约 256 µs。其中最耗时的是实际存储写入(spdk,约 138 µs),其次是请求发给 daemon 和结果返回应用这两段通信等待(c2s + s2c,约 101 µs)。回包队列相关开销(reply_q)已经压到约 10 µs,不再是主要瓶颈。


pgbenchPostgreSQL TPC-B单客户端

kernel ZVFS
TPS 39.1 38.2
avg latency 25.6 ms 26.6 ms

端到端数据库工作负载下IPC 开销被稀释ZVFS 与 kernel 路径性能基本持平(~4% 差距)。


⚠️ 当前局限

  • 不支持 mmap
  • 非对齐写存在 RMW 开销
  • IPC 请求大小存在上限:大 I/O 需在 hook 层分片;改用共享内存 scatter-gather 可消除此限制。

future work

  • 支持 mmap可通过 /dev/shm + userfaultfd 方向探索。
  • 缓解非对齐写开销、!O_DIRECT语义:实现 类似 pagecache 的bufferpool
  • 修改IPC方式使用更快的 Shared Memory
  • 减少通信、拷贝开销:将 I/O 操作迁移至 Application 进程。MetaData操作保留在 Daemon 中。

🧩 遇到的一些问题

SPDK metadata 线程模型

SPDK Blobstore metadata 回调必须在初始化线程执行, 需要严格区分:

  • metadata thread -io thread

否则会导致 callback 无法返回。resize barrier 卡死

spdk_for_each_channel() 在 resize / delete 中会触发 barrier

如果某些线程未 poll 会导致系统卡死。

解决方式:

保证所有 IO thread 持续 poll thread 退出时释放 io_channel

PostgreSQL tablespace hook 失效

PostgreSQL tablespace 通过 symbolic link 访问路径: pg_tblspc/xxx

简单字符串前缀匹配 /zvfs 会漏判。

解决realpath() 后再判断路径

write 延迟显著高于预期

这次 fio 延迟排查里,最初 WRITE 延迟明显高于预期。沿端到端路径加轻量打点后发现问题并不在 SPDK 本体,而是同时叠加了无条件 RMW、VM 中 poller 调度抖动、线程未绑核,以及后期 trace 暴露出来的 reactor 唤醒后核心切换抖动。对应处理是:整块对齐写跳过 read phase、将 reactor/md/io 线程固定到指定 CPU并把 io 线程数和绑核目标收敛到配置项中。修复后 dispatch_spdk 从毫秒级降到几十微秒,WRITE 平均延迟也回落到约 700 µs但剩余尾延迟仍主要表现为请求进入与回包阶段的调度等待。


脚本参数

以下脚本都支持通过环境变量 ZVFS_LD_PRELOAD_VALUE 指定加载的 so 库:

  • scripts/run_fio_matrix.sh
  • scripts/run_pgbench_zvfs.sh
  • scripts/run_db_bench_zvfs.sh
  • scripts/run_test_hook_api.sh

示例:

sudo env ZVFS_LD_PRELOAD_VALUE="$PWD/src/libzvfs.so" ./scripts/run_fio_matrix.sh

其他脚本同理:

sudo env ZVFS_LD_PRELOAD_VALUE="$PWD/src/libzvfs.so" ./scripts/run_pgbench_zvfs.sh
sudo env ZVFS_LD_PRELOAD_VALUE="$PWD/src/libzvfs.so" ./scripts/run_db_bench_zvfs.sh
env ZVFS_LD_PRELOAD_VALUE="$PWD/src/libzvfs.so" ./scripts/run_test_hook_api.sh

延迟 Trace

WRITE / SYNC_MD 的端到端阶段打印通过环境变量 ZVFS_TRACE_LATENCY=1 打开。
打印代码在客户端侧 src/spdk_engine/io_engine.c,输出会写到执行 workload 的进程标准错误。

示例:

sudo env \
  ZVFS_TRACE_LATENCY=1 \
  LD_PRELOAD="$PWD/src/libzvfs.so" \
  fio ./zvfs_fio_test/zvfs/randwrite_4k.fio 2> /tmp/zvfs.write.trace.log

筛出 WRITE trace

grep '\[zvfs\]\[trace\]\[WRITE\]' /tmp/zvfs.write.trace.log

筛出 SYNC_MD trace

grep '\[zvfs\]\[trace\]\[SYNC_MD\]' /tmp/zvfs.write.trace.log

单行输出字段包括:

  • total
  • c2s
  • send
  • server_rx_wait
  • rx_dispatch
  • dispatch_spdk
  • spdk
  • phase1
  • phase2
  • spdk_post
  • kick
  • wake_sched
  • wake_to_tx
  • reply_q
  • cq_wait

更新 README 中的 WRITE 请求端到端延迟分解 时,可对多条 [zvfs][trace][WRITE] 日志按字段取平均后再汇总。