postgres hook 测试成功

This commit is contained in:
2026-03-13 01:59:05 +00:00
parent a153ca5040
commit 544f532bf5
53 changed files with 5964 additions and 1674 deletions

366
README.md
View File

@@ -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. 架构设计
![](zvfs架构图.excalidraw.svg)
```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_*` syscallPOSIX 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. 补齐更系统的性能复测(固定硬件、固定参数、全量报告)。