4d350d5aea14404cf515ac7b879c4d438fb26f2c
ZVFS
ZVFS 是一个基于 SPDK Blobstore 的用户态文件系统原型,目标是在不改业务代码的前提下,将常见 POSIX 文件 I/O 重定向到用户态高性能存储路径。 核心思想是复用 Linux 文件管理机制(命名空间/目录/元数据),把文件数据平面放到 ZVFS。
- Hook 方式:
LD_PRELOAD - 挂载前缀:
/zvfs - 架构:多进程 Client + 独立 Daemon + SPDK
- 语义:同步阻塞(请求-响应)
1. 项目定位
这个项目重点不只是“把 I/O 跑起来”,而是把以下工程问题串起来:
- 在多线程/多进程应用(RocksDB / PostgreSQL)里做透明接管。
- 保留 POSIX 语义(open/close/dup/fork/append/sync 等)。
- 把 SPDK 资源集中在 daemon 管理,避免每进程重复初始化。
- 在同步阻塞语义下,把协议、并发、错误处理做完整。
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:真实openLinux 文件 + 读取 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语义由 inodelogical_size保证。write成功后会同步更新 Linux 文件大小(ftruncate),保持stat视角一致。mmap对 zvfs fd 当前返回ENOTSUP(非 zvfs fd 透传)。
3.3 映射关系
- 文件数据在 SPDK blob 中。
- 文件到 blob 的映射通过 xattr:
user.zvfs.blob_id。
4. 构建与运行
4.1 构建
cd /home/lian/try/zvfs
git submodule update --init --recursive
cd spdk
./scripts/pkgdep.sh
./configure --with-shared
make -j"$(nproc)"
cd /home/lian/try/zvfs
make -j"$(nproc)"
make test -j"$(nproc)"
产物:
src/libzvfs.sosrc/daemon/zvfs_daemontests/bin/*
4.2 启动 daemon
cd /home/lian/try/zvfs
./src/daemon/zvfs_daemon
可选环境变量:
SPDK_BDEV_NAMESPDK_JSON_CONFIGZVFS_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 工具与脚本
- 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 开销,吞吐明显下降。
6. 关键工程难点与踩坑复盘(重点)
这一节是项目最有价值的部分,记录了从“能跑”到“可用于数据库 workload”过程中遇到的关键问题。
6.1 SPDK 元数据回调线程模型
问题:把 metadata 操作随意派发到任意线程,容易卡住或回调不回来。
根因:
- blobstore metadata 操作与创建线程/通道绑定。
resize/delete/unload内部会走spdk_for_each_channel()barrier。
修复策略:
- 明确 metadata thread 和 io thread 分工。
- 保证持有 channel 的线程持续 poll。
- 线程退出时严格释放 channel,避免 barrier 永久等待。
6.2 Daemon 卡住(请求已收但流程停滞)
现象:请求日志打印到一半后卡住,压测进程阻塞。
根因:
- UDS 流式读取没有完整分帧处理。
- 固定小缓冲导致回包序列化失败(
serialize resp failed)。
修复:
- 改为连接级接收缓冲,循环读到
EAGAIN。 - 按“完整包”消费,残包保留到下一轮。
- 回包序列化改为动态缓冲 +
send_all。
6.3 PostgreSQL Tablespace 无法命中 Hook
现象:建表空间后文件操作路径是 pg_tblspc/...,daemon 无请求日志。
根因:
- PostgreSQL 通过符号链接访问 tablespace。
- 仅按字符串前缀
/zvfs判断会漏判。
修复:
- 路径判定增加
realpath()后再判断。 O_CREAT且文件尚不存在时,使用realpath(parent)+basename判定。
6.4 PostgreSQL 报 Permission denied(跨用户连接 daemon)
现象:CREATE DATABASE ... TABLESPACE ... 报权限错误。
根因:
- daemon 由 root 启动,UDS 文件权限受 umask 影响。
- postgres 用户无法
connect(/tmp/zvfs.sock)。
修复:
- daemon
bind后显式chmod(socket, 0666)。
6.5 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做透明分片发送(保持同步阻塞语义)。
6.6 dup/dup2/fork 语义一致性
问题:多个 fd 指向同一 open file description 时,如何保证 handle 引用计数一致。
方案:
- 协议增加
ADD_REF/ADD_REF_BATCH。 - 在 hook 中对
dup/dup2/dup3/fork明确执行引用增加。 close_range增加边界保护(避免UINT_MAX场景死循环)。
7. 当前限制与下一步
7.1 当前限制
- 单请求仍受
ZVFS_IPC_BUF_SIZE约束。 mmap暂不支持 zvfs fd。ADD_REF_BATCH当前优先功能,不保证原子性。
7.2 下一步计划
- 实现
WRITE客户端透明分片,彻底消除单包上限问题。 - 持续完善 PostgreSQL 场景(tablespace + pgbench + crash/restart)。
- 补齐更系统的性能复测(固定硬件、固定参数、全量报告)。
Description
Languages
C
95.5%
Shell
3.8%
Makefile
0.7%