# ZVFS

ZVFS 是一个基于 SPDK Blobstore 的用户态文件系统原型，目标是在不改业务代码的前提下，将常见 POSIX 文件 I/O 重定向到用户态高性能存储路径。
核心思想是复用 Linux 文件管理机制（命名空间/目录/元数据），把文件数据平面放到 ZVFS。

- Hook 方式：`LD_PRELOAD`
- 挂载前缀：`/zvfs`
- 架构：多进程 Client + 独立 Daemon + SPDK
- 语义：同步阻塞（请求-响应）

---

## 1. 项目定位

这个项目重点不只是“把 I/O 跑起来”，而是把以下工程问题串起来：

1. 在多线程/多进程应用（RocksDB / PostgreSQL）里做透明接管。
2. 保留 POSIX 语义（open/close/dup/fork/append/sync 等）。
3. 把 SPDK 资源集中在 daemon 管理，避免每进程重复初始化。
4. 在同步阻塞语义下，把协议、并发、错误处理做完整。

---

## 2. 架构设计

![](zvfs架构图.excalidraw.svg)

```text
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.1 透传策略

**控制面复用 Linux，数据面走 ZVFS**。

- 控制面（Linux 负责）
  - 目录/命名空间管理。
  - 文件节点生命周期与权限语义（create/open/close/stat/rename/unlink 等）。
  - 这些操作在 `/zvfs` 下也会真实执行系统调用，ZVFS 不重复实现目录树管理。

- 数据面（ZVFS 负责）
  - 文件内容读写由 blob 承载。
  - `read/write` 的真实数据路径不走 Linux 文件数据面，而走 ZVFS IPC + SPDK。

- 关键绑定方式
  - `create`：真实创建 Linux 文件 + 在 ZVFS 创建 blob + 把 `blob_id` 写入文件 xattr。
  - `open`：真实 `open` Linux 文件 + 读取 xattr 获取 `blob_id` + 在 ZVFS 打开 blob。
  - `write`：写入 blob 成功后，使用 `ftruncate` 同步 Linux 视角 `st_size`。

- 工程收益
  - 直接减少约 50% 的实现工作量。
  - 兼容性更好，数据库可直接复用现有文件组织方式。

### 2.2 分层职责

- Client（`src/hook` + `src/spdk_engine/io_engine.c`）
  - 判断是否 `/zvfs` 路径。
  - 拦截 POSIX API 并发起同步 IPC。
  - 维护最小本地状态（`fd_table/path_cache/inode_table`）。

- Daemon（`src/daemon`）
  - 独占 SPDK 环境与线程。
  - 统一执行 blob create/open/read/write/resize/sync/delete。
  - 统一管理 handle/ref_count。

- 协议层（`src/proto/ipc_proto.*`）
  - 统一头 + per-op body。
  - Request Header：`opcode + payload_len`
  - Response Header：`opcode + status + payload_len`

### 2.3 为什么是同步阻塞 IPC

- 业务侧兼容成本低，最容易对齐 POSIX 语义。
- 调试路径更直接（一个请求对应一个响应）。
- 先解决正确性和语义完整，再考虑异步化。

---

## 3. 功能覆盖（当前）

### 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)"

cd /home/lian/try/zvfs
make -j"$(nproc)"
make test -j"$(nproc)"
```

产物：

- `src/libzvfs.so`
- `src/daemon/zvfs_daemon`
- `tests/bin/*`

### 4.2 启动 daemon

```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
LD_PRELOAD=./src/libzvfs.so ZVFS_TEST_ROOT=/zvfs ./tests/bin/hook_api_test
./tests/bin/ipc_zvfs_test
```

---

## 5. 性能测试

### 5.1 测试目标

- 目标场景：低队列深度下阻塞 I/O 性能。
- 对比对象：`spdk_nvme_perf` 与内核路径（`O_DIRECT`）。

### 5.2 工具与脚本

- RocksDB：`scripts/run_db_bench_zvfs.sh`
- PostgreSQL：`codex/run_pgbench_no_mmap.sh`

建议：

- PostgreSQL 测试时关闭 mmap 路径（shared memory 改为 sysv，避免 mmap 干扰）。

### 5.3 历史结果

> 以下是历史版本结论，用于说明设计方向。

- QD=1 下可达到 `spdk_nvme_perf` 的约 `90%~95%`。
- 相对同机 `O_DIRECT`，顺序写吞吐可有约 `2.2x~2.3x` 提升。
- 非对齐写因 RMW 开销，吞吐明显下降。

---

## 6. 关键工程难点与踩坑复盘（重点）

这一节是项目最有价值的部分，记录了从“能跑”到“可用于数据库 workload”过程中遇到的关键问题。

### 6.1 SPDK 元数据回调线程模型

问题：把 metadata 操作随意派发到任意线程，容易卡住或回调不回来。

根因：

- blobstore metadata 操作与创建线程/通道绑定。
- `resize/delete/unload` 内部会走 `spdk_for_each_channel()` barrier。

修复策略：

- 明确 metadata thread 和 io thread 分工。
- 保证持有 channel 的线程持续 poll。
- 线程退出时严格释放 channel，避免 barrier 永久等待。

### 6.2 Daemon 卡住（请求已收但流程停滞）

现象：请求日志打印到一半后卡住，压测进程阻塞。

根因：

- UDS 流式读取没有完整分帧处理。
- 固定小缓冲导致回包序列化失败（`serialize resp failed`）。

修复：

- 改为连接级接收缓冲，循环读到 `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. 补齐更系统的性能复测（固定硬件、固定参数、全量报告）。
