Files
zvfs/README.md
2026-03-13 13:40:19 +00:00

433 lines
16 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 /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.so`
- `src/daemon/zvfs_daemon`
- `tests/bin/*`
### 4.2 启动 daemon
```bash
cd /home/lian/try/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:/home/lian/try/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=53.0MiB/s][w=3455 IOPS][eta 00m:00s]
test: (groupid=0, jobs=1): err= 0: pid=23035: Fri Mar 13 13:25:32 2026
Description : ["variable bs"]
write: IOPS=3644, BW=56.9MiB/s (59.7MB/s)(570MiB/10001msec); 0 zone resets
clat (usec): min=127, max=3496, avg=272.07, stdev=91.62
lat (usec): min=128, max=3497, avg=272.41, stdev=91.69
clat percentiles (usec):
| 1.00th=[ 155], 5.00th=[ 169], 10.00th=[ 184], 20.00th=[ 202],
| 30.00th=[ 225], 40.00th=[ 249], 50.00th=[ 262], 60.00th=[ 277],
| 70.00th=[ 293], 80.00th=[ 322], 90.00th=[ 371], 95.00th=[ 420],
| 99.00th=[ 545], 99.50th=[ 611], 99.90th=[ 881], 99.95th=[ 1467],
| 99.99th=[ 2409]
bw ( KiB/s): min=49376, max=70387, per=99.97%, avg=58295.30, stdev=6828.37, samples=20
iops : min= 3086, max= 4399, avg=3643.40, stdev=426.71, samples=20
lat (usec) : 250=41.44%, 500=56.78%, 750=1.59%, 1000=0.10%
lat (msec) : 2=0.07%, 4=0.01%
cpu : usr=1.82%, sys=37.19%, ctx=54169, 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,36448,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=56.9MiB/s (59.7MB/s), 56.9MiB/s-56.9MiB/s (59.7MB/s-59.7MB/s), io=570MiB (597MB), run=10001-10001msec
Disk stats (read/write):
dm-0: ios=122/36085, merge=0/0, ticks=32/7748, in_queue=7780, util=99.13%, aggrios=109/36455, aggrmerge=13/12, aggrticks=28/7260, aggrin_queue=0, aggrutil=98.95%
sda: ios=109/36455, merge=13/12, ticks=28/7260, in_queue=0, util=98.95%
root@ubuntu20:/home/lian/try/zvfs# LD_PRELOAD=/home/lian/try/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
Jobs: 1 (f=1): [w(1)][100.0%][w=10.2MiB/s][w=650 IOPS][eta 00m:00s]
test: (groupid=0, jobs=1): err= 0: pid=23891: Fri Mar 13 13:26:54 2026
Description : ["variable bs"]
write: IOPS=521, BW=8345KiB/s (8545kB/s)(81.5MiB/10001msec); 0 zone resets
clat (usec): min=529, max=52465, avg=1909.16, stdev=1181.14
lat (usec): min=530, max=52467, avg=1909.72, stdev=1181.21
clat percentiles (usec):
| 1.00th=[ 734], 5.00th=[ 922], 10.00th=[ 1037], 20.00th=[ 1237],
| 30.00th=[ 1418], 40.00th=[ 1500], 50.00th=[ 1614], 60.00th=[ 1860],
| 70.00th=[ 2024], 80.00th=[ 2311], 90.00th=[ 3130], 95.00th=[ 3982],
| 99.00th=[ 5669], 99.50th=[ 5932], 99.90th=[ 6456], 99.95th=[ 6849],
| 99.99th=[52691]
bw ( KiB/s): min= 4704, max=11200, per=99.95%, avg=8339.75, stdev=2577.19, samples=20
iops : min= 294, max= 700, avg=521.10, stdev=161.03, samples=20
lat (usec) : 750=1.23%, 1000=7.07%
lat (msec) : 2=60.10%, 4=26.84%, 10=4.74%, 100=0.02%
cpu : usr=0.00%, sys=3.84%, ctx=5461, 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,5216,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=8345KiB/s (8545kB/s), 8345KiB/s-8345KiB/s (8545kB/s-8545kB/s), io=81.5MiB (85.5MB), run=10001-10001msec
Disk stats (read/write):
dm-0: ios=39/7287, merge=0/0, ticks=12/7888, in_queue=7900, util=1.72%, aggrios=39/478, aggrmerge=0/6818, aggrticks=12/352, aggrin_queue=0, aggrutil=1.70%
sda: ios=39/478, merge=0/6818, ticks=12/352, in_queue=0, util=1.70%
```
#### psync
```shell
root@ubuntu20:/home/lian/try/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=53.0MiB/s][w=3455 IOPS][eta 00m:00s]
test: (groupid=0, jobs=1): err= 0: pid=23035: Fri Mar 13 13:25:32 2026
Description : ["variable bs"]
write: IOPS=3644, BW=56.9MiB/s (59.7MB/s)(570MiB/10001msec); 0 zone resets
clat (usec): min=127, max=3496, avg=272.07, stdev=91.62
lat (usec): min=128, max=3497, avg=272.41, stdev=91.69
clat percentiles (usec):
| 1.00th=[ 155], 5.00th=[ 169], 10.00th=[ 184], 20.00th=[ 202],
| 30.00th=[ 225], 40.00th=[ 249], 50.00th=[ 262], 60.00th=[ 277],
| 70.00th=[ 293], 80.00th=[ 322], 90.00th=[ 371], 95.00th=[ 420],
| 99.00th=[ 545], 99.50th=[ 611], 99.90th=[ 881], 99.95th=[ 1467],
| 99.99th=[ 2409]
bw ( KiB/s): min=49376, max=70387, per=99.97%, avg=58295.30, stdev=6828.37, samples=20
iops : min= 3086, max= 4399, avg=3643.40, stdev=426.71, samples=20
lat (usec) : 250=41.44%, 500=56.78%, 750=1.59%, 1000=0.10%
lat (msec) : 2=0.07%, 4=0.01%
cpu : usr=1.82%, sys=37.19%, ctx=54169, 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,36448,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=56.9MiB/s (59.7MB/s), 56.9MiB/s-56.9MiB/s (59.7MB/s-59.7MB/s), io=570MiB (597MB), run=10001-10001msec
Disk stats (read/write):
dm-0: ios=122/36085, merge=0/0, ticks=32/7748, in_queue=7780, util=99.13%, aggrios=109/36455, aggrmerge=13/12, aggrticks=28/7260, aggrin_queue=0, aggrutil=98.95%
sda: ios=109/36455, merge=13/12, ticks=28/7260, in_queue=0, util=98.95%
```
### 5.5 pgbench
```shell
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. 关键工程难点与踩坑复盘(重点)
这一节是项目最有价值的部分,记录了从“能跑”到“可用于数据库 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 下一步计划
1. 实现 `WRITE` 客户端透明分片,彻底消除单包上限问题。
2. 持续完善 PostgreSQL 场景tablespace + pgbench + crash/restart
3. 补齐更系统的性能复测(固定硬件、固定参数、全量报告)。