Files
SerpMax/docs/02_STORAGE_HOOK.md
T

527 lines
22 KiB
Markdown
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.
# ProfileMax Storage Hook / StorageFS 开发设计
> 项目:ProfileMax
> 模块:ProfileMax Node Storage Hook
> 目标:为 ProfileMax Node 上所有 Profile 数据提供统一存储入口,统一管理持久化磁盘与 tmpfs 临时存储。
> 原则:IO 优先、低 CPU、不压缩、不依赖节点数据库;所有 Profile 数据访问均经过 Storage Hook。
## 1. 设计目标
Storage Hook 是 ProfileMax Node 上**所有 Profile 存储的统一入口**,不只是 Chromium Profile 主目录的磁盘分配器。Chromium、impersonate-proxy 以及 Profile 相关服务产生的文件,都必须通过 Storage Hook 管理。
Storage Hook 统一提供两类存储:
1. **持久化存储**:多磁盘 StorageFS,保存需要跨进程重启、Profile 重启继续存在的数据。
2. **临时存储**:由 Storage Hook 创建和管理的 tmpfs,承载可丢弃、可重新生成或已由代理层缓存的重复数据。
核心目标:
1. **IO 优先**:读写路径短,减少复制、锁竞争、系统调用和不必要的元数据操作。
2. **低 CPU**:不做压缩/解压,不做内容哈希,不在热路径执行复杂算法。
3. **避免重复缓存**:impersonate-proxy 已缓存的 HTTP 资源,不再让 Chromium 将同一份 HTTP Cache 持久化到磁盘。
4. **减少底层 inode**:按收益选择 Pack,不强制所有文件打包。
5. **统一入口**:所有 Profile 数据,包括 impersonate-proxy 的缓存目录,都由 Storage Hook 分配、挂载和监控。
6. **节点无数据库**:Storage Hook 不连接 PostgreSQL、SQLite、Redis 或 Admin 数据库。
7. **数据语义正确**:对 SQLite、LevelDB 等活跃数据库文件提供正确的锁、同步、重命名和随机读写语义。
不以压缩率、去重率或极限节省磁盘空间作为目标。
## 2. 总体架构
```text
ProfileMax Admin
(唯一配置数据库持有方)
|
Storage Hook 配置下发
|
v
+--------------------------------------------------------------+
| ProfileMax Node |
| |
| Chromium Profile impersonate-proxy Profile Services |
| | | | |
| +-----------------------+--------------------+ |
| | |
| v |
| Storage Hook (统一入口) |
| | |
| +-------------------+-------------------+ |
| | | |
| v v |
| Persistent Storage tmpfs Manager |
| StorageFS / FUSE Linux tmpfs |
| 多磁盘 / Direct / Pack 临时缓存路径 |
| | | |
| v v |
| /mnt/storage-* /run/profilemax/* |
| 持久化 Profile 数据 可丢弃缓存数据 |
+--------------------------------------------------------------+
```
职责边界:
- Admin 数据库只保存每个 Node 的 Storage Hook 配置,不保存文件、目录、inode、Pack 或 Profile 文件映射。
- Node 接收配置后,在本地完成持久化存储、tmpfs 创建、路径挂载、容量控制和恢复。
- Chromium 与 impersonate-proxy 都使用 Storage Hook 提供的路径,不允许各自绕过 Hook 写入未管理目录。
- Storage Hook 不连接 Admin 数据库,也不运行 SQLite 等数据库。
- Node 持久化元数据使用 StorageFS 自己的二进制索引、日志和快照文件;tmpfs 内容不进入持久化元数据恢复流程。
Admin 与 Node 的职责严格分离:
- Admin 数据库只保存节点 Storage Hook 的配置,不保存文件、目录、inode、Pack 或 Profile 文件映射。
- Node 接收配置后在本地执行挂载、分配、读写和恢复。
- Node Storage Hook 不连接 Admin 数据库,也不运行 SQLite 等数据库。
- Node 的本地元数据使用 StorageFS 自己的二进制索引、日志和快照文件,不将其实现成数据库服务。
## 3. Admin 配置模型
Admin 数据库只需要保存每个 Node 的 Storage Hook 配置。配置内容包括:
- 是否启用 Storage Hook。
- 统一存储根目录和元数据目录。
- 多个持久化 Volume 的标识、物理目录、容量上限、分配权重。
- tmpfs 总容量、单 Profile 上限、挂载路径规则及目录路由规则。
- Chromium 缓存目录策略与 impersonate-proxy 缓存目录策略。
- Pack 大小和小文件阈值(如果启用 Pack)。
- IO、刷盘和预留空间等运行参数。
- 配置版本号,用于 Node 热更新和回滚。
示例配置:
```json
{
"enabled": true,
"mount_point": "/var/lib/profilemax/storage",
"metadata_path": "/var/lib/profilemax/storage-meta",
"volumes": [
{
"id": "volume-a",
"path": "/mnt/storage-a/profilemax",
"capacity_gib": 500,
"weight": 1
},
{
"id": "volume-b",
"path": "/mnt/storage-b/profilemax",
"capacity_gib": 1000,
"weight": 2
}
],
"tmpfs": {
"enabled": true,
"root": "/run/profilemax",
"total_size_gib": 16,
"per_profile_limit_mib": 512,
"mode": "per-profile"
},
"routes": {
"chromium_http_cache": "tmpfs",
"chromium_rebuildable_cache": "tmpfs",
"impersonate_proxy_cache": "persistent"
},
"io": {
"pack_enabled": true,
"small_file_threshold": 65536,
"pack_segment_size_mib": 256,
"read_cache_mib": 256,
"write_batch_max_ms": 2,
"sync_policy": "data"
}
}
```
以上为配置结构示例。容量单位统一使用 GiB;持久化 Volume 容量上限是 StorageFS 逻辑分配上限;tmpfs 容量是内存/交换空间预算,必须单独限制,不能与磁盘容量混为一谈。路由规则必须支持按 Profile 相对路径匹配,并由 Node 在启动 Profile 前完成挂载。
以上为配置结构示例。容量单位统一使用 GiB;容量上限是 StorageFS 的逻辑分配上限,不等同于底层文件系统的硬配额。
配置下发流程:
1. Admin 保存配置并递增 config_version。
2. Node 收到配置后校验路径、权限、磁盘可用空间和配置版本。
3. Node 创建缺失目录并初始化 Volume。
4. Storage Hook 应用配置并挂载 StorageFS。
5. Node 上报实际挂载状态、Volume 可用容量和错误信息。
6. 配置变更不得直接删除已有数据;Volume 下线必须先进入迁移/只读流程。
Node 可以在本地保存一份配置快照用于重启恢复,但该文件只是配置缓存,不是数据库,也不作为配置权威来源。
## 4. Node 本地目录布局
建议:
```text
/var/lib/profilemax/
├── storage/ # StorageFS 统一持久化挂载点
└── storage-meta/ # StorageFS 内部元数据
/run/profilemax/
└── profiles/
└── <profile-id>/ # 每个 Profile 的 tmpfs 挂载目录
/mnt/storage-a/profilemax/
├── packs/
└── direct/
/mnt/storage-b/profilemax/
├── packs/
└── direct/
```
要求:
- metadata 与数据 Volume 分离,避免 Pack 写入挤占元数据日志空间。
- Pack 使用预分配或按需扩展的固定上限 Segment。
- Segment 不压缩,文件内容按原始字节存储。
- 不对每个 Profile 创建一套独立的内部数据库。
- tmpfs 由 Storage Hook 统一创建、限额、挂载和清理,不由 Chromium 或代理进程自行创建。
- tmpfs 不得覆盖整个 Profile 根目录,只挂载到明确允许丢弃的子目录。
- Profile 退出时按策略销毁该 Profile 的 tmpfs;重新启动时重新创建空缓存目录。
## 4.1 Profile 路径路由与 tmpfs 策略
Storage Hook 必须维护一份明确的路径路由规则。Profile 根目录和所有子目录仍属于 Storage Hook 的统一命名空间,但不同子目录可以映射到不同后端。
**A. 浏览器 HTTP Cache:tmpfs**
impersonate-proxy 已经缓存 HTTP 响应资源时,Chromium 本地 HTTP Cache 属于重复缓存。浏览器侧对应目录应挂载到 tmpfs,避免重复写入持久化磁盘:
- `Default/Cache/`
- Chromium 实际版本使用的 HTTP Cache 数据目录及其索引文件。
该策略只针对浏览器 HTTP 网络缓存,不代表所有网站存储都可以丢弃。
**B. 可重建缓存:tmpfs**
以下目录通常可重新生成,默认可路由到 tmpfs:
- `Default/Code Cache/`
- `GPUCache/`
- `GrShaderCache/`
- `ShaderCache/`
实际路径应根据 Chromium 版本和启动参数检测,不得依赖单一固定目录名。
**C. 必须持久化的数据**
以下数据默认留在 StorageFS 持久化后端:
- Cookies、Local Storage、IndexedDB。
- Preferences、登录态及 Profile 配置。
- Extension 数据及其状态。
- Service Worker 注册信息和 CacheStorage(除非业务明确允许清空)。
- SQLite/LevelDB 数据目录及其 WAL/SHM 文件。
- 其他无法确认可重建的 Profile 数据。
禁止将整个 `Default/`、整个 `Service Worker/` 或整个 Profile 目录直接挂载到 tmpfs。
**D. impersonate-proxy 缓存**
impersonate-proxy 的缓存目录也必须位于 Storage Hook 命名空间内,不能绕过 Hook 直接写入任意节点目录。默认将代理缓存放在持久化 StorageFS 中,以便跨 Profile/浏览器重启复用;如果代理缓存被明确配置为短期缓存,也允许由 Admin 将该目录路由到 tmpfs。
浏览器侧 HTTP Cache 与代理侧缓存是两份不同的缓存:默认保留代理侧缓存,浏览器侧重复 HTTP Cache 使用 tmpfs。两者不得误配置成同一个目录或互相覆盖。
**E. 挂载生命周期**
1. Node 启动时先挂载 StorageFS。
2. 创建 Profile 时,Storage Hook 创建该 Profile 的 tmpfs 目录并应用路径路由。
3. Chromium 与 impersonate-proxy 启动前,检查所有必需挂载均已就绪。
4. Profile 运行期间,Storage Hook 统计 tmpfs 使用量并执行硬容量限制。
5. Profile 正常停止后销毁 tmpfs;异常退出后由 Node 清理残留挂载。
6. tmpfs 满时只允许缓存写入失败或按明确策略清理可重建缓存,不得影响 Cookies、IndexedDB 等持久化数据。
## 5. 文件存储策略
StorageFS 使用 Hybrid Backend,不强制所有文件都进入 Pack。
### 5.1 Pack Backend:小文件
小文件可合并写入 Pack Segment,以减少底层 inode 数量。
Pack 记录包含:
- object_id
- segment_id
- offset
- length
- generation
- flags
元数据索引负责把虚拟文件映射到 Pack 的 offset/length。
写入流程:
1. 从内存索引查询或创建文件元数据。
2. 在当前 Segment 分配连续空间。
3. 直接写入原始数据,不压缩、不加密、不计算内容哈希。
4. 追加必要的元数据日志。
5. 更新内存索引。
读取流程:
1. 从内存索引获取 segment、offset 和 length。
2. 使用 pread/preadv 读取目标范围。
3. 尽可能使用内核 Page Cache。
4. 不经过解压、反序列化或额外的数据转换。
Pack 适合大量小型、非频繁随机修改的文件。Pack 记录更新采用新位置写入和旧记录回收,不在原位置进行复杂的碎片整理。
### 5.2 Direct Backend:活跃数据库和大文件
以下文件/目录优先使用 Direct Backend:
- SQLite 数据库及其 WAL/SHM 文件。
- LevelDB/RocksDB 等依赖文件锁和随机写入语义的目录。
- 大文件。
- 高频随机更新文件。
- 需要 mmap、fallocate 或严格 POSIX 语义的文件。
Direct Backend 由底层文件系统直接承载,StorageFS 只管理命名空间和路径映射,尽可能通过 FUSE passthrough 或等效机制减少数据面拷贝。
**不得把 SQLite、LevelDB 的活跃文件打包后再用简化接口模拟普通文件。** 如果无法完整满足锁、mmap、fsync、rename 等语义,必须使用 Direct Backend。
### 5.3 默认阈值
初始建议:
- 小于 64 KiB:允许进入 Pack。
- 大于或等于 64 KiB:Direct Backend。
- 数据库文件及其关联目录:强制 Direct Backend。
- 阈值必须可由 Admin 配置,后续通过实测调整。
Pack 不意味着所有小文件都适合打包。高频修改、频繁 truncate 的小文件也应允许强制 Direct。
## 6. IO 与 CPU 优化要求
性能目标是减少热路径开销,而不是增加复杂的存储算法。
### 6.1 禁止进入热路径的操作
- 不压缩、不解压。
- 不做内容哈希或跨 Profile 去重。
- 不进行每次读写的 JSON 序列化。
- 不在每次文件访问时访问 Admin。
- 不在每次 IO 时扫描目录或扫描 Pack。
- 不在读路径执行 GC、全量统计或容量重平衡。
- 不为每个文件启动独立任务或线程。
### 6.2 数据面优化
- 使用 Rust 实现,尽量减少不必要的内存分配和数据复制。
- 使用 FUSE low-level API,避免高层封装带来的额外开销。
- 优先评估 Linux FUSE passthrough,让 Direct Backend 的数据读写尽可能绕过用户态数据复制。
- 对 Pack 使用 pread/pwrite、preadv/pwritev 等位置 IO,避免共享文件游标锁。
- 使用有界 IO 队列和批量提交,避免无限队列导致内存增长。
- 元数据索引常驻内存,启动时通过快照和日志恢复,正常访问不查磁盘索引。
- 利用 Linux Page Cache;不要默认使用 O_DIRECT,避免绕过有效缓存。
- Segment 可预分配,降低写入时的文件扩展和碎片开销。
- 对同一 Segment 的写入采用轻量分配器,避免全局大锁。
### 6.3 元数据优化
- 使用紧凑二进制结构,不使用 JSON 存储运行时元数据。
- 目录项和 inode 元数据采用分层索引。
- inode/object ID 单调分配,避免频繁随机生成。
- 目录项查询优先走内存索引。
- 元数据日志采用 append-only 方式,减少随机更新。
- 周期性生成快照;恢复时加载快照并重放增量日志。
- 元数据日志与数据 Pack 分开存放。
## 7. 元数据与恢复机制
StorageFS 不使用数据库。
本地元数据由以下文件构成:
```text
storage-meta/
├── superblock
├── metadata.snapshot
├── metadata.wal
└── volume.state
```
- superblock:文件系统版本、UUID、配置版本和基础参数。
- metadata.snapshot:inode、目录项和对象映射的紧凑快照。
- metadata.wal:快照之后的增量元数据操作。
- volume.state:各 Volume、Segment 的分配与恢复状态。
要求:
1. WAL 记录具备长度和版本字段,能够识别尾部不完整记录。
2. 启动时先加载快照,再顺序重放 WAL。
3. 崩溃恢复不得依赖 Admin 在线。
4. WAL 达到阈值后生成新快照,再安全截断旧日志。
5. 数据写入与元数据提交必须定义明确的顺序,避免索引指向尚未落盘的数据。
6. 通过 fsync/fdatasync 提供明确的持久化语义,不得用异步写入冒充同步完成。
## 8. 多磁盘分配
Allocator 负责选择 Volume 和 Segment,不参与文件数据读写。
分配原则:
1. 只向已启用、可写且未达到容量上限的 Volume 分配。
2. 根据剩余容量、配置权重和当前 IO 压力选择 Volume。
3. 新文件/新 Pack 优先分配,不在每次读写时重新平衡。
4. 已存在的对象不因权重变化而自动搬迁。
5. 磁盘达到水位时停止新分配并上报 Admin。
6. Volume 故障时不得静默丢弃元数据或把原数据映射到其他 Volume。
初始实现使用容量权重分配即可,不引入复杂的实时全局调度算法。IO 压力指标可先用于监控,后续有实测收益再参与分配决策。
## 9. Profile 命名空间
Profile 在 StorageFS 中表现为普通目录:
```text
/var/lib/profilemax/storage/
├── profiles/
│ ├── <profile-id-1>/
│ ├── <profile-id-2>/
│ └── <profile-id-3>/
└── shared/
```
- Profile 生命周期由 ProfileMax Node 管理。
- Storage Hook 不保存 Profile 业务数据,不建立 profiles 数据库表。
- Profile 目录的 inode、目录项和对象映射属于 StorageFS 内部元数据。
- Profile 删除由 Node 调用 StorageFS 接口完成,删除过程必须支持可恢复状态,避免中途崩溃产生悬挂对象。
- 是否提供 Profile 级容量限制作为后续能力;第一阶段不增加不必要的配额索引和统计热路径。
## 10. POSIX 兼容要求
StorageFS 面向 Chromium Profile,必须重点验证:
- open / close / read / write / pread / pwrite
- mkdir / rmdir / unlink / rename
- stat / fstat / readdir
- truncate / ftruncate
- fsync / fdatasync
- flock / fcntl 文件锁
- mmap / msync
- fallocate
- hard link / symbolic link(按 Chromium 实际需求支持)
SQLite、LevelDB 等目录默认走 Direct Backend。不能完整支持的操作不得静默返回成功;必须返回正确错误,或通过 Direct Backend 提供原生语义。
## 11. 运行与管理接口
Storage Hook 作为 ProfileMax Node 的本地组件运行,不独立提供公网 API。
Node 内部接口建议:
- `Mount(config)`:初始化 Volume、StorageFS 和 tmpfs 路径规则。
- `ReloadConfig(config, version)`:应用新配置。
- `GetStatus()`:返回持久化挂载、tmpfs 挂载和 Volume 状态。
- `GetMetrics()`:返回磁盘 IO、tmpfs 使用量、容量、错误和队列指标。
- `CreateProfile(profileID)`:创建 Profile 持久化目录及对应 tmpfs 挂载。
- `GetProfilePath(profileID, category)`:向 Chromium、impersonate-proxy 返回受管路径。
- `DeleteProfile(profileID)`:停止相关进程后清理 Profile 数据及 tmpfs。
- `Flush()`:按策略刷盘;tmpfs 内容不执行持久化刷盘。
- `Unmount()`:停止接收新 IO,完成必要刷盘后卸载。
Admin 通过 ProfileMax Node 现有控制通道下发配置并读取状态,不让 Storage Hook 自己连接 Admin 数据库。
## 12. 监控指标
至少提供:
- 持久化 Volume 总容量、已分配容量、可分配容量。
- tmpfs 总容量、每 Profile 使用量、峰值和 OOM/ENOSPC 次数。
- Pack Segment 数量和利用率。
- Direct Backend 文件数量与容量。
- 按后端分类的读写吞吐量、IOPS、平均延迟、P95/P99 延迟。
- FUSE 请求数、请求排队时间。
- 元数据缓存命中率。
- WAL 大小、快照耗时和恢复耗时。
- CPU 使用率、RSS、线程数。
- IO 错误、Volume 不可用、容量不足次数。
指标采集必须低频、低开销,不允许每次文件 IO 都进行复杂统计。
## 13. 实施阶段
### Phase 1:存储路由与最小可用版本
- 先建立原生 ext4/XFS 与 tmpfs 基准。
- 实现 Storage Hook 的统一 Profile 路径入口。
- 实现持久化 Direct Backend 和 tmpfs Manager。
- 配置 Chromium HTTP Cache、Code Cache、GPU/Shader Cache 的 tmpfs 路由。
- 将 impersonate-proxy 缓存目录纳入 Storage Hook,默认持久化。
- 验证 Cookies、Local Storage、IndexedDB、Service Worker 等持久化数据不受影响。
- 验证 Chromium 与 impersonate-proxy 都无法绕过 Storage Hook 写入未管理目录。
### Phase 2:Pack Backend
- 通过基准测试确认 inode 压力是否值得引入 Pack。
- 实现小文件 Pack Segment、内存索引、追加写和随机读。
- 实现对象删除标记和后台回收。
- 增加 Direct/Pack/tmpfs 路由规则。
- 对比 Pack、Direct、tmpfs 与原生文件系统的 CPU、IO 延迟和文件数量。
### Phase 3:多 Volume
- 实现容量权重分配。
- 实现 Volume 状态检测和容量水位。
- 实现配置热更新。
- 验证单盘满载、掉盘、重启等异常场景。
### Phase 4:性能优化
- 评估 FUSE passthrough。
- 优化请求批处理、锁竞争和元数据缓存。
- 根据真实 Profile IO 负载调整 Pack 阈值和 Segment 大小。
- 只有基准数据证明有收益时才增加更复杂的缓存或调度策略。
## 14. 性能验收
所有性能测试必须与原生 ext4/XFS 目录进行对照,使用相同磁盘、挂载参数和测试数据。
测试场景:
1. 大量小文件创建、读取、删除。
2. 大量小文件随机读取。
3. 大文件顺序读写。
4. SQLite WAL 模式读写。
5. LevelDB 高频随机读写。
6. Chromium HTTP Cache 写入 tmpfs 后的启动和页面加载表现。
7. impersonate-proxy 缓存命中时,浏览器侧不再产生持久化 HTTP Cache 写入。
8. Cookies、IndexedDB、Service Worker 等持久化数据重启后完整保留。
9. 多 Profile 并发运行时 tmpfs 总量与单 Profile 限额。
10. Profile 正常退出、异常退出后的 tmpfs 清理。
11. 异常终止后的 StorageFS 恢复时间。
核心指标:
- IO 吞吐量。
- IOPS。
- P50/P95/P99 延迟。
- CPU 使用率和 CPU 时间/GB。
- RSS、tmpfs 使用量和元数据内存占用。
- 底层 inode 使用量。
- 浏览器侧重复 HTTP Cache 的持久化写入量(目标为零)。
- 崩溃恢复时间。
性能决策原则:
- 如果 FUSE 在某类 IO 上明显慢于原生文件系统,该类文件必须走 Direct Backend 或 passthrough。
- 如果 Pack 节省 inode 但显著增加 CPU 或 P99 延迟,应提高 Pack 适用门槛,而不是强制打包。
- 不以单纯减少文件数量作为成功标准;IO 性能和 CPU 开销优先。
- 不启用压缩。
## 15. 明确不做的事情
- 不使用节点数据库。
- 不把 Admin 数据库复制到 Node。
- 不做数据压缩或解压。
- 不做跨 Profile 内容去重。
- 不做内容哈希校验热路径。
- 不把所有文件强制放进 Pack。
- 不允许 Chromium 或 impersonate-proxy 绕过 Storage Hook。
- 不把整个 Profile 或未经分类的网站数据目录放进 tmpfs。
- 不在每次 IO 时请求 Admin。
- 不在第一阶段实现复杂的自动数据均衡。
- 不为了抽象而引入独立的存储集群服务。
最终交付应是一个由 ProfileMax Node 管理的本地 Storage Hook:Admin 负责配置,Node 负责执行;所有 Profile 数据统一经由 Storage Hook;持久化数据进入多磁盘 StorageFS,重复 HTTP Cache 和可重建缓存进入受限 tmpfs,impersonate-proxy 缓存也由 Hook 管理;无节点数据库、无压缩,并以 IO 性能和 CPU 效率作为主要优化方向。