# 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 的 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/ │ ├── / │ ├── / │ └── / └── 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 效率作为主要优化方向。