Compare commits
18
Commits
a2e1f0d923
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1822ae3129 | ||
|
|
07d7f77a8a | ||
|
|
414259a5d7 | ||
|
|
bd5a4532ff | ||
|
|
43978a4023 | ||
|
|
7f477972ba | ||
|
|
5bfa386312 | ||
|
|
593798efe8 | ||
|
|
ef5a3504ec | ||
|
|
b59e4e92a4 | ||
|
|
cb8bb28b18 | ||
|
|
5063100a90 | ||
|
|
220008f5c3 | ||
|
|
cf72a14cd0 | ||
|
|
da6b6cf235 | ||
|
|
91eb61a1a8 | ||
|
|
1be06f7e9e | ||
|
|
0572999bc7 |
@@ -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 效率作为主要优化方向。
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
# 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 显式指纹导入)。
|
||||||
|
> **部署路径勘误(2026-11-04)**:三节点 systemd 服务 WorkingDirectory=/app,实际运行代码在 `/app/src`(bundle 含 /app/node_modules,勿动);
|
||||||
|
> `/opt/{serpmax,profilemax}/src` 是滞留副本。部署 = rsync src → 节点 `/app/src` + package.json → `/app` + 更新日期戳 `/app/version` → restart。
|
||||||
|
|
||||||
|
## 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 足够)。
|
||||||
|
|
||||||
|
## 6.5 typed 搜索调查(2026-11-04 下午,node 0.49.0/0.49.1)
|
||||||
|
|
||||||
|
**用户判断(定稿):go-fetch 打 302/门壳标的 Profile,WebDesk 里 veil 手动输入搜索全畅通 ⇒ 只要 xLightpanda 能像真实浏览器一样打开 google.com 在输入框填词回车,搜索应当没问题;两处部署动作:①`/nodes` 编辑弹窗设置多了加 Tabs 分类;②温池搜索路径保证「粘真实浏览器」。受此推动做的实测链:**
|
||||||
|
|
||||||
|
1. **pagefetch 门壳不再直接采信(0.49.0,1475479)**:`~10k Profile 只绑 3 个上游 IP` 的密度下,pagefetch 类 XHR 拿 0 结果/sorry/verify 概率高,而真浏览器路径畅通 ⇒ 改为仅当 `results>0 且 !sorry&&!verify` 采信,其余一律回退 **fill 搜索词 + Enter** 导航路径(真实浏览器行为)。
|
||||||
|
2. **JS 引导就绪门(0.49.1,55eb63d)**——本地 xLightpanda 复现实锤:
|
||||||
|
- Google 主包(xjs)未初始化完成时填词提交 → **原生 form GET**(抓到 `biw=&bih=` 空、`ie=GB2312`、无 sxsrf)→ Google 判异常流量 **302 /sorry**(title "302 Moved" 半秒转sorry);
|
||||||
|
- 等就绪(`readyState=complete` + `gk 命名空间≥30/联想容器 .UUbT9/隐藏域 biw` 有值,约需 10-15s 本地)之后同样操作 → **自然结果**(带 sxsrf/sca_esv 富参数);
|
||||||
|
- 故 navigate 提交前等待就绪标记(上限 10s,日志 `serp.pool_boot_gate`),温页早已就绪因此实测 `ms≈1-2` 即通过,零延迟副作用。
|
||||||
|
3. **ETH-33 生产复测:typed 就绪后提交,入参富化正确(sei= 等),但 Google 仍 302→/sorry** —— 剩余根因嫌疑二选一:① 每上游 IP 的 trip 计数(池 64 ≈21/IP, Typed 探针集中于少数 Profile);② impersonate MITM 链对**导航型请求**的头序/TLS 会话特征与真 Chromium 型有残余差异(veilWebDesk 同代理畅通是对照组)。**待 MITM 详查** = `IMPERSONATE_HEADER_DUMP` 开窗抓 xlp 导航 vs 规范浏览器导航头序对比(xlp 0.10.5 已留工具)。
|
||||||
|
4. 部署注记:node 0.49.1 三节点 `/app/version=20261104.1300`(ETH/HZ/KS5B 全部 xlightpanda 温池态,版本戳一处更新)。
|
||||||
|
5. admin 1.81.0(144955f):`/nodes` 编辑节点弹窗改 **Tabs 三页(基本信息/存储与缓存/温池与守卫)**;Hook 与守卫面板改为各自「恢复默认」按钮(原「联动恢复默认」取消)。前端已构建,HZ-DE-245 侧 profilemax-admin 已重启生效。
|
||||||
|
|
||||||
|
**排查小径(可复用)**:在服务器上 spawn 一只不走 MITM 的 `lightpanda serve` + `/app/node_modules/playwright-core` 装载 typed 探针,可隔离 MITM 因素做二元定位;直连(无 proxy 无 MITM)typed 探针发现 Google 主包就绪但 Enter 提交**不触发导航**,生产池路径(有 MITM)却能正常提交——驱动/键事件传递在直连实例上的差异也收入待修(独立小问题,非 sorry 主因)。
|
||||||
|
|
||||||
|
### 6.6 typed 决胜局:生产 dump 对照(2026-11-04 下午二)(0.49.1→0.49.2)
|
||||||
|
|
||||||
|
**背景**:用户拿 `24447424 / 2305745 / 65368457` 三只被限 Profile 在 WebDesk(veil)手测**全畅通** ⇒ IP 无恙,差异收敛到 xlp 引擎/MITM 链。用 `impersonate-proxy-dbg`(IMP dump 工具)在生产抓上游可见头,拿到关键证据并各进一环:
|
||||||
|
|
||||||
|
1. **生产 dump 三连发(34221790)**:req0 pagefetch XHR(`sec-fetch-mode:cors`/`accept:*/*`,形正常);**req1 typed 导航 = `sec-fetch-site:none` + 无 Referer + 裸 q 提交**(真 Chrome 表单回车应为 `same-origin` + `Referer:https://www.google.com/` + sxsrf 富化);req2(sei= 重定向链,same-origin+referer 正确)。→ 引擎把「激活型提交」送出了地址栏/CDP 型导航特征,Google 判异常。
|
||||||
|
2. **本地直连 typed 对照(reprobe10/11)**:引导就绪后首轮与结果页二连 typed 均 **organic**(sxsrf/newwindow/sca_esv 富化,10 结果);本地 echo 自建表单提交分类 `same-origin+referer` 正确 ⇒ 引擎直连形态本身良好,问题在生产恢复页态 + MITM 链。
|
||||||
|
3. **0.49.1 进站门实测有效但不够**:boot 门标志(`readyState+gk/ac/biw`)温页 1-2ms 即过(免延迟),但提交后 Typed 每 0 结果 —— gate 抓的是页内引导而非请求指纹。
|
||||||
|
4. **0.49.2(55b594c)「typed 回家门」**:pagefetch 门壳回退时若温页是结果/sorry 页,先 `goto google 首页` 再输入(与用户「新开 google → 输入框填词」路径完全对齐),boot 门上限扩到 15s。部署三节点(版本戳 20261104.1240)。
|
||||||
|
5. **复测仍 0 结果** —— 回家门在位、提交链通,Google 依旧 302→/sorry:剩余根因收敛于 **impersonate MITM 链对 document 导航请求的 HTTP/2/TLS 帧特征(h2fp 的 SETTINGS/伪头序/TLS preset chrome 与 Chrome 146 不匹配)**。同一链路 pagefetch(XHR 类)如常 0.9~1.7s organic,而 document(GET)一击即中 —— Google 对 document GET 的 JS/一致性门更严。**待修专项:Chrome 146 帧级对齐 h2fp(与上述直连 typed 字同步联动验证)。**
|
||||||
|
|
||||||
|
压测余量与守卫机制不受影响;搜流量主体照旧 pagefetch 健康;typed 兜底环节在 h2fp 对齐完成前保持现状。
|
||||||
|
|
||||||
|
### 6.7 内核 TLS 对齐(2026-11-04 下午三,node 0.49.3)
|
||||||
|
|
||||||
|
**用户定案走专项①后,第一手就是对齐上游 TLS。逐项对照 Real Chrome 148 akamai 指纹后确认 h2 帧参数层(SETTINGS/WINDOW_UPDATE/无 PRIORITY/伪头序 m,a,s,p)已逐位一致,剩余差异收敛到 TLS ClientHello;部署态 `preset: chrome` 是 uTLS 静态模板而非真内核。**
|
||||||
|
|
||||||
|
1. **采样器(impersonate-proxy,deployed-but-uncommitted 状态保持)**:`proxy/clienthello.go` 新增浏览器侧 ClientHello 字面采样(env 门控 `IMPERSONATE_CLIENT_HELLO_DUMP=<json 路径>`,MITM 终结前截读解析、成功一次落盘,零开销直通),输出与 node `kernel-hello.json` 同型(ciphers/extensions/curves/versions,另记 ext_bodies)。
|
||||||
|
2. **ETH-33 独立链采样**(不经 node:手工 impersonate 实例 + 手工 veil Chromium 153 `--headless=new` 打 Google):真内核是 **Chromium 153.0.8010.52**(与指纹品牌号 146 是两码事),hello 完整特征:GREASE×2(头 0xωaωa + 尾 compliance-GREASE **0xCA34/51764,恒定,186 字节结构化 payload**)、ALPS 0x44cd、ECH 0xfe0d、`x25519mlkem768`(IANA 4588=0x11EC)。
|
||||||
|
3. **修 dialer 三个兼容点(fp/dialer.go)**:① `0xCA34` 不占 uTLS 动态 GREASE 槽,以 GenericExtension 发结构化体(config 新增 `ext_bodies`,缺省带采样同型骨架)——**实锤**:CA34 发 1 字节短体时 Google 上游直接 `tls: error decoding message`(186 字节真 payload 即过,两端皆证);② hybrid 组映射纠正:0x11EC 是 `x25519mlkem768`(此前误标 kyber768 → utls 发出已废弃的 0x6399);③ `keySharesForCurves` 补 Kyber 分支混合复用。
|
||||||
|
4. **kernel-hello.json 转正 + node 侧下发**:采样体放 `DATA_DIR/impersonate/kernel-hello.json`,node `buildImpersonateYaml` 自动生成 `tls: preset: custom + custom_hello`(含 ext_bodies hex);profile config.yaml 重生成即生效(0.49.3,git 54574fe)。
|
||||||
|
5. 部署:新二进制 + kernel-hello.json + node src → 三节点(/app/version=20261104.1330);ETH 全池 config 已带 `preset: custom`;Google HTTPS 经 custom hello 200(4 连测)。
|
||||||
|
|
||||||
|
### 6.8 typed 决胜第三局:根因是引擎不跟 302(2026-11-04 下午四,node 0.49.4)
|
||||||
|
|
||||||
|
1. **dump 窗口复测(0.49.2 回家门 + 0.49.3 TLS 对齐后)**:typed 提交形态已完全正确(`same-origin`+`document`+`referer`+sxsrf/sca_esv 全富化)——但落点页 `document.title = "302 Moved"`。Google 对 document 导航回 302(sei/sxsrf 令牌轮转),**xlp 显示 302 静态体而非跟随**,轮询环拿不到结果 → results=0、无 sorry(此前所有 typed 0 结果的真正根因)。
|
||||||
|
2. 引擎侧排查:本地复现(跨主机 302/多跳链/playwright 拦截四对照)全部正常跟随 —— 生产差异在 Google 的 sei 跳转语义(出口 SG → google.com 302 富化),未再深挖引擎内因。
|
||||||
|
3. **节点侧绕法(实证有效)**:`+sei` 落点 URL 直接再取即 200 结果页(生产 execute_js 实证)。0.49.4 在 typed 轮询环检测 `302 Moved` 标题 → `page.goto(当前URL)` 手动吃跳转(至多两跳)→ 继续轮询。
|
||||||
|
4. **附:此前"typed 0 结果"探针有一半假阴性** —— `verify492_0`/`tlsfixNNNN` 乱码词 Google 合法返回零结果页;验证探针要用真实查询词。
|
||||||
|
|
||||||
|
### 6.9 goto 缓存重构(2026-11-04,node 0.49.4 / admin 1.82.0,git bc56b63 / 80e56bc)
|
||||||
|
|
||||||
|
**用户定稿:goto 缓存从 admin 库单点改成「节点文件缓存 + admin 共享库」双层,把「节点搜完 → HTML→admin→admin 解析 goto→节点逐条 fetch location」的解析环砍掉。**
|
||||||
|
|
||||||
|
- 键不变:`sha256(title+换行+cite)`(与 admin goto_cache 逐字节同型,两端互读)。
|
||||||
|
- 节点 `core/serp/goto.cache.ts`:搜索拿到结果 HTML 后就地提取(typed/页内 fetch 两路统一 DOMParser;`a[href*="/goto?url="]` 且含 h3,近 4 层找 cite)→ 本地文件缓存(`DATA_DIR/goto-cache/<key>`,内容=location)→ 未命中批量直连 admin(`/api/node/serp/goto-cache`,节点密钥,不经 CF)→ 仍无则同 Profile impersonate 链路并发取 location(8 并发,3xx+Location 绝对地址且非 goto 才采信)→ 双写(本地文件 + 异步回填 admin 库跨节点共享)→ **HTML 就地改写为真实地址再返回**。
|
||||||
|
- admin `POST /api/node/serp/goto-cache`(authorizeSerp 鉴权):`keys` 批量查(脏值=仍为 goto 形状的值不采信)、`entries` 回填(过滤非法形状);先回填后批查,节点上一轮写入本轮即可命中。
|
||||||
|
- admin 侧 `resolveGotoLinks` 无需改动:节点已改写的 HTML 扫到 0 目标,旧解析环(含逐条 fetchViaNode)自然整段跳过。
|
||||||
|
- 单测 `tests/goto.cache.test.ts` 5 例(键同型/写读回环/脏值防护/三变体改写/零命中不动);bun test 195 全过。
|
||||||
|
|
||||||
|
- **端到端实测(0.49.6 部署后)**:typed 手动跟随机制逐次触发(journal `pool_typed_302_follow`),信任态正常的会话 2/3 organic(9/10 结果,11-15s);随后 ETH-33 整机一度过载(15:02 admin UI 侧 reboot),回填后 ETH/HZ 会话出现持续 302+enablejs 壳。
|
||||||
|
- **~~「舰队级 IP 信任态塌陷」结论已撤回(2026-11-04 晚)**:用户用同一批指纹 ID(ETH 13594328/2302407/2302770/2303411/2305556/2305846/2306594/38879162/67196721,HZ 32955298/77526684)走正常指纹浏览器(cloak)手工复测,**全部干净无 302/enablejs**。复探针亦证:cloak 同 Profile 走**同一条 impersonate 链**(uTLS+h2fp)typed GET → 200 organic。门态非 IP 级、非 TLS/h2 级。
|
||||||
|
- **真根因(MITM 头 dump 实证,`IMPERSONATE_HEADER_DUMP=/tmp/hdump3`)**:lightpanda 引擎**自造的 Client Hints 残缺**,Google document 门态对残缺 CH 的 typed 导航下发 302。cloak(真 Chromium 内核生成 CH)干净 vs xlp 池被门,同为 typed(sec-fetch-site:none、无 referer)、同链路:
|
||||||
|
- `sec-ch-ua`:cloak `"Not-A.Brand";v="24",…`(Chrome 标准 grease)vs xlp `"Not/A)Brand";v="8",…`(lightpanda 自造 grease v8);
|
||||||
|
- cloak 有 `sec-ch-ua-full-version-list`(146.0.7680.177)/`sec-ch-ua-model`/`sec-ch-ua-wow64`/`sec-ch-ua-bitness`,**xlp 全缺**;
|
||||||
|
- `downlink`/`rtt`:cloak 0.4/450(真实感)vs xlp 10/50(满分值)。
|
||||||
|
- **修复方向**:在 impersonate-proxy rewriter(或 xlp 启动参数)对 google document 导航**覆写 Client Hints** 为与 UA 一致的完整 CH(grease 用 Not-A.Brand v24、补 full-version-list/model/wow64/bitness),对齐 cloak 实测值;修后用 pinned 搜索(`/api/serp/html/search?node=…&runtime=browser`)A/B 验证 302 消失。
|
||||||
|
- **排查流程规则(用户定稿)**:xlp 调试中出现 302/门态时,**必须先用正常指纹浏览器(同 Profile)复测一次再下结论**——否则会把 xlp 特有的请求层缺陷误判为 IP/外部门态,导致修复方向偏差(本轮实证)。
|
||||||
|
- ETH-33 dump 窗口(调试用)`IMPERSONATE_HEADER_DUMP=/tmp/hdump3` 经 systemd drop-in 仍开着(机器重启 /tmp 已清空重写;**注意 dumpRequest 不建目录,重启后需 `mkdir -p /tmp/hdump3`**);排查关闭:`rm /etc/systemd/system/serpmax-node.service.d/dump.conf && systemctl daemon-reload && systemctl restart serpmax-node`。
|
||||||
|
|
||||||
|
### 6.10 引擎侧修复落地:xLightpanda 0.10.6(Google typed-document CH 门,2026-11-04 晚)
|
||||||
|
|
||||||
|
**用户定案:0.10.6 不走 MITM 覆写(impersonate-proxy 维持 deployed-but-uncommitted 不动),直接在 xLightpanda 引擎自体把 UA-CH 修真。修复内容详录见 xLightpanda `docs/CH_GATE_FIX.md`(§1-§4 全量),要点:**
|
||||||
|
|
||||||
|
1. **GREASE 与品牌顺序改为 major 纯函数**(providers/Navigator.zig):移植 Chromium `GetGreasedUserAgentBrandVersion` 原算法 —— `Not{c}A{c}Brand`(11 字符表)+ `{8,99,24}[major%3]` + 顺序表 `order_table[major%6]`(6 个公开实测 major 交叉锚定)。146 → `Not-A.Brand`/`24` 顺序 [Chromium,grease,Chrome](与 cloak-146 200 样本全同);153 → `Not_A Brand`/`8` 顺序 [Google Chrome,grease,Chromium](ETH-33 抓包同)。0.10.4 以前按 module_seed 随机 5×3 组合发出真实 Chrome 从未发过的组合,门由此识别。
|
||||||
|
2. **高熵提示默认回带**(HttpClient appendClientHints):已声明源仍按 Accept-CH 声明回带(Set-Persist 语义);未声明可信源在指纹模式按 cloak-200 样本字段集(11 头,无 ect)回带默认全集 —— 解 xlp 冷启动永远收不到声明的鸡生蛋。裸启动(无指纹 seed)不回带。
|
||||||
|
3. **downlink/rtt 档位化**:9 档真实采样(10/50…0.4/450),network 模块 seed 派生、实例内稳定;HTTP 头与 JS `navigator.connection` 同一 NetworkQuality(禁漂移),替代全舰队 10/50 满分值。
|
||||||
|
4. **平台版本与真实构建号**:Persona.platformVersion linux→"6.11.0"/macos→"26.2.0";导入 UA(cloak/veil 快照)四段真实构建号(`Chrome/153.0.8010.52`)提取进 full-version 与品牌 full-version-list —— 真实 Chrome 从不发 `{major}.0.0.0`。
|
||||||
|
5. **生产验证(ETH-33,dump 窗开启)**:0.10.6 部署重启后 pinned 浏览器搜索 11.8s/1 attempt → **无 gate 头、475KB 真结果页**,xlp typed GET(sec-fetch-site:none)含 `downlink/rtt/prefers-color-scheme/arch/form-factors/platform-version/full-version(153.0.8010.52)` 7 头,与 cloak-146 200 样本逐字段结构一致(GREASE 对 + 顺序按 153 算法,real build 走导入 UA 提取);后续连发两查询进 `/sorry`(每 IP 会话配额,§7.2 已存现象,与 CH 门无关 —— sorry URL 带 sca_esv/sxsrf,doc-gate 302+enablejs 未再复现)。dump 数据存 `/tmp/hdump3.20261104.bak`(dumpRequest 不建目录)。**部署:ETH-33 + HZ-DE-245(备份 lightpanda.0.10.5.bak);FR/KS5B 节点仅跑 proxy 基建,无 xlp 池,不在本轮。**举一反六实现全量 `make test` 通过,版本 0.10.6(package.json),engine 仓库 xLightpanda 未提交待验证定稿后推送。
|
||||||
|
|
||||||
|
### 6.11 指纹自定义/导入第一步:真实构建号全链路(2026-11-04 深夜,node 0.49.7 / xlp 0.10.7,git 130f808 / 90e758ef7)
|
||||||
|
|
||||||
|
**用户定稿三步走(自定义 persona + veil/cloak 快照导入 + 温池代班)的第一步:把构建号列入 persona/导入 schema,消掉 0.10.6 已知边界「纯派生 profile full-version 发 {major}.0.0.0」。**
|
||||||
|
|
||||||
|
1. **xlp 0.10.7 漂移修正(实为 0.10.6 缺陷)**:derive 路径的 `sec-ch-ua-full-version-list` 与 JS `js_full_version_brands` 在导入 UA 覆写块**之前**物化 → HTTP 头与 JS fullVersionList 仍发 `{major}.0.0.0`,只有单值 full-version 拿到真实构建号。0.10.7 把两处物化移到全部覆写之后(回归测试入档)。
|
||||||
|
2. **xlp 0.10.7 `overrides.uaFullVersion`**:四段全数字构建号显式钉定(自定义 persona 无导入 UA 用),显式 wins 于导入 UA 提取;仅 `derivation=xlightpanda` 生效。Profile.zig 校验 ≥4 段全数字。
|
||||||
|
3. **node 0.49.7**:`generateFingerprint` 快照 UA 直接带真实四段构建号(cloak wrapper 五段截前四);`xlightpanda.launcher` 对存量 `.0.0.0` 快照 UA 用 `extra.chromeVersion` 就地改写(存量 Profile 免迁移),**major 取改写后 UA**(否则 persona 钉错被 xlp Validator 拒启动)。
|
||||||
|
4. 验证:xlp `make test` 全绿;node `bun test` 197 全过(新增 xlightpanda.launcher.test 2 例 + veil.test 2 例改期望)。部署与生产 A/B 见 §6.10 同款流程(pinned 搜索 + dump 窗),验证项:typed GET 的 UA 构建号段、full-version、full-version-list 三处同为真实构建号。
|
||||||
|
5. **node 0.49.8(f380615)—— 自定义 persona 生成 + veil/cloak 快照导入面(三步走第二步的节点 API 半)**:
|
||||||
|
- `generateFingerprint` 自定义 persona:`fingerprint.userAgent/webgl/screen` 显式参数优先;**声明版本随 UA 构建号段归一**(UA ↔ extra.chromeVersion ↔ impersonate TLS 档案三者一致,身份锚 = 页面 UA;仅主版本 UA 补全四段)。
|
||||||
|
- `importFingerprintSnapshot`:`createProfile` 请求 `fingerprint.snapshot` 收编一份完整 FingerprintSnapshot 形状 JSON(本系统导出格式,veilbrowser/cloakbrowser 出身通用)—— seed/UA/screen 原样收编**身份不重抽**,id 重生成、出身保留、importedAt 盖章、platformKey/chromeVersion 缺省从 platform/UA 补全。导入优先级 snapshot > admin import > 生成器。
|
||||||
|
- bun test 204 全过(新增 fingerprint.custom.test 7 例);两节点已随 0.49.7 同款 rsync 部署。
|
||||||
|
- **0.49.9 + admin 1.83.0(919b98a)—— admin 接线完成,导入面 API 全链路可测**:
|
||||||
|
- node `importAdminBrowserFingerprint` 顶层 seed 形状自适应:veil/cloak 快照自动走快照导入(身份不重抽),admin BrowserFingerprint 形状不变 —— **admin relay 零改造**。
|
||||||
|
- admin `POST /api/profiles` 新增 `fingerprintSnapshot`(须含 seed/userAgent):原样收编为 Profile browser 指纹(跳过随机生成),补 version(供 TLS 档案)/languages 派生键;`BROWSER_KINDS` 对齐 node 加 `xlightpanda`。
|
||||||
|
- **端到端实测**:admin API 建号(seed 31415926/UA 真实构建号原样)✓;node 直连导入建号 → xlp spawn 真实构建号 UA → google 导航成功 ✓;0.10.7 full-version-list 真实构建号生产 dump 实证 ✓。测试产物已清理,dump 窗已关。
|
||||||
|
- 待办:admin 前端 UI 入口(自定义 Tab + 快照上传);温池代班批量验证 = 三步走第三步(需 Profile 带 task_state=serp + 代理入池)。
|
||||||
|
6. **0.10.7 部署与 dump 复核(ETH-33)**:二进制(90e758ef7)两节点部署,launcher 真实构建号改写已实证生效(新 spawn 的 profile.json:`Chrome/146.0.7680.177` + major 146、`Chrome/153.0.8010.52` + major 153)。dump 复核 typed GET:`sec-ch-ua-full-version 153.0.8010.52`、GREASE/顺序 153 算法正确、档位 10/150(seed 派生)。
|
||||||
|
- **重要校准认知(免误判)**:dump 中 UA 串 `Chrome/153.0.0.0` vs full-version `153.0.8010.52` 并非缺陷 —— veil_compat 0.9.0 期对真实 veil 内核(fingerprint-chromium)的标定即此形态:**UA 串构建号段缩减({major}.0.0.0),fullVersionList 带完整构建号**(kernel_version vs kernel_full_version 双常量即为此设计)。真实内核同款行为,Google 门已实证放行(§6.10 round1)。曾计划 0.10.8 在 veil 路径跳过占位 UA 覆写,复核标定后确认无行为差异,已撤销(树保持 0.10.7)。
|
||||||
|
- full-version-list 头的 dump 复核(0.10.7 漂移修正在 HTTP 侧的实证)待 /sorry 冷却后下轮搜索补验;代码路径已有回归测试(0.10.7 提交)。
|
||||||
|
|
||||||
|
## 7. 极限压测 + 全节点温池化(2026-11-04,node 0.48.0 / admin 1.80.0)
|
||||||
|
|
||||||
|
**用户定稿:ETH-33 旧 veil 池架构经常死机(实测 10-24~11-03 五崩四次,11-03 xlp 灰度后稳定);测新架构极限,系统资源保留 20% 保证不死机;全部节点切温池,全部 SERP 请求走温池路径。**
|
||||||
|
|
||||||
|
### 7.1 节点资源守卫(node 0.48.0,git f50a13f)
|
||||||
|
- `src/core/system/resource.guard.ts`:可用内存(MemTotal-MemAvailable)< 20% 或 load1/核心数 > 80% ⇒ 拒绝**新预热**(SERP_POOL_FULL 语义,admin 补池 15s 重试自愈);池内 ready/busy 与进行中搜索不动。配置 `node-settings.json` 的 `resourceGuard` 键(admin /nodes 可视化编辑,1.80.0)+ `RESOURCE_GUARD_ENABLED=0` 逃生口;判定用 load1(load5 衰减太慢会压着守卫十几分钟)。失败读数不阻塞(降级语义)。
|
||||||
|
- /serp/pool 响应即带 guard 水位;metrics 新增 mem_free_pct / system_load_pct / serp_pool_guard_rejections_total;`resource.guard.test.ts` 5 用例(bun test 190 全过)。
|
||||||
|
- xLightpanda 端口扫描数可配 `XLIGHTPANDA_PORT_COUNT`(默认 64 → 压测时可放更深)。
|
||||||
|
|
||||||
|
### 7.2 ETH-33 极限压测数据(12 核 / 64G,守卫版压测)
|
||||||
|
| 档位 | 池态 | lightpanda 总 RSS(≈单实例) | 内存可用 | 稳态 load1 | 结论 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 64 | 64/64 ready | 6.1G(95MB) | 82-89% | ~0.2(闲) | 资源余量极大 |
|
||||||
|
| 96 | 96/96 ready | 8.6G(88MB) | 85.6% | ~0.2(闲) | 同上 |
|
||||||
|
| 128 | 128/128 ready | 11.7G(91MB) | 82% | ~0.04(闲) | 内存仍远未到保留线 |
|
||||||
|
|
||||||
|
- **机器资源极限远未到**:128 深池内存只占 18%,闲时 CPU≈0;负载尖峰(ll 12-19)全部来自共租任务(保活 veil 浏览器 churn、重启风暴),守卫在尖峰窗口正确拒绝增容、尖峰过后(分钟级)自动恢复补池——“保留 20% 保证不死机”机制实测有效。
|
||||||
|
- **真正的瓶颈是 Google 每 IP 会话配额**:ETH-33 全部 ~10k Profile 只绑 3 个上游代理 IP。池深 20(≈7 会话/IP)时搜索 0.9-1.7s 正常;128 深突发(43 会话/IP + 6 分钟 128 连发)后单实例首搜即 302→/sorry,5 分钟未恢复。**操作定值:ETH-33 池收在 64(≈21/IP)**;曾冲到 128 的会话密度待 IP 冷却后另行评估。
|
||||||
|
- 压测期间 admin 补池 15s×4/轮,自 0 填满 128 仅 ~7 分钟(重启风暴窗口守卫会自动压住几分钟后恢复)。
|
||||||
|
|
||||||
|
### 7.3 全节点温池化(2026-11-04 16:xx 前)
|
||||||
|
| 节点 | SERP_POOL_SIZE | admin override | 状态 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| OVH-ETH-33 | 64 | 64 | xlightpanda,64/64 回填完成 |
|
||||||
|
| HZ-DE-245 | 50 | 50 | xlightpanda(原 veil 兜底),回填中 |
|
||||||
|
| OVH-DEDI-KS5B | 80 | 80 | xlightpanda(原 veil 兜底),回填中 |
|
||||||
|
- 服务名:ETH/HZ=serpmax-node(/opt/serpmax),KS5B=profilemax-node(/opt/profilemax);统一 WorkingDirectory=/app;三节点 /app/version=20261104.1300(/system/version 显示之)。
|
||||||
|
- admin serpConfig `fetchPerSec: 1→0`:令牌桶永不放行 go-fetch,**SERP 请求 100% 走浏览器温池**(go-fetch 代码保留;回滚 = 改回正数即可)。依据:错误请求集中于 go-fetch 直连路径,温池路径无此问题(用户 2026-11-04 定稿)。
|
||||||
|
- Storage Hook 三节点保持 enabled(合并写修复后 /system/dirs 保存不再覆盖 storageHook 块);资源守卫全部默认开启(20/20)。
|
||||||
|
- 回滚:节点删 node.env 的 SERP_BROWSER_KIND 行重启即回 veil/cloak 温池。
|
||||||
|
|
||||||
|
### 7.4 admin 1.80.0(git 07f696c)
|
||||||
|
- /api/nodes/:name/settings(中继节点 GET /system/dirs)+ PUT /api/nodes/:name 支持 storageHook/resourceGuard(undefined=不动/null=恢复默认/对象=合并下发)。
|
||||||
|
- /nodes 编辑弹窗新增「Storage Hook(缓存路由 tmpfs)」与「资源守卫(温池准入)」设置区,打开即回显节点现值;节点 /system/dirs 增加 GET,保存按顶层键合并(修复只改目录导致手配 storageHook 被抿掉的隐患)。前端已构建,profilemax-admin 已重启。
|
||||||
|
(注:守卫与 Hook 值均为节点侧即读即生效,保存无需重启。)
|
||||||
@@ -6,14 +6,16 @@
|
|||||||
* 双路径原形态(runtime=auto,无 sticky,全池公平负载选号):
|
* 双路径原形态(runtime=auto,无 sticky,全池公平负载选号):
|
||||||
* GET /api/serp/json/search?q=...&num=10
|
* GET /api/serp/json/search?q=...&num=10
|
||||||
* 令牌内走 go-fetch(全局限流 fetchPerSec),限流外并发走温池浏览器 —— 组合即生产形态。
|
* 令牌内走 go-fetch(全局限流 fetchPerSec),限流外并发走温池浏览器 —— 组合即生产形态。
|
||||||
* AIMD 控制器 v5(dp6 全天实测教训驱动;v4 基础上改四点):
|
* AIMD 控制器 v5.4(dp6 全天 + dp7/dp7b/dp7c/dp7d 四轮前段实测定稿;核心=故障二分):
|
||||||
* 硬约束(成功率即约束本身):首次跌破 99%(失败≥2)×0.7、<96% ×0.5;退化段内续砍
|
* Google 侧(captcha/门壳/空)→ 零砍速,波次骑行:四轮实测一致证明砍速在任何速率下
|
||||||
* 需短窗 <96% 且全程增量新失败 ≥2 —— dp5 教训:同一波旧失败被短窗反复引用,
|
* 都让占比恶化(8/min 时 148/时波次占比 10.5% 窗口 88%;117/min 4.2%;300/min 1.6%
|
||||||
* 五分钟连砍五刀 175→30/min,直接打灭浏览器溢出路径;
|
* 窗口 98.4%)—— 唯一出路是分母,宁骑不砍;
|
||||||
* 软信号按事件段(episode)迟滞 + 绝对数护栏(低速率下 1~2 个噪音 = 高比率,不得触发):
|
* 我方故障(httpErr/netErr;poolEmpty 仅低速 <60/min 计入,高速期池空是需求伴生现象)
|
||||||
* 验证码 10min 窗 ≥3 个且占比>2% ×0.5、≥3 个且>0.5% ×0.7;门壳 ≥3 且占比>15% ×0.85;
|
* → AIMD:入段(浅破线 <99% 失败 ≥8 且窗口 ≥200 样本 / 深破线 <96% 失败 ≥4)×0.7/
|
||||||
* 池空>5 ×0.85;软砍下限 SOFT_FLOOR=25 —— 背景验证码供给恒定,速率越低比率越高,
|
* ×0.5,续砍需短窗 <96% 且增量新失败 ≥2,地板 HARD_FLOOR=8,低速池空软砍 ×0.85;
|
||||||
* 比率规则在低位永真(dp5 实测 12:00-17:00 自 52/min 自残至 3/min);
|
* 噪声预算退段/爬坡(稀释论):零我方故障且联合噪声(captcha+门壳)≤6 → 退段(短窗
|
||||||
|
* ≥96%)/≤8 → 可爬坡(短窗 ≥96%),水位下 ×1.3/拍,之上 ×1.08/5 拍;
|
||||||
|
* workers 默认 24(页内 fetch 1.1~2s/请求,12 worker 天花板 ~135/min 是波次稀释瓶颈)。
|
||||||
* 干净爬坡:不在退化段 且 短窗干净 且 无池空 且(验证码 0 个或样本 ≥300)→ 向「干净
|
* 干净爬坡:不在退化段 且 短窗干净 且 无池空 且(验证码 0 个或样本 ≥300)→ 向「干净
|
||||||
* 水位」(30min 窗 ≥99.5% 时的最高速率)以 ×1.3/拍快速回收,水位之上每 5 拍 ×1.08。
|
* 水位」(30min 窗 ≥99.5% 时的最高速率)以 ×1.3/拍快速回收,水位之上每 5 拍 ×1.08。
|
||||||
* v5 修 dp6 实测「低速恢复锁死」:短窗成功率 ≥99.5% 在低速涓流下永不可达
|
* v5 修 dp6 实测「低速恢复锁死」:短窗成功率 ≥99.5% 在低速涓流下永不可达
|
||||||
@@ -42,8 +44,9 @@ const RATE0 = Number(argOf("--rate0", "15"));
|
|||||||
/** # 成员变量 速率下限/上限(req/min) **/
|
/** # 成员变量 速率下限/上限(req/min) **/
|
||||||
const RATE_MIN = Number(argOf("--min", "3"));
|
const RATE_MIN = Number(argOf("--min", "3"));
|
||||||
const RATE_MAX = Number(argOf("--max", "400"));
|
const RATE_MAX = Number(argOf("--max", "400"));
|
||||||
/** # 成员变量 并发 worker 数 **/
|
/** # 成员变量 并发 worker 数(v5.4:默认 12→24 —— 页内 fetch 后浏览器单请求 1.1~2s,
|
||||||
const WORKERS = Number(argOf("--workers", "12"));
|
* 12 worker 的吞吐天花板(~135/min 实测)成了灾难波次稀释(需 ≥250/min)的瓶颈) **/
|
||||||
|
const WORKERS = Number(argOf("--workers", "24"));
|
||||||
/** # 成员变量 控制器窗口(分钟)与加速步间隔(分钟) **/
|
/** # 成员变量 控制器窗口(分钟)与加速步间隔(分钟) **/
|
||||||
const WINDOW_MIN = 30;
|
const WINDOW_MIN = 30;
|
||||||
const RAMP_EVERY_MIN = 5;
|
const RAMP_EVERY_MIN = 5;
|
||||||
@@ -148,8 +151,10 @@ let degraded = false;
|
|||||||
let failsAtLastCut = 0;
|
let failsAtLastCut = 0;
|
||||||
/** # 成员变量 干净水位:30min 窗 ≥99.5% 且短窗 ≥99% 时达到过的最高速率(深跌后向水位快速回收;dp5 教训:×1.08/5 拍从 3/min 爬回需 3 小时,温池 150 ready 全程空转) **/
|
/** # 成员变量 干净水位:30min 窗 ≥99.5% 且短窗 ≥99% 时达到过的最高速率(深跌后向水位快速回收;dp5 教训:×1.08/5 拍从 3/min 爬回需 3 小时,温池 150 ready 全程空转) **/
|
||||||
let maxCleanRate = 0;
|
let maxCleanRate = 0;
|
||||||
/** # 成员变量 软砍速率下限(req/min):背景验证码供给恒定,速率越低比率越高,比率规则在低位永真 → 钉死地板(dp5 实测 12:00-17:00 自 52/min 自残至 3/min)。软规则不砍破此位;真退化由硬规则(无下限)兜底 **/
|
/** # 成员变量 软砍速率下限(req/min):背景验证码供给恒定,速率越低比率越高,比率规则在低位永真 → 钉死地板(dp5 实测 12:00-17:00 自 52/min 自残至 3/min)。软规则不砍破此位。v5.1 上调 40:99% 目标下 25/min 时涓流(~14 个/时)占比已达 0.93% 贴线,40/min 摊薄到 0.58% 留出安全余量(稀释论) **/
|
||||||
const SOFT_FLOOR = 25;
|
const SOFT_FLOOR = 40;
|
||||||
|
/** # 成员变量 硬砍速率下限(req/min;v5.2 新增):dp7 实测 99% 线下小样本噪音(4 个失败)即可驱动 ×0.5 级联到 3/min —— 硬规则也设地板,真退化到 8/min 已足够压缩暴露量,3/min 只会恶化涓流占比(稀释论) **/
|
||||||
|
const HARD_FLOOR = 8;
|
||||||
|
|
||||||
/** # 函数
|
/** # 函数
|
||||||
* 控制器决策(每 60s 调用):按窗口成功率/内部压力调整速率,返回决策记录或 null。
|
* 控制器决策(每 60s 调用):按窗口成功率/内部压力调整速率,返回决策记录或 null。
|
||||||
@@ -170,48 +175,57 @@ function controlTick(): Record<string, unknown> | null {
|
|||||||
const from = rate;
|
const from = rate;
|
||||||
let to = from;
|
let to = from;
|
||||||
let reason = "hold";
|
let reason = "hold";
|
||||||
// 硬约束:首次跌破 99% 即砍;段内续砍需「进行中」证据 —— 短窗 <96%(比入段门槛更强的
|
// 故障二分(v5.4,dp7d 实测定稿:148/时灾难波次下,poolEmpty 3~4 个(高需求伴生)误判
|
||||||
// 活跃失败信号,波尾噪声不算)且全程增量新失败 ≥2(高速率下 2 个新失败转瞬即攒,单靠
|
// 我方故障解锁硬地板 8 → 砸到 8/min 窗口 88%(captcha 占比 10.5%);而同一波次在
|
||||||
// 增量计数仍会逐分钟级联,dp5 教训:10:24-10:28 五分钟连砍五刀 175→30/min 打灭浏览器溢出)
|
// 117/min 为 95.8%、300/min 为 98.4% —— Google 侧事件引发砍速在任何速率下都是毒药):
|
||||||
|
// infraFails = 纯我方故障(httpErr/netErr;poolEmpty 仅低速 <60/min 时计入 —— 高速期
|
||||||
|
// 池空是需求压力的伴生现象,温池自补,砍速只会加重浏览器饿死);
|
||||||
|
// Google 侧(captcha/门壳/空)→ 零砍速,波次骑行:当前速率下占比已是最优解,
|
||||||
|
// 砍速减分母提占比、提速摊薄降占比,唯一出路在分母;
|
||||||
|
// ① 退段:窗口 ≥99%,或零我方故障且联合噪声 ≤6 且短窗 ≥96%;
|
||||||
|
// ② 入段/续砍:仅我方故障 ≥3 且(浅破线 <99% 失败 ≥8 窗口 ≥200 样本 / 深破线 <96%
|
||||||
|
// 失败 ≥4 / 短窗 <96% 增量新失败 ≥2),地板 8;
|
||||||
|
// ③ 爬坡:零我方故障且联合噪声 ≤8 且短窗 ≥96%(水位下 ×1.3/拍,之上 ×1.08/5 拍)
|
||||||
const fails = tot - wH.ok;
|
const fails = tot - wH.ok;
|
||||||
const allFails = totals.sent - totals.ok;
|
const allFails = totals.sent - totals.ok;
|
||||||
const successS = wS.sent > 0 ? wS.ok / wS.sent : 1;
|
const successS = wS.sent > 0 ? wS.ok / wS.sent : 1;
|
||||||
if (success >= 0.99) degraded = false;
|
const infraFails = wH.httpErr + wH.netErr + (from < 60 ? wH.poolEmpty : 0);
|
||||||
const entry = !degraded && success < 0.99 && fails >= 2;
|
const googleNoiseS = wS.captcha + wS.gate;
|
||||||
const ongoing = degraded && wS.sent >= 10 && successS < 0.96 && allFails - failsAtLastCut >= 2;
|
// v5.5:入段/续砍用 10min 短窗「现役」我方故障证据 —— dp7f 实测:30min 窗里 6 个
|
||||||
|
// 零星 httpErr 在 420/时 captcha 灾难波次下持续解锁硬规则,级联到 8/min;现役证据
|
||||||
|
// 才配得上「正在发生的我方故障」。混合证据(联合噪声 >8)时地板抬到 SOFT_FLOOR,
|
||||||
|
// 杜绝 dp7d 式「2 个伴生池空解锁硬地板 8」
|
||||||
|
const infraFailsS = wS.httpErr + wS.netErr + (from < 60 ? wS.poolEmpty : 0);
|
||||||
|
const trickle = infraFails === 0 && googleNoiseS <= 6;
|
||||||
|
if (success >= 0.99 || (trickle && successS >= 0.96)) degraded = false;
|
||||||
|
const entry = !degraded && !trickle && infraFailsS >= 2 && ((success < 0.99 && fails >= 8 && tot >= 200) || (success < 0.96 && fails >= 4));
|
||||||
|
const ongoing = degraded && wS.sent >= 30 && successS < 0.96 && infraFailsS >= 2 && allFails - failsAtLastCut >= 2;
|
||||||
if (entry || ongoing) {
|
if (entry || ongoing) {
|
||||||
degraded = true;
|
degraded = true;
|
||||||
failsAtLastCut = allFails;
|
failsAtLastCut = allFails;
|
||||||
to = success < 0.96 ? from * 0.5 : from * 0.7;
|
to = success < 0.96 ? from * 0.5 : from * 0.7;
|
||||||
|
to = Math.max(googleNoiseS > 8 ? SOFT_FLOOR : HARD_FLOOR, to);
|
||||||
reason = `success=${(success * 100).toFixed(1)}%<${success < 0.96 ? 96 : 99}%(失败 ${fails}${entry ? "" : ";短窗<96%+新失败≥2"}) ×${success < 0.96 ? 0.5 : 0.7}`;
|
reason = `success=${(success * 100).toFixed(1)}%<${success < 0.96 ? 96 : 99}%(失败 ${fails}${entry ? "" : ";短窗<96%+新失败≥2"}) ×${success < 0.96 ? 0.5 : 0.7}`;
|
||||||
} else {
|
} else {
|
||||||
// 软信号:事件段迟滞 + 绝对数护栏(低速率下 1~2 个噪音即高比率,不得触发;dp5 教训)
|
// 软信号(v5.4:仅剩池空,且仅低速期 —— 高速期池空是需求伴生现象,温池自补,
|
||||||
if (wS.captcha >= 3 && capPctS > 0.005 && !captchaWave) {
|
// 砍速只会加重浏览器饿死;captcha/门壳软砍废除,灾难波次骑行,dp7 四轮实测)
|
||||||
captchaWave = true;
|
if (from < 100 && wS.poolEmpty > 5 && !poolEmptyWave) {
|
||||||
const heavy = wS.captcha >= 5 && capPctS > 0.02;
|
|
||||||
to = Math.max(SOFT_FLOOR, from * (heavy ? 0.5 : 0.7));
|
|
||||||
reason = `验证码 ${wS.captcha} 个占比 ${(capPctS * 100).toFixed(2)}% ${heavy ? "×0.5" : "×0.7"}`;
|
|
||||||
} else if (wS.captcha === 0) captchaWave = false;
|
|
||||||
if (to === from && wS.gate >= 3 && gateRatioS > 0.15 && !gateWave) {
|
|
||||||
gateWave = true;
|
|
||||||
to = Math.max(SOFT_FLOOR, from * 0.85);
|
|
||||||
reason = `门壳 ${wS.gate} 占比 ${(gateRatioS * 100).toFixed(1)}%>15% ×0.85`;
|
|
||||||
} else if (gateRatioS < 0.05) gateWave = false;
|
|
||||||
if (to === from && wS.poolEmpty > 5 && !poolEmptyWave) {
|
|
||||||
poolEmptyWave = true;
|
poolEmptyWave = true;
|
||||||
to = Math.max(SOFT_FLOOR, from * 0.85);
|
to = Math.max(SOFT_FLOOR, from * 0.85);
|
||||||
reason = `池空 ${wS.poolEmpty} ×0.85`;
|
reason = `池空 ${wS.poolEmpty} ×0.85`;
|
||||||
} else if (wS.poolEmpty === 0) poolEmptyWave = false;
|
} else if (wS.poolEmpty === 0) poolEmptyWave = false;
|
||||||
}
|
}
|
||||||
// 干净水位记账:30min 窗 ≥99.5% 且短窗 ≥99% 时的速率即为「实测可行」
|
// 干净水位记账(v5.2:门槛 99.5%→99%,99% 线下 ≥99.5% 的 30min 窗在涓流时段不可达,
|
||||||
if (success >= 0.995 && successS >= 0.99 && wS.sent >= 10) maxCleanRate = Math.max(maxCleanRate, from);
|
// 水位永远立不起来 → 深跌后只剩 ×1.08/5 拍龟速爬坡,dp7 实测):窗口 ≥99% 且短窗 ≥99%
|
||||||
// 干净爬坡:退化段中不爬(30min 窗未恢复 99%);深跌后向水位快速回收 ×1.3/拍,水位之上 ×1.08/5 拍;
|
// 时的速率即为「实测可行」
|
||||||
// 爬坡验证码条件:主条件短窗成功率 ≥99.5%,备选「验证码 ≤1 且短窗 ≥98.5%」修 dp6 实测
|
if (success >= 0.99 && successS >= 0.99 && wS.sent >= 10) maxCleanRate = Math.max(maxCleanRate, from);
|
||||||
// 低速恢复锁死(涓流 1~2 个验证码在低速小分母下永远压住 99.5%,高速率下占比可忽略);
|
// 干净爬坡(v5.3):退化段中不爬;主条件短窗 ≥99.5%,备选「零我方故障且联合噪声 ≤8
|
||||||
// 短窗 0 个验证码恒可爬,或样本 ≥300 时比率已无意义(绝对护栏统一交给软砍规则)
|
// 且短窗 ≥96%」(与退段口径一致 —— 低速小分母下高窗口线不可达,爬坡本身就是摊薄手段,
|
||||||
|
// ×1.3/拍 中速率每上台阶噪声占比自动减半,自校正);池空独查;样本/占比条件废除,
|
||||||
|
// 绝对护栏统一交给软砍规则
|
||||||
if (to === from && !degraded && wS.sent >= 10) {
|
if (to === from && !degraded && wS.sent >= 10) {
|
||||||
const cleanS = successS >= 0.995 || (wS.captcha <= 1 && successS >= 0.985);
|
const cleanS = successS >= 0.995 || (infraFails === 0 && googleNoiseS <= 8 && successS >= 0.96);
|
||||||
if (cleanS && gateRatioS < 0.02 && wS.poolEmpty === 0 && (wS.captcha === 0 || wS.sent >= 300)) {
|
if (cleanS && wS.poolEmpty === 0) {
|
||||||
rampSeq++;
|
rampSeq++;
|
||||||
if (maxCleanRate > 0 && from < maxCleanRate * 0.75) {
|
if (maxCleanRate > 0 && from < maxCleanRate * 0.75) {
|
||||||
to = from * 1.3 + 0.2;
|
to = from * 1.3 + 0.2;
|
||||||
|
|||||||
Reference in New Issue
Block a user