219 lines
6.4 KiB
Markdown
Executable File
219 lines
6.4 KiB
Markdown
Executable File
# ZVFS
|
||
|
||
ZVFS 是一个基于 `SPDK Blobstore` 的轻量级用户态文件系统原型,
|
||
通过 `LD_PRELOAD` 拦截常见 POSIX 文件 API,把 `/zvfs` 路径下的文件 I/O 转换为 Blob I/O。
|
||
|
||
目标是让上层应用尽量少改动地复用阻塞式文件接口,同时接近 SPDK 在低队列深度(QD≈1)场景的性能上限。
|
||
|
||
## 1. 项目结构
|
||
|
||
```text
|
||
zvfs/
|
||
├── src/
|
||
│ ├── hook/ # POSIX API hook 层(open/read/write/...)
|
||
│ ├── fs/ # inode/path/fd 运行时元数据管理
|
||
│ ├── spdk_engine/ # SPDK Blobstore 封装
|
||
│ ├── common/ # 对齐与缓冲区工具函数
|
||
│ ├── config.h # 默认配置(JSON、bdev、xattr key 等)
|
||
│ └── Makefile # 产出 libzvfs.so
|
||
├── tests/
|
||
│ ├── hook/ # hook API 语义测试
|
||
│ ├── ioengine_test/ # Blob 引擎单元测试
|
||
│ └── Makefile
|
||
├── scripts/ # db_bench/hook 测试辅助脚本
|
||
├── spdk/ # SPDK 子模块
|
||
└── README.md
|
||
```
|
||
|
||
## 2. 核心架构
|
||
|
||
### 2.1 分层
|
||
|
||
当前实现:
|
||
|
||
```text
|
||
App (open/read/write/fstat/...)
|
||
-> LD_PRELOAD Hook (src/hook)
|
||
-> ZVFS Runtime Metadata (src/fs)
|
||
-> SPDK Engine (src/spdk_engine)
|
||
-> SPDK Blobstore
|
||
-> bdev (Malloc/NVMe)
|
||
```
|
||
|
||
目标架构(Daemon + IPC):
|
||
|
||
```text
|
||
App (multi-process, e.g. PostgreSQL)
|
||
-> LD_PRELOAD Hook Client
|
||
-> IPC (Unix Domain Socket)
|
||
-> zvfs daemon
|
||
-> metadata manager
|
||
-> SPDK worker threads
|
||
-> SPDK Blobstore / bdev
|
||
```
|
||
|
||
### 2.2 目标架构简版(HOOK 层 + daemon 层)
|
||
|
||
- `HOOK 层`
|
||
- 拦截 `/zvfs` 路径的 POSIX API 并同步发起 IPC 请求。
|
||
- 维护本地最小状态(如 `fd -> remote_handle_id`)。
|
||
- 对非 `/zvfs` 路径继续透传到 `real_*` syscall(POSIX passthrough)。
|
||
- `daemon 层`
|
||
- 独占 SPDK 资源(`spdk_env/blobstore/spdk_thread`)。
|
||
- 统一处理元数据与并发控制(path/inode/handle)。
|
||
- 接收 IPC 请求并执行实际 I/O,返回 POSIX 风格结果与 errno。
|
||
|
||
### 2.3 元数据与数据映射
|
||
|
||
- 文件数据:存储在 SPDK blob 中。
|
||
- 文件到 blob 的映射:写入真实文件的 `xattr`(key: `user.zvfs.blob_id`)。
|
||
- 运行时维护三张表:
|
||
- `inode_table`:`blob_id -> inode`
|
||
- `path_cache`:`path -> inode`
|
||
- `fd_table`:`fd -> open_file`
|
||
|
||
### 2.4 当前实现的 I/O 路径要点
|
||
|
||
- `blob_read/blob_write` 统一走按 `io_unit_size` 对齐的 DMA 缓冲。
|
||
- 非对齐写会触发读改写(RMW):先读对齐块,再覆盖局部写回。
|
||
- `readv/writev` 在 hook 层会做聚合,减少多次 I/O 提交。
|
||
- `fsync/fdatasync` 对 zvfs fd 调用 `blob_sync_md`;`sync_file_range` 在 zvfs 路径直接返回成功。
|
||
|
||
## 3. 构建
|
||
|
||
> 下面命令以仓库根目录为 `/home/lian/try/zvfs` 为例。
|
||
|
||
### 3.1 初始化并构建 SPDK
|
||
|
||
```bash
|
||
git submodule update --init --recursive
|
||
cd spdk
|
||
./scripts/pkgdep.sh
|
||
./configure --with-shared
|
||
make -j"$(nproc)"
|
||
```
|
||
|
||
### 3.2 构建 ZVFS 与测试
|
||
|
||
```bash
|
||
cd /home/lian/try/zvfs
|
||
make -j"$(nproc)"
|
||
make test -j"$(nproc)"
|
||
```
|
||
|
||
产物:
|
||
|
||
- `src/libzvfs.so`
|
||
- `tests/bin/hook_api_test`
|
||
- `tests/bin/ioengine_single_blob_test`
|
||
- `tests/bin/ioengine_multi_blob_test`
|
||
- `tests/bin/ioengine_same_blob_mt_test`
|
||
|
||
## 4. 运行与验证
|
||
|
||
### 4.1 Hook API 语义测试
|
||
|
||
```bash
|
||
mkdir -p /zvfs
|
||
cd /home/lian/try/zvfs
|
||
LD_PRELOAD=$PWD/src/libzvfs.so ZVFS_TEST_ROOT=/zvfs ./tests/bin/hook_api_test
|
||
```
|
||
|
||
覆盖点包括:
|
||
|
||
- `open/openat/rename/unlink`
|
||
- `read/write/pread/pwrite/readv/writev/pwritev`
|
||
- `fstat/lseek/ftruncate`
|
||
- `fcntl/ioctl(FIONREAD)`
|
||
- `fsync/fdatasync`
|
||
|
||
### 4.2 SPDK 引擎测试
|
||
|
||
```bash
|
||
cd /home/lian/try/zvfs
|
||
SPDK_BDEV_NAME=Malloc0 ./tests/bin/ioengine_single_blob_test
|
||
SPDK_BDEV_NAME=Malloc0 ./tests/bin/ioengine_multi_blob_test
|
||
SPDK_BDEV_NAME=Malloc0 ./tests/bin/ioengine_same_blob_mt_test
|
||
```
|
||
|
||
## 5. 关键环境变量
|
||
|
||
- `SPDK_BDEV_NAME`:选择后端 bdev(默认 `Malloc0`)。
|
||
- `ZVFS_BDEV`:`zvfs_ensure_init` 使用的 bdev 名称(未设置时使用 `config.h` 默认值)。
|
||
- `SPDK_JSON_CONFIG`:覆盖默认 SPDK JSON 配置路径。
|
||
|
||
## 6. 性能说明(仅保留趋势)
|
||
|
||
`README` 历史压测数据来自旧版本,不能直接当作当前版本结论;但可作为设计趋势参考:
|
||
|
||
- 目标工作负载为阻塞 API,近似 `QD=1`。
|
||
- 旧数据下,ZVFS 在 `QD=1` 时约达到 `spdk_nvme_perf` 的 `90%~95%`。
|
||
- 4K:约 `95 MiB/s` vs `100 MiB/s`
|
||
- 128K:约 `1662 MiB/s` vs `1843 MiB/s`
|
||
- 相对同机 `O_DIRECT` 路径,旧数据写带宽约有 `2.2x~2.3x` 提升。
|
||
- 非对齐写存在 RMW,吞吐明显下降(旧数据常见接近对齐写的一半)。
|
||
|
||
如果需要用于对外汇报,请重新在当前 commit 与固定硬件环境下复测。
|
||
|
||
## 7. 当前限制
|
||
|
||
- 仅拦截 `/zvfs` 路径。
|
||
- `mmap` 对 zvfs fd 当前返回 `ENOTSUP`(建议上层关闭 mmap 读写)。
|
||
- `dup/dup2/dup3` 对 zvfs fd 当前返回 `ENOTSUP`。
|
||
- `rename` 跨 `/zvfs` 与非 `/zvfs` 路径返回 `EXDEV`。
|
||
- `fallocate(FALLOC_FL_PUNCH_HOLE)` 未实现。
|
||
|
||
## 8. 后续建议
|
||
|
||
- 补齐 mmap 路径(mmap table + 脏页回写)。
|
||
- 完善多线程/高并发下的语义与压测基线。
|
||
- 增加版本化 benchmark 报告,避免 README 中历史数据失真。
|
||
|
||
## 9. Blob Store 血泪教训
|
||
|
||
### Owner Thread 绑定
|
||
|
||
blobstore内部负责并发控制,让所有metadata操作都在一个线程上执行,回调固定绑定给创建blobstore的线程。所以多线程模型下不是send给谁谁就能poll到回调的。
|
||
|
||
正确架构:
|
||
```
|
||
metadata thread
|
||
spdk_bs_load()
|
||
resize
|
||
delete
|
||
sync_md
|
||
|
||
worker thread
|
||
blob_io_read
|
||
blob_io_write
|
||
```
|
||
|
||
### spdk_for_each_channel() Barrier
|
||
某些 metadata 操作非常慢:
|
||
```
|
||
resize
|
||
delete
|
||
unload
|
||
snapshot
|
||
```
|
||
这些操作内部会调用:spdk_for_each_channel()
|
||
|
||
语义:在所有 io_channel 所属线程执行 callback
|
||
|
||
类似
|
||
```c
|
||
for each channel:
|
||
send_msg(channel->thread)
|
||
```
|
||
|
||
#### 问题1:持有 Channel 的 Thread 不 poll
|
||
如果所属线程不poll,就会卡住。
|
||
#### 问题2:线程退出 Channel 没有释放
|
||
永远卡住
|
||
|
||
### IO 操作的回调行为与 metadata 操作不同
|
||
spdk_blob_io_read / spdk_blob_io_write 的回调,是通过传入的 io_channel 投递的,回调回到分配该 channel 的 thread。
|
||
|
||
### 超时任务
|
||
设置超时就免不了超时后回调成功执行,超时后回调仍会触发,存在 UAF 风险
|