Files
zvfs/README.md
2026-03-23 22:05:23 +08:00

246 lines
7.5 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
> 透明用户态 POSIX 文件系统,基于 SPDK Blobstore。
ZVFS 是一个 **透明用户态文件系统原型**,通过 `LD_PRELOAD` 劫持 POSIX I/O
将应用程序的文件数据路径从 Linux 内核 I/O 栈重定向到 **SPDK 用户态 NVMe 存储路径**
目标是在 **零业务代码修改** 的情况下,为数据库与向量检索系统提供更低延迟的存储访问。
目前已在 **PostgreSQL + pgvector** 场景完成功能验证。
---
# 设计思路
大多数用户态文件系统(如 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 的额外开销,不代表真实硬件上的性能对比。
### 顺序写吞吐
| Block Size | spdk_nvme_perf | ZVFS |
|---|---|---|
| 4K | 100 MiB/s | 94 MiB/s |
| 128K | 1843 MiB/s | 1662 MiB/s |
ZVFS 达到 **SPDK 原生性能约 90%**
---
### fio 随机写16Kpsync
| | kernel (psync) | ZVFS |
|---|---|---|
| IOPS | 1855 | 1353 |
| 吞吐 | 28.0 MiB/s | 21.2 MiB/s |
| avg clat | 492 µs | 692 µs |
| sys% | 28.6% | 8.4% |
> 当前 ZVFS 在该单线程 `psync` 随机写场景下达到 kernel `psync` 的约 73% IOPS。daemon 内部 `SPDK + reply_q` 已收敛到较稳定范围,剩余主要开销集中在 `client -> daemon` 请求进入阶段。
---
### WRITE 请求端到端延迟分解(单位 µs
基于 12 条 `WRITE` trace 样本统计,下面按调用栈层级展开平均耗时。由于四舍五入,父子项相加会有 `±1 µs` 误差。
```text
total 748
├─ c2s 317
│ ├─ send 39
│ └─ server_rx_wait 278
├─ server 336
│ ├─ rx_dispatch 12
│ ├─ dispatch_spdk 25
│ ├─ spdk 194
│ └─ reply_q 103
│ ├─ spdk_post 11
│ └─ cq_wait 91
│ ├─ kick 13
│ ├─ wake_sched 65
│ └─ wake_to_tx 12
└─ s2c 95
├─ resp_wait 83
└─ parse 12
```
当前 `WRITE` 的主要额外开销已经比较清晰:一是 `c2s / server_rx_wait`,二是 `server` 内部的 `spdk``reply_q`。在 `reply_q` 中,`wake_sched` 已明显大于 `kick``wake_to_tx`,说明回包路径的主要损耗不在 `eventfd` 写入本身,而在 reactor 被唤醒后的调度等待。
---
### 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但剩余尾延迟仍主要表现为请求进入与回包阶段的调度等待。