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

381 lines
10 KiB
Markdown
Executable File
Raw Permalink 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
> 透明用户态 POSIX 文件系统,基于 SPDK Blobstore。
ZVFS 是一个 **透明用户态文件系统原型**,通过 `LD_PRELOAD` 劫持 POSIX I/O
将应用程序的文件数据路径从 Linux 内核 I/O 栈重定向到 **SPDK 用户态 NVMe 存储路径**
目标是在 **零业务代码修改** 的情况下,为数据库与向量检索系统提供更低延迟的存储访问。
目前已在 **PostgreSQL + pgvector** 场景完成功能验证。
---
# 测试方案
```shell
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 通信。
---
# 🧠 系统架构
![](zvfs架构图.excalidraw.svg)
架构设计关键点:
- **同步阻塞语义**
- **零侵入接管应用 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 指向同一文件句柄时语义一致。
---
# 📦 构建
```bash
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
```bash
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.log``107946``WRITE` trace 样本统计,下面按调用栈层级展开平均耗时。由于四舍五入,父子项相加会有 `±1 µs` 误差。
```text
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`
示例:
```bash
sudo env ZVFS_LD_PRELOAD_VALUE="$PWD/src/libzvfs.so" ./scripts/run_fio_matrix.sh
```
其他脚本同理:
```bash
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`](/home/lian/share/zvfs/src/spdk_engine/io_engine.c),输出会写到执行 workload 的进程标准错误。
示例:
```bash
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
```bash
grep '\[zvfs\]\[trace\]\[WRITE\]' /tmp/zvfs.write.trace.log
```
筛出 `SYNC_MD` trace
```bash
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]` 日志按字段取平均后再汇总。