381 lines
10 KiB
Markdown
Executable File
381 lines
10 KiB
Markdown
Executable File
# 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 的映射通过 xattr(user.zvfs.blob_id)持久化,无需额外的映射数据库。
|
||
数据面走 SPDK:read/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 通信。
|
||
|
||
---
|
||
|
||
# 🧠 系统架构
|
||
|
||

|
||
|
||
架构设计关键点:
|
||
|
||
- **同步阻塞语义**
|
||
- **零侵入接管应用 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 IO:read / 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 4K(psync,30s)
|
||
|
||
测试口径:
|
||
|
||
- `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_4k(50/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`,不再是主要瓶颈。
|
||
|
||
---
|
||
|
||
### pgbench(PostgreSQL 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]` 日志按字段取平均后再汇总。
|