Compare commits

...
2 Commits
2 changed files with 616 additions and 0 deletions
+526
View File
@@ -0,0 +1,526 @@
# 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 效率作为主要优化方向。
+90
View File
@@ -0,0 +1,90 @@
# xLightpanda 温池切换 + Storage Hook 部署手册(v0.47.0 / xlp 0.10.0)
> 日期:2026-11-03/04。交付:xLightpanda 接管 SERP 浏览器温池 + Storage Hook Phase 1(tmpfs 路由 + 共享 ShaderCache)。
> 代码:SerpMax node v0.47.0(zcode/serpmax-node 已推送);xLightpanda upstream 提交 `4f69ccd2`(0.10.0 显式指纹导入)。
## 1. 交付物
| 产物 | 位置 | 说明 |
|---|---|---|
| lightpanda 二进制(ReleaseFast,glibc 2.36 目标) | `/opt/{serpmax,profilemax}/bin/lightpanda` | 222MB;xlp 0.10.0;**必须用 `-Dtarget=x86_64-linux-gnu.2.36` 构建**(节点 Debian 12 glibc 2.36,宿主 2.39 构建会缺 `GLIBC_2.38/__isoc23_strtoull`) |
| SerpMax node 源码 v0.47.0 | `/opt/{serpmax,profilemax}/src` + package.json | xlightpanda kind / SERP_BROWSER_KIND / storage.hook / /storage/hook 端点 |
## 2. 温池引擎切换(每节点)
```bash
# 1) 二进制与代码
rsync -az upstream/zig-out/bin/lightpanda NODE:/opt/serpmax/bin/lightpanda
rsync -az --delete node/src/ NODE:/opt/serpmax/src/ # 源码内含 node_modules 除外
rsync -az node/package.json NODE:/opt/serpmax/
# 2) node.env 追加(二进制路径必配;SERP_BROWSER_KIND 分两步灰度)
grep -q XLIGHTPANDA_BINARY_PATH /opt/serpmax/node.env || \
echo 'XLIGHTPANDA_BINARY_PATH=/opt/serpmax/bin/lightpanda' >> /opt/serpmax/node.env
# 灰度第二阶段才加:
echo 'SERP_BROWSER_KIND=xlightpanda' >> /opt/serpmax/node.env
# 3) 重启并验证
systemctl restart serpmax-node
curl -s localhost:3100/version # 版本号 0.47.0
tail -f /opt/serpmax/data/logs/* # 观察 browser.started_xlightpanda / serp.pool_warm
```
- **回滚**:node.env 删掉 `SERP_BROWSER_KIND`(或置空)→ `systemctl restart serpmax-node`,温池立即回到 Profile kind(veil/cloak)。Profile 元数据不动。
- 温池入池门槛/搜索逻辑(serp-pool.ts)零改动;媒体拦截 `page.route` 与页内 fetch 在 xLightpanda 上已实测(CDP Fetch domain / Runtime.evaluate)。
- xLightpanda 仅无头运行(无渲染引擎);kind=xlightpanda 的 Profile 请求有头(VNC/WebDesk)会显式报错。
## 3. Storage Hook Phase 1(每节点,默认关闭)
配置:`DATA_DIR/node-settings.json` 合并(注意保留既有键;HZ-DE-245 已有 httpCacheDir/profileDir):
```json
{
"httpCacheDir": "…(保留)",
"profileDir": "…(保留)",
"storageHook": {
"enabled": true,
"tmpfsRoot": "/run/serpmax",
"perProfileCacheSizeMb": 256,
"sharedShaderCache": true,
"sharedShaderSizeMb": 256
}
}
```
- 生效后 veil/cloak 浏览器启动前:Cache/Code Cache/GPUCache/Dawn/Media Cache → 每 Profile 独立 tmpfs(256MB 上限);ShaderCache/GrShaderCache → bind 到 `/run/serpmax/shared/shader` 共享 tmpfs。停机/崩溃/hardReset 自动 lazy umount。
- **验证**:`curl -s localhost:3100/storage/hook`(activeProfiles/mounts/recentErrors);浏览器启动后 `mount | grep <profileDir>`;`du` 确认 Cache 目录不再占磁盘;Cookies/IndexedDB 重启保留(路由清单永不含存储类目录)。
- **回滚**:node-settings 置 `"enabled": false` → 重启。挂载失败(无 root/CAP_SYS_ADMIN)自动降级直写磁盘并记 `storage.hook_no_root` 告警,不影响业务。
- xLightpanda 温池实例无此问题(无渲染无 shader cache;HTTP cache 为单 sqlite 文件),storage.hook 只对 veil/cloak 分支挂载。
## 4. 已验证结论(2026-11-03/04,本机)
1. **种子一致性**:6 seed(10000/42/123456/999999/573891/248166)veil-chromium vs xLightpanda 全字段对比,均只剩同 7 处引擎基底差异(canvas/webgl 像素、rects 布局基底、audio DSP),身份字段逐字段一致(xLightpanda docs/SEED_PARITY.md)。
2. **温池可行性**:google 首页入池门槛(搜索框)、登录门槛 evaluate、页内 fetch+DOMParser、route 媒体拦截、导航回退全部通过;fetch 拿到 enablejs 空壳时 results=0 → serp-pool 原有回退逻辑按设计工作。
3. **Cookie 桥**:注入真实形 Google 会话 cookie(SID/HSID/SSID/SAPISID/NID)导航后全部存活。注意:Google 会对请求中出现的**陌生名字** cookie 主动下发删除 Set-Cookie(fake 名必被清,系 Google 侧行为,真实 Chrome 同样)。
4. **Storage Hook**:unshare 用户命名空间实测 18/18 挂载(含带空格目录)、共享 shader 互通、卸载干净、tmpfs 易失语义、disabled 零动作。
5. **导入连贯性守卫**:cloak UA(151)误配 derivation=veil(内核 153)时启动即失败 `InconsistentUserAgentVersion`——node 侧转换器已按 `origin==="veil" && uaMajor===153` 门控。
## 5. 温池灰度现状与遗留问题(2026-11-04)
**当前舰队状态:OVH-ETH-33 已开 xLightpanda 灰度(0.10.5,`SERP_BROWSER_KIND=xlightpanda`);HZ-DE-245 / OVH-DEDI-KS5B 暂保持 veil/cloak 兜底。三节点 lightpanda 0.10.5 二进制已就位;Storage Hook 三节点全启用。**
### 已突破
- 引擎接入全链路可用:克隆启动、impersonate MITM 链、cookie 桥、入池门槛、平均 RSS 75MB(veil 同池 ~450MB)。
- Google 的 **captcha/sorry 硬门已突破**(0.10.2-0.10.4 头集修复:UA-CH 三件套含引号、navigation accept 全段、accept-encoding 值序)。
- **enablejs/critical-ch 软门已突破(0.10.5,2026-11-04)**:给 impersonate-proxy 加了受控转发头 dump(env 门控 `IMPERSONATE_HEADER_DUMP=<目录>`(+可选 `_FILTER=<url子串>),默认零开销;源码在 /home/bigcode/data/dev/impersonate-proxy,该仓库 deployed-but-uncommitted 状态保持),veil vs xlp 同 Profile 同 IP 同 MITM 抓包对比,锁定门因:
- **Accept-CH client hints 全套缺失**。Google 响应声明 accept-ch/critical-ch 后,Chromium 对同源后续请求自动回带全部高熵提示(arch/bitness/full-version(-list)/model/platform-version/wow64/form-factors/prefers-color-scheme/downlink/rtt/ect);xlp 一概不带,Google critical-ch-check 判客户端撒谎 → enablejs 门壳 → hop2 302 /sorry。
- 次要差异:accept-language 多了 `,en;q=0.8` 第三段(veil/Chromium 实测 `de-DE,de;q=0.9` 无英文兜底)。
- 修复:0.10.5 Client.noteAcceptCh/appendClientHints(Accept-CH 持久化+每跳回带,值取自指纹 ctx 高熵集合)+ accept-language 去兜底。
- 验证:ETH-33 xlp 灰度池 pagefetch `ok=true 9 结果 0.9~1.7s`(与 veil 同形);新鲜实例单测通过。
- **已知的非引擎限制**:同一实例被连续高频手打搜索(≥7 次)后会在 navigate 回退跳吃 /sorry——频率疲劳,veil 同样如此,非引擎门;温池轮换天然规避。
### 灰度操作
- 开:`node.env` 加 `SERP_BROWSER_KIND=xlightpanda` → `systemctl restart serpmax-node`。
- 关:删该行 → 重启,立即回 veil/cloak(Profile 元数据不动)。
- 二进制升级:`upstream/zig-out/bin/lightpanda`(ReleaseFast + `-Dtarget=x86_64-linux-gnu.2.36`)→ 节点 `/opt/{serpmax,profilemax}/bin/lightpanda`;先 `sudo pkill -f "lightpanda [s]erve"`(注意 pkill 模式勿自匹配 ssh 命令行,否则 ssh 会话被杀 exit 255)。
## 6. 后续(不在本轮)
- StorageFS FUSE/Rust/Pack(02_STORAGE_HOOK.md Phase 2+),另行立项。
- xLightpanda localStorage/IndexedDB 落盘(SERP 场景 cookie 足够)。