Files
SerpMax/docs/02_STORAGE_HOOK.md
T

22 KiB
Raw Blame History

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. 总体架构

                         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 热更新和回滚。

示例配置:

{
    "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 本地目录布局

建议:

/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 不使用数据库。

本地元数据由以下文件构成:

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 中表现为普通目录:

/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 效率作为主要优化方向。