# 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. 架构设计 ![](zvfs架构图.excalidraw.svg) ```text 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 分层职责 - Client(`src/hook` + `src/spdk_engine/io_engine.c`) - 判断是否 `/zvfs` 路径。 - 拦截 POSIX API 并发起同步 IPC。 - 维护最小本地状态(`fd_table/path_cache/inode_table`)。 - Daemon(`src/daemon`) - 独占 SPDK 环境与线程。 - 统一执行 blob create/open/read/write/resize/sync/delete。 - 统一管理 handle/ref_count。 - 协议层(`src/proto/ipc_proto.*`) - 统一头 + per-op body。 - Request Header:`opcode + payload_len` - Response Header:`opcode + 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 的映射通过 xattr:`user.zvfs.blob_id`。 --- ## 4. 构建与运行 ### 4.1 构建 ```bash cd zvfs git submodule update --init --recursive --progress cd spdk ./scripts/pkgdep.sh ./configure --with-shared make -j"$(nproc)" cd .. make -j"$(nproc)" mkdir -p tests/bin make test -j"$(nproc)" ``` 产物: - `src/libzvfs.so` - `src/daemon/zvfs_daemon` - `tests/bin/*` ### 4.2 启动 daemon ```bash cd zvfs ./src/daemon/zvfs_daemon ``` 可选环境变量: - `SPDK_BDEV_NAME` - `SPDK_JSON_CONFIG` - `ZVFS_SOCKET_PATH` / `ZVFS_IPC_SOCKET_PATH` ### 4.3 快速验证 ```bash 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 工具与脚本 - RocksDB:`scripts/run_db_bench_zvfs.sh` - PostgreSQL:`codex/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 ```shell 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% ``` ### 5.5 pgbench ```shell root@ubuntu20-129:/home/lian/share/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 15.06 s, remaining 0.00 s) done. [2/2] pgbench 压测(-T) starting vacuum...end. transaction type: scaling factor: 1 query mode: simple number of clients: 1 number of threads: 1 duration: 15 s number of transactions actually processed: 564 latency average = 26.614 ms tps = 37.573586 (including connections establishing) tps = 38.176262 (excluding connections establishing) ``` ```shell root@ubuntu20-129:/home/lian/share/zvfs# ./scripts/run_pgbench.sh 当前配置: host=127.0.0.1 port=5432 db=postgres scale=1 clients=1 threads=1 time=15s preload=0 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 1.08 s, remaining 0.00 s) done. [2/2] pgbench 压测(-T) starting vacuum...end. transaction type: scaling factor: 1 query mode: simple number of clients: 1 number of threads: 1 duration: 15 s number of transactions actually processed: 586 latency average = 25.602 ms tps = 39.059387 (including connections establishing) tps = 39.102273 (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` 提高到 `16MB`(`src/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. 补齐更系统的性能复测(固定硬件、固定参数、全量报告)。