postgres hook 测试成功
This commit is contained in:
366
README.md
366
README.md
@@ -1,101 +1,126 @@
|
||||
# ZVFS
|
||||
|
||||
ZVFS 是一个基于 `SPDK Blobstore` 的轻量级用户态文件系统原型,
|
||||
通过 `LD_PRELOAD` 拦截常见 POSIX 文件 API,把 `/zvfs` 路径下的文件 I/O 转换为 Blob I/O。
|
||||
ZVFS 是一个基于 SPDK Blobstore 的用户态文件系统原型,目标是在不改业务代码的前提下,将常见 POSIX 文件 I/O 重定向到用户态高性能存储路径。
|
||||
核心思想是复用 Linux 文件管理机制(命名空间/目录/元数据),把文件数据平面放到 ZVFS。
|
||||
|
||||
目标是让上层应用尽量少改动地复用阻塞式文件接口,同时接近 SPDK 在低队列深度(QD≈1)场景的性能上限。
|
||||
- Hook 方式:`LD_PRELOAD`
|
||||
- 挂载前缀:`/zvfs`
|
||||
- 架构:多进程 Client + 独立 Daemon + SPDK
|
||||
- 语义:同步阻塞(请求-响应)
|
||||
|
||||
## 1. 项目结构
|
||||
---
|
||||
|
||||
## 1. 项目定位
|
||||
|
||||
这个项目重点不只是“把 I/O 跑起来”,而是把以下工程问题串起来:
|
||||
|
||||
1. 在多线程/多进程应用(RocksDB / PostgreSQL)里做透明接管。
|
||||
2. 保留 POSIX 语义(open/close/dup/fork/append/sync 等)。
|
||||
3. 把 SPDK 资源集中在 daemon 管理,避免每进程重复初始化。
|
||||
4. 在同步阻塞语义下,把协议、并发、错误处理做完整。
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||

