Files
zvfs/README.md

13 KiB
Executable File
Raw Blame History

ZVFS

ZVFS 是一个基于 SPDK Blobstore 的用户态文件系统原型,目标是在不改业务代码的前提下,将常见 POSIX 文件 I/O 重定向到用户态高性能存储路径。 核心思想是复用 Linux 文件管理机制(命名空间/目录/元数据),把文件数据平面放到 ZVFS。

  • Hook 方式:LD_PRELOAD
  • 挂载前缀:/zvfs
  • 架构:多进程 Client + 独立 Daemon + SPDK
  • 语义:同步阻塞(请求-响应)

1. 项目定位

这个项目重点不只是“把 I/O 跑起来”,而是把以下工程问题串起来:

  1. 在多线程/多进程应用RocksDB / PostgreSQL里做透明接管。
  2. 保留 POSIX 语义open/close/dup/fork/append/sync 等)。
  3. 把 SPDK 资源集中在 daemon 管理,避免每进程重复初始化。
  4. 在同步阻塞语义下,把协议、并发、错误处理做完整。

2. 架构设计

App (PostgreSQL / RocksDB / db_bench / pgbench)
  -> LD_PRELOAD libzvfs.so
  -> Hook Client (POSIX 拦截 + 本地状态)
  -> Unix Domain Socket IPC (sync/blocking)
  -> zvfs_daemon
      -> 协议反序列化 + 分发
      -> metadata thread + io threads
      -> SPDK Blobstore / bdev

2.1 透传策略

控制面复用 Linux数据面走 ZVFS

  • 控制面Linux 负责)

    • 目录/命名空间管理。
    • 文件节点生命周期与权限语义create/open/close/stat/rename/unlink 等)。
    • 这些操作在 /zvfs 下也会真实执行系统调用ZVFS 不重复实现目录树管理。
  • 数据面ZVFS 负责)

    • 文件内容读写由 blob 承载。
    • read/write 的真实数据路径不走 Linux 文件数据面,而走 ZVFS IPC + SPDK。
  • 关键绑定方式

    • create:真实创建 Linux 文件 + 在 ZVFS 创建 blob + 把 blob_id 写入文件 xattr。
    • open:真实 open Linux 文件 + 读取 xattr 获取 blob_id + 在 ZVFS 打开 blob。
    • write:写入 blob 成功后,使用 ftruncate 同步 Linux 视角 st_size
  • 工程收益

    • 直接减少约 50% 的实现工作量。
    • 兼容性更好,数据库可直接复用现有文件组织方式。