|
||||
|
||||
```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
|
||||
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. 核心架构
|
||||
### 2.1 透传策略
|
||||
|
||||
### 2.1 分层
|
||||
**控制面复用 Linux,数据面走 ZVFS**。
|
||||
|
||||
当前实现:
|
||||
- 控制面(Linux 负责)
|
||||
- 目录/命名空间管理。
|
||||
- 文件节点生命周期与权限语义(create/open/close/stat/rename/unlink 等)。
|
||||
- 这些操作在 `/zvfs` 下也会真实执行系统调用,ZVFS 不重复实现目录树管理。
|
||||
|
||||
```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)
|
||||
```
|
||||
- 数据面(ZVFS 负责)
|
||||
- 文件内容读写由 blob 承载。
|
||||
- `read/write` 的真实数据路径不走 Linux 文件数据面,而走 ZVFS IPC + SPDK。
|
||||
|
||||
目标架构(Daemon + IPC):
|
||||
- 关键绑定方式
|
||||
- `create`:真实创建 Linux 文件 + 在 ZVFS 创建 blob + 把 `blob_id` 写入文件 xattr。
|
||||
- `open`:真实 `open` Linux 文件 + 读取 xattr 获取 `blob_id` + 在 ZVFS 打开 blob。
|
||||
- `write`:写入 blob 成功后,使用 `ftruncate` 同步 Linux 视角 `st_size`。
|
||||
|
||||
```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
|
||||
```
|
||||
- 工程收益
|
||||
- 直接减少约 50% 的实现工作量。
|
||||
- 兼容性更好,数据库可直接复用现有文件组织方式。
|
||||
|
||||
### 2.2 目标架构简版(HOOK 层 + daemon 层)
|
||||
### 2.2 分层职责
|
||||
|
||||
- `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。
|
||||
- Client(`src/hook` + `src/spdk_engine/io_engine.c`)
|
||||
- 判断是否 `/zvfs` 路径。
|
||||
- 拦截 POSIX API 并发起同步 IPC。
|
||||
- 维护最小本地状态(`fd_table/path_cache/inode_table`)。
|
||||
|
||||
### 2.3 元数据与数据映射
|
||||
- Daemon(`src/daemon`)
|
||||
- 独占 SPDK 环境与线程。
|
||||
- 统一执行 blob create/open/read/write/resize/sync/delete。
|
||||
- 统一管理 handle/ref_count。
|
||||
|
||||
- 文件数据:存储在 SPDK blob 中。
|
||||
- 文件到 blob 的映射:写入真实文件的 `xattr`(key: `user.zvfs.blob_id`)。
|
||||
- 运行时维护三张表:
|
||||
- `inode_table`:`blob_id -> inode`
|
||||
- `path_cache`:`path -> inode`
|
||||
- `fd_table`:`fd -> open_file`
|
||||
- 协议层(`src/proto/ipc_proto.*`)
|
||||
- 统一头 + per-op body。
|
||||
- Request Header:`opcode + payload_len`
|
||||
- Response Header:`opcode + status + payload_len`
|
||||
|
||||
### 2.4 当前实现的 I/O 路径要点
|
||||
### 2.3 为什么是同步阻塞 IPC
|
||||
|
||||
- `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 路径直接返回成功。
|
||||
- 业务侧兼容成本低,最容易对齐 POSIX 语义。
|
||||
- 调试路径更直接(一个请求对应一个响应)。
|
||||
- 先解决正确性和语义完整,再考虑异步化。
|
||||
|
||||
## 3. 构建
|
||||
---
|
||||
|
||||
> 下面命令以仓库根目录为 `/home/lian/try/zvfs` 为例。
|
||||
## 3. 功能覆盖(当前)
|
||||
|
||||
### 3.1 初始化并构建 SPDK
|
||||
### 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)"
|
||||
```
|
||||
|
||||
### 3.2 构建 ZVFS 与测试
|
||||
|
||||
```bash
|
||||
cd /home/lian/try/zvfs
|
||||
make -j"$(nproc)"
|
||||
make test -j"$(nproc)"
|
||||
@@ -104,115 +129,158 @@ 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`
|
||||
- `src/daemon/zvfs_daemon`
|
||||
- `tests/bin/*`
|
||||
|
||||
## 4. 运行与验证
|
||||
### 4.2 启动 daemon
|
||||
|
||||
### 4.1 Hook API 语义测试
|
||||
```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
|
||||
cd /home/lian/try/zvfs
|
||||
LD_PRELOAD=$PWD/src/libzvfs.so ZVFS_TEST_ROOT=/zvfs ./tests/bin/hook_api_test
|
||||
LD_PRELOAD=./src/libzvfs.so ZVFS_TEST_ROOT=/zvfs ./tests/bin/hook_api_test
|
||||
./tests/bin/ipc_zvfs_test
|
||||
```
|
||||
|
||||
覆盖点包括:
|
||||
---
|
||||
|
||||
- `open/openat/rename/unlink`
|
||||
- `read/write/pread/pwrite/readv/writev/pwritev`
|
||||
- `fstat/lseek/ftruncate`
|
||||
- `fcntl/ioctl(FIONREAD)`
|
||||
- `fsync/fdatasync`
|
||||
## 5. 性能测试
|
||||
|
||||
### 4.2 SPDK 引擎测试
|
||||
### 5.1 测试目标
|
||||
|
||||
```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
|
||||
```
|
||||
- 目标场景:低队列深度下阻塞 I/O 性能。
|
||||
- 对比对象:`spdk_nvme_perf` 与内核路径(`O_DIRECT`)。
|
||||
|
||||
## 5. 关键环境变量
|
||||
### 5.2 工具与脚本
|
||||
|
||||
- `SPDK_BDEV_NAME`:选择后端 bdev(默认 `Malloc0`)。
|
||||
- `ZVFS_BDEV`:`zvfs_ensure_init` 使用的 bdev 名称(未设置时使用 `config.h` 默认值)。
|
||||
- `SPDK_JSON_CONFIG`:覆盖默认 SPDK JSON 配置路径。
|
||||
- RocksDB:`scripts/run_db_bench_zvfs.sh`
|
||||
- PostgreSQL:`codex/run_pgbench_no_mmap.sh`
|
||||
|
||||
## 6. 性能说明(仅保留趋势)
|
||||
建议:
|
||||
|
||||
`README` 历史压测数据来自旧版本,不能直接当作当前版本结论;但可作为设计趋势参考:
|
||||
- PostgreSQL 测试时关闭 mmap 路径(shared memory 改为 sysv,避免 mmap 干扰)。
|
||||
|
||||
- 目标工作负载为阻塞 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,吞吐明显下降(旧数据常见接近对齐写的一半)。
|
||||
### 5.3 历史结果
|
||||
|
||||
如果需要用于对外汇报,请重新在当前 commit 与固定硬件环境下复测。
|
||||
> 以下是历史版本结论,用于说明设计方向。
|
||||
|
||||
## 7. 当前限制
|
||||
- QD=1 下可达到 `spdk_nvme_perf` 的约 `90%~95%`。
|
||||
- 相对同机 `O_DIRECT`,顺序写吞吐可有约 `2.2x~2.3x` 提升。
|
||||
- 非对齐写因 RMW 开销,吞吐明显下降。
|
||||
|
||||
- 仅拦截 `/zvfs` 路径。
|
||||
- `mmap` 对 zvfs fd 当前返回 `ENOTSUP`(建议上层关闭 mmap 读写)。
|
||||
- `dup/dup2/dup3` 对 zvfs fd 当前返回 `ENOTSUP`。
|
||||
- `rename` 跨 `/zvfs` 与非 `/zvfs` 路径返回 `EXDEV`。
|
||||
- `fallocate(FALLOC_FL_PUNCH_HOLE)` 未实现。
|
||||
---
|
||||
|
||||
## 8. 后续建议
|
||||
## 6. 关键工程难点与踩坑复盘(重点)
|
||||
|
||||
- 补齐 mmap 路径(mmap table + 脏页回写)。
|
||||
- 完善多线程/高并发下的语义与压测基线。
|
||||
- 增加版本化 benchmark 报告,避免 README 中历史数据失真。
|
||||
这一节是项目最有价值的部分,记录了从“能跑”到“可用于数据库 workload”过程中遇到的关键问题。
|
||||
|
||||
## 9. Blob Store 血泪教训
|
||||
### 6.1 SPDK 元数据回调线程模型
|
||||
|
||||
### Owner Thread 绑定
|
||||
问题:把 metadata 操作随意派发到任意线程,容易卡住或回调不回来。
|
||||
|
||||
blobstore内部负责并发控制,让所有metadata操作都在一个线程上执行,回调固定绑定给创建blobstore的线程。所以多线程模型下不是send给谁谁就能poll到回调的。
|
||||
根因:
|
||||
|
||||
正确架构:
|
||||
```
|
||||
metadata thread
|
||||
spdk_bs_load()
|
||||
resize
|
||||
delete
|
||||
sync_md
|
||||
- blobstore metadata 操作与创建线程/通道绑定。
|
||||
- `resize/delete/unload` 内部会走 `spdk_for_each_channel()` barrier。
|
||||
|
||||
worker thread
|
||||
blob_io_read
|
||||
blob_io_write
|
||||
```
|
||||
修复策略:
|
||||
|
||||
### spdk_for_each_channel() Barrier
|
||||
某些 metadata 操作非常慢:
|
||||
```
|
||||
resize
|
||||
delete
|
||||
unload
|
||||
snapshot
|
||||
```
|
||||
这些操作内部会调用:spdk_for_each_channel()
|
||||
- 明确 metadata thread 和 io thread 分工。
|
||||
- 保证持有 channel 的线程持续 poll。
|
||||
- 线程退出时严格释放 channel,避免 barrier 永久等待。
|
||||
|
||||
语义:在所有 io_channel 所属线程执行 callback
|
||||
### 6.2 Daemon 卡住(请求已收但流程停滞)
|
||||
|
||||
类似
|
||||
```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。
|
||||
- UDS 流式读取没有完整分帧处理。
|
||||
- 固定小缓冲导致回包序列化失败(`serialize resp failed`)。
|
||||
|
||||
### 超时任务
|
||||
设置超时就免不了超时后回调成功执行,超时后回调仍会触发,存在 UAF 风险
|
||||
修复:
|
||||
|
||||
- 改为连接级接收缓冲,循环读到 `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. 补齐更系统的性能复测(固定硬件、固定参数、全量报告)。
|
||||
|
||||
Reference in New Issue
Block a user