2.2 分层职责

  • Clientsrc/hook + src/spdk_engine/io_engine.c

    • 判断是否 /zvfs 路径。
    • 拦截 POSIX API 并发起同步 IPC。
    • 维护最小本地状态(fd_table/path_cache/inode_table)。
  • Daemonsrc/daemon

    • 独占 SPDK 环境与线程。
    • 统一执行 blob create/open/read/write/resize/sync/delete。
    • 统一管理 handle/ref_count。
  • 协议层(src/proto/ipc_proto.*

    • 统一头 + per-op body。
    • Request Headeropcode + payload_len
    • Response Headeropcode + status + payload_len

2.3 为什么是同步阻塞 IPC

  • 业务侧兼容成本低,最容易对齐 POSIX 语义。
  • 调试路径更直接(一个请求对应一个响应)。
  • 先解决正确性和语义完整,再考虑异步化。

3. 功能覆盖(当前)

3.1 已接管的核心接口

  • 控制面协同:open/openat/creat/rename/unlink/...(真实 syscall + ZVFS 元数据协同)
  • 数据面接管:read/write/pread/pwrite/readv/writev/pwritev
  • 元数据:fstat/lseek/ftruncate/fallocate
  • 同步:fsync/fdatasync/sync_file_range
  • FD 语义:dup/dup2/dup3/fork/close_range

3.2 语义要点

  • write 默认使用 AUTO_GROW
  • AUTO_GROW 写越界返回 ENOSPC
  • O_APPEND 语义由 inode logical_size 保证。
  • write 成功后会同步更新 Linux 文件大小(ftruncate),保持 stat 视角一致。
  • mmap 对 zvfs fd 当前返回 ENOTSUP(非 zvfs fd 透传)。

3.3 映射关系

  • 文件数据在 SPDK blob 中。
  • 文件到 blob 的映射通过 xattruser.zvfs.blob_id

4. 构建与运行

4.1 构建

cd zvfs
git submodule update --init --recursive --progress

cd spdk
./scripts/pkgdep.sh
./configure --with-shared
make -j"$(nproc)"

cd ..
make -j"$(nproc)"
make test -j"$(nproc)"

产物:

  • src/libzvfs.so
  • src/daemon/zvfs_daemon
  • tests/bin/*

4.2 启动 daemon

cd zvfs
./src/daemon/zvfs_daemon

可选环境变量:

  • SPDK_BDEV_NAME
  • SPDK_JSON_CONFIG
  • ZVFS_SOCKET_PATH / ZVFS_IPC_SOCKET_PATH

4.3 快速验证

mkdir -p /zvfs
LD_PRELOAD=./src/libzvfs.so ZVFS_TEST_ROOT=/zvfs ./tests/bin/hook_api_test
./tests/bin/ipc_zvfs_test

5. 性能测试

5.1 测试目标

  • 目标场景:低队列深度下阻塞 I/O 性能。
  • 对比对象:spdk_nvme_perf 与内核路径(O_DIRECT)。

5.2 工具与脚本

  • RocksDBscripts/run_db_bench_zvfs.sh
  • PostgreSQLcodex/run_pgbench_no_mmap.sh

建议:

  • PostgreSQL 测试时关闭 mmap 路径shared memory 改为 sysv避免 mmap 干扰)。

5.3 历史结果

以下是历史版本结论,用于说明设计方向。

  • QD=1 下可达到 spdk_nvme_perf 的约 90%~95%
  • 相对同机 O_DIRECT,顺序写吞吐可有约 2.2x~2.3x 提升。
  • 非对齐写因 RMW 开销,吞吐明显下降。

5.4 fio

root@ubuntu20-129:/home/lian/share/zvfs# LD_PRELOAD=/home/lian/share/zvfs/src/libzvfs.so fio ./fio_script/zvfs.fio 
test: (g=0): rw=randwrite, bs=(R) 16.0KiB-16.0KiB, (W) 16.0KiB-16.0KiB, (T) 16.0KiB-16.0KiB, ioengine=psync, iodepth=64
fio-3.16
Starting 1 thread
test: Laying out IO file (1 file / 0MiB)
Jobs: 1 (f=1): [w(1)][100.0%][w=13.4MiB/s][w=857 IOPS][eta 00m:00s]
test: (groupid=0, jobs=1): err= 0: pid=16519: Sat Mar 14 14:11:27 2026
  Description  : ["variable bs"]
  write: IOPS=829, BW=12.0MiB/s (13.6MB/s)(130MiB/10001msec); 0 zone resets
    clat (usec): min=778, max=4000, avg=1199.89, stdev=377.74
     lat (usec): min=779, max=4001, avg=1200.38, stdev=377.78
    clat percentiles (usec):
     |  1.00th=[  848],  5.00th=[  898], 10.00th=[  922], 20.00th=[  955],
     | 30.00th=[  979], 40.00th=[ 1004], 50.00th=[ 1029], 60.00th=[ 1074],
     | 70.00th=[ 1221], 80.00th=[ 1500], 90.00th=[ 1614], 95.00th=[ 1975],
     | 99.00th=[ 2606], 99.50th=[ 2966], 99.90th=[ 3359], 99.95th=[ 3425],
     | 99.99th=[ 4015]
   bw (  KiB/s): min=10048, max=15520, per=99.91%, avg=13258.32, stdev=1465.96, samples=19
   iops        : min=  628, max=  970, avg=828.63, stdev=91.62, samples=19
  lat (usec)   : 1000=38.46%
  lat (msec)   : 2=56.79%, 4=4.74%, 10=0.01%
  cpu          : usr=5.27%, sys=0.00%, ctx=8499, majf=0, minf=7
  IO depths    : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     issued rwts: total=0,8295,0,0 short=0,0,0,0 dropped=0,0,0,0
     latency   : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):
  WRITE: bw=12.0MiB/s (13.6MB/s), 12.0MiB/s-12.0MiB/s (13.6MB/s-13.6MB/s), io=130MiB (136MB), run=10001-10001msec

Disk stats (read/write):
  sda: ios=0/118, merge=0/104, ticks=0/66, in_queue=67, util=0.24%

psync

root@ubuntu20-129:/home/lian/share/zvfs# fio ./fio_script/psync.fio 
test: (g=0): rw=randwrite, bs=(R) 16.0KiB-16.0KiB, (W) 16.0KiB-16.0KiB, (T) 16.0KiB-16.0KiB, ioengine=psync, iodepth=64
fio-3.16
Starting 1 thread
Jobs: 1 (f=1): [w(1)][100.0%][w=39.5MiB/s][w=2528 IOPS][eta 00m:00s]
test: (groupid=0, jobs=1): err= 0: pid=16831: Sat Mar 14 14:13:39 2026
  Description  : ["variable bs"]
  write: IOPS=2468, BW=38.6MiB/s (40.4MB/s)(386MiB/10001msec); 0 zone resets
    clat (usec): min=184, max=16636, avg=401.35, stdev=153.93
     lat (usec): min=184, max=16636, avg=401.86, stdev=153.98
    clat percentiles (usec):
     |  1.00th=[  247],  5.00th=[  273], 10.00th=[  289], 20.00th=[  318],
     | 30.00th=[  338], 40.00th=[  355], 50.00th=[  375], 60.00th=[  400],
     | 70.00th=[  429], 80.00th=[  469], 90.00th=[  545], 95.00th=[  611],
     | 99.00th=[  783], 99.50th=[  873], 99.90th=[ 1106], 99.95th=[ 1287],
     | 99.99th=[ 1942]
   bw (  KiB/s): min=31136, max=51008, per=100.00%, avg=39550.32, stdev=4617.82, samples=19
   iops        : min= 1946, max= 3188, avg=2471.79, stdev=288.55, samples=19
  lat (usec)   : 250=1.32%, 500=83.72%, 750=13.61%, 1000=1.11%
  lat (msec)   : 2=0.23%, 4=0.01%, 20=0.01%
  cpu          : usr=2.83%, sys=28.58%, ctx=24699, majf=0, minf=0
  IO depths    : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     issued rwts: total=0,24690,0,0 short=0,0,0,0 dropped=0,0,0,0
     latency   : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):
  WRITE: bw=38.6MiB/s (40.4MB/s), 38.6MiB/s-38.6MiB/s (40.4MB/s-40.4MB/s), io=386MiB (405MB), run=10001-10001msec

Disk stats (read/write):
  sda: ios=0/24384, merge=0/4, ticks=0/7764, in_queue=7764, util=99.10%

5.5 pgbench

root@ubuntu20:/home/lian/try/zvfs# ./scripts/run_pgbench_no_mmap.sh 
当前配置:
  host=127.0.0.1 port=5432 db=benchdb
  scale=1 clients=1 threads=1 time=15s preload=1
  init_jobs=1 init_steps=dtg skip_init=0

[1/2] pgbench 初始化(-i
some of the specified options cannot be used in initialization (-i) mode
root@ubuntu20:/home/lian/try/zvfs# ./scripts/run_pgbench_no_mmap.sh 
当前配置:
  host=127.0.0.1 port=5432 db=benchdb
  scale=1 clients=1 threads=1 time=15s preload=1
  init_jobs=1 init_steps=dtg skip_init=0

[1/2] pgbench 初始化(-i
dropping old tables...
NOTICE:  table "pgbench_accounts" does not exist, skipping
NOTICE:  table "pgbench_branches" does not exist, skipping
NOTICE:  table "pgbench_history" does not exist, skipping
NOTICE:  table "pgbench_tellers" does not exist, skipping
creating tables...
generating data...
100000 of 100000 tuples (100%) done (elapsed 0.02 s, remaining 0.00 s)
done.
[2/2] pgbench 压测(-T
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
duration: 15 s
number of transactions actually processed: 1381
latency average = 10.869 ms
tps = 92.003503 (including connections establishing)
tps = 92.206743 (excluding connections establishing)

6. 关键工程难点与踩坑复盘(重点)

SPDK 元数据回调线程模型

问题:把 metadata 操作随意派发到任意线程容易卡住或回调不回来。metadata的回调默认派发给初始化blobstore的线程。

blobstore metadata 操作与创建线程/通道绑定。

需要明确 metadata thread 和 io thread 分工。

resize 导致程序卡死

  • resize/delete/unload 内部会走 spdk_for_each_channel() barrier。大概是让其他的spdk线程同步resize之后的状态才能返回。所以如果要做要尽可能少做resize。

如果其他spdk线程持有iochannel并且没有持续poll就会导致卡死。

  • 保证持有 channel 的线程持续 poll。
  • 线程退出时严格释放 channel避免 barrier 永久等待。

PostgreSQL Tablespace 无法命中 Hook

现象:建表空间后文件操作路径是 pg_tblspc/...daemon 无请求日志。

根因:

  • PostgreSQL 通过符号链接访问 tablespace。
  • 仅按字符串前缀 /zvfs 判断会漏判。

修复:

  • 路径判定增加 realpath() 后再判断。
  • O_CREAT 且文件尚不存在时,使用 realpath(parent)+basename 判定。

PostgreSQL 报 Permission denied(跨用户连接 daemon

现象:CREATE DATABASE ... TABLESPACE ... 报权限错误。

根因:

  • daemon 由 root 启动UDS 文件权限受 umask 影响。
  • postgres 用户无法 connect(/tmp/zvfs.sock)

修复:

  • daemon bind 后显式 chmod(socket, 0666)

PostgreSQL 报 Message too long

现象:部分 SQL尤其 CREATE DATABASE 路径)失败,错误为 Message too long

根因:

  • 不是 daemon 解析失败,而是 client 序列化请求时超出 ZVFS_IPC_BUF_SIZE
  • 当前 hook 会把 writev 聚合成一次大写请求,容易触发上限。

当前处理:

  • ZVFS_IPC_BUF_SIZE 提高到 16MBsrc/common/config.h)。

后续优化方向:

  • 在 client blob_write_ex 做透明分片发送(保持同步阻塞语义)。

dup/dup2/fork 语义一致性

问题:多个 fd 指向同一 open file description 时,如何保证 handle 引用计数一致。

方案:

  • 协议增加 ADD_REF / ADD_REF_BATCH
  • 在 hook 中对 dup/dup2/dup3/fork 明确执行引用增加。
  • close_range 增加边界保护(避免 UINT_MAX 场景死循环)。

pg \c 调用链条

  1. psql \c ...

  2. fork backend

  3. InitPostgres -> ValidatePgVersion ...

  4. libc fopen

  5. libc 内部 -> __IO_file_fopen -> _IO_file_open -> __open64

  6. kernel openat

  7. fscanf

  8. libc __isoc99_fscanf -> _IO_* -> __read

  9. kernel read

glic 内部调用走的不是 动态符号定位可能是一些隐藏别名。可能会绕过hook。需要拦截非常多变体

7. 当前限制与下一步

7.1 当前限制

  • 单请求仍受 ZVFS_IPC_BUF_SIZE 约束。
  • mmap 暂不支持 zvfs fd。
  • ADD_REF_BATCH 当前优先功能,不保证原子性。

7.2 下一步计划

  1. 实现 WRITE 客户端透明分片,彻底消除单包上限问题。
  2. 持续完善 PostgreSQL 场景tablespace + pgbench + crash/restart
  3. 补齐更系统的性能复测(固定硬件、固定参数、全量报告)。