onesvm-browser-server/bench/plan.md
chii 983259836d chore: init workspace with onesvm-dev-md + casa-commander
docs: 联网搜索服务架构方案全套(plan-final/design-arch/选型决策/整合导览/MCP文档/部署预设/联调手册)
bench: 5 方案 + 代理 + 站点矩阵本机实测工程(无密钥)
部署目标:primary mgr1 先行测试(待批准后执行)
2026-09-01 15:19:52 +08:00

488 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# S3b / S3c 本机 Docker 实测计划
- 对应筛选:`.dsh/artifacts/run-20260901-browser-arch/select.md`
- 主机假设:darwin arm64 / Docker Desktop 29.4.0(与 S3a 相同)
- 代理:先 `export PROXY_SUB_URL`(只从 `.dsh/artifacts/proxy-subscriptions.local.md` SUB-1 注入),再 `bash bench/proxy/up.sh`。国外组复用 `http://127.0.0.1:17890`。**脚本与 JSON 禁止出现订阅 URL / uuid / password。**
- 同时运行 **候选容器 ≤3**。`bench-proxy-mihomo` 算基建,不占 3 席。
- 每方案第一次发业务请求前:**idle 采样 5 分钟**。
## 0. 目录骨架(S3b 建,本文件只约定)
```
bench/
lib/
sample_stats.sh # docker stats 循环
curl_time.sh # 单次计时
summarize.py # 读 raw → results.json
searxng-cn/ compose.yml settings.yml request.sh results.json
trafilatura-http/ compose.yml Dockerfile app.py request.sh results.json
searxng-global/ compose.yml settings.yml request.sh results.json
lightpanda/ compose.yml request.sh results.json
chrome-headless-shell/ compose.yml request.sh results.json
raw/ # 各批次 stats-*.tsv 与 curl-*.jsonl(可选)
```
`results.json` 顶层必须能被评分卡直接引用,字段见 §4。
---
## 1. 统一采集规范
### 1.1 容器命名
| 方案 | 容器名 | 宿主机端口 |
|---|---|---|
| searxng-cn | `bench-searxng-cn` | 18880 → 8080 |
| trafilatura-http | `bench-trafilatura` | 18881 → 8080 |
| searxng-global | `bench-searxng-global` | 18882 → 8080 |
| lightpanda | `bench-lightpanda` | 19222 → 9222 |
| chrome-headless-shell | `bench-headless-shell` | 19223 → 9222 |
| 代理(基建) | `bench-proxy-mihomo` | 17890 / 19090(已有) |
### 1.2 docker stats
`bench/lib/sample_stats.sh` 约定(S3b 实现时按此抄):
```bash
# 用法: sample_stats.sh <label> <interval_s> <duration_s> <container...>
# 例: bash bench/lib/sample_stats.sh searxng-cn-idle 5 300 bench-searxng-cn
label="$1"; interval="$2"; duration="$3"; shift 3
out="bench/raw/${label}.tsv"
echo -e "ts_iso\tname\tmem_bytes\tmem_limit_bytes\tcpu_pct" > "$out"
end=$(( $(date +%s) + duration ))
while [ "$(date +%s)" -lt "$end" ]; do
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
docker stats --no-stream --format '{{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}' "$@" \
| while IFS=$'\t' read -r name mem cpu; do
# MemUsage 形如 "85.2MiB / 256MiB" —— summarize.py 解析
printf '%s\t%s\t%s\t%s\n' "$ts" "$name" "$mem" "$cpu" >> "$out"
done
sleep "$interval"
done
```
- idle:`interval=5` `duration=300`(5 分钟),**期间禁止发业务请求**。
- 单会话:发请求前 10s 起采到请求结束后 10s,`interval=1`。
- T5 burst:从 t=0 到全部结束 + 15s,`interval=1`。
- 指标:`idle_mb` = 5 min 中位;`session_delta_mb` = 单会话峰值 − idle 中位;`burst_peak_mb` = burst 窗口最大。
辅助(可选,更准 RSS):
```bash
docker exec "$c" sh -c 'cat /sys/fs/cgroup/memory.current 2>/dev/null || cat /sys/fs/cgroup/memory/memory.usage_in_bytes'
```
cgroup 读不到就只信 `docker stats`,在 `results.json` 标 `rss_source=docker_stats`。
### 1.3 curl 计时
```bash
# bench/lib/curl_time.sh
# 环境: URL METHOD DATA PROXY(可选)
extra=()
[ -n "${PROXY:-}" ] && extra+=(-x "$PROXY")
code_file=$(mktemp)
body_file=$(mktemp)
metrics=$(curl -sS "${extra[@]}" -X "${METHOD:-GET}" \
-o "$body_file" \
-w '%{http_code} %{time_namelookup} %{time_connect} %{time_starttransfer} %{time_total} %{size_download}' \
--connect-timeout 10 --max-time "${MAX_TIME:-30}" \
-A 'onesvm-bench/s3b' \
${DATA:+-H 'Content-Type: application/json' -d "$DATA"} \
"$URL")
# 输出一行 JSON 到 stdout,body 路径给断言脚本
```
写入 jsonl 的字段:`ts, scheme, template, http_code, t_namelookup, t_connect, ttfb, t_total, bytes, assert_ok, note`。
p50 / p95:对该模板全部 `t_total` 排序后取分位(线性插值即可)。**不要**用 Excel 手估。
### 1.4 镜像 pin 与 digest
每个 `compose.yml` 写死 `select.md` 的 tag,启动后:
```bash
docker inspect --format '{{.Name}} {{.Image}} {{index .RepoDigests 0}}' "$c"
```
写入 `results.json.image`。拉不到 Hub 时用 `docker load`(见 select.md §5),不要改 pin 成 `latest`。
### 1.5 清理
每批次结束:
```bash
docker rm -f bench-searxng-cn bench-trafilatura bench-searxng-global bench-lightpanda bench-headless-shell || true
# 代理只在当天国外批次全部结束时 down
# bash bench/proxy/down.sh
```
---
## 2. 五套需求模板
### T1 国内关键词搜索 — 方案 `searxng-cn`
**请求(可执行):**
```bash
curl -sS -G 'http://127.0.0.1:18880/search' \
--data-urlencode 'q=跨境电商 出口退税 政策 2026' \
--data-urlencode 'format=json' \
--data-urlencode 'language=zh-CN' \
--data-urlencode 'categories=general'
```
**断言(脚本硬判):**
1. HTTP 200。
2. `results` 为数组且 `length >= 5`。
3. 前 10 条中,至少 5 条同时具备非空 `title`、非空 `url`(`http` 开头)、非空 `content`(strip 后 ≥ 8 字符)。
4. `results[*].engine` 去重后 **≥ 2**(允许百度+必应,或搜狗+360)。若 `unresponsive_engines` 含某引擎,记入 `note`,不单独失败。
5. 至少 1 条 `url` host 匹配 `gov.cn|customs.gov.cn|chinatax.gov.cn|mofcom.gov.cn|yidaiyilu.gov.cn|news.cn|people.com.cn|xinhuanet.com`(政策向)。不满足则 `assert_ok=false` 但把实际 host 列表留下,供人工改阈值。
**失败分类:** `json_403`(忘开 formats)/ `engine_outage` / `thin_snippets` / `timeout`。
**质量抽检(人工 2 分钟):** 打开第 1 条 URL,标题是否与结果 `title` 同主题。
---
### T2 国内页面正文提取 — 方案 `trafilatura-http`
**URL 选择顺序:**
1. 优先用 T1 结果里第一条 host ∈ `gov.cn|news.cn|people.com.cn` 且 GET 200 的文章 URL。
2. 回退固定页(公开、长期稳定、有导航壳):
```
https://www.gov.cn/zhengce/2021-03/13/content_5592681.htm
```
(《国民经济和社会发展第十四个五年规划和2035年远景目标纲要》。若 2026 年该 URL 变更,改用 T1 第一条 gov.cn。)
**请求:**
```bash
# 约定包装 API(S3b 实现 app.py 时遵守)
curl -sS -X POST 'http://127.0.0.1:18881/v1/read' \
-H 'Content-Type: application/json' \
-d '{"url":"https://www.gov.cn/zhengce/2021-03/13/content_5592681.htm","formats":["markdown"],"max_chars":20000}'
```
包装器内部:`trafilatura.fetch_url` + `trafilatura.extract(..., output_format="markdown", include_comments=False, include_tables=True, favor_precision=True)`。
**断言:**
1. HTTP 200,`ok=true`,`markdown` 长度 ≥ 800。
2. `title` 非空。
3. **无导航/广告残渣(规则):** `markdown` 不得匹配下列任一条(忽略大小写):
- `网站地图|设为首页|加入收藏|政务微信|客户端下载|无障碍浏览`
- 连续导航口吻:`首页\s*[>|/]\s*政策` 且出现 ≥ 3 次面包屑分隔
4. **正文在:** 须同时命中 `十四五` 与 `2035`(回退 URL);若改用 T1 URL,改为命中该条 T1 `title` 的前 4 个汉字。
5. 不得把整页当 HTML 倾倒:`markdown` 中 `<div` / `<script` 出现次数 = 0。
6. 若 `truncated=true`,`char_count` 必须 = `max_chars`。
**失败分类:** `fetch_fail` / `empty_extract` / `nav_residue` / `html_dump`。
---
### T3 国外关键词搜索 — 方案 `searxng-global`(经代理)
**前置:** `bench-proxy-mihomo` 已 up;SearXNG 容器 `HTTP_PROXY=http://host.docker.internal:17890`(或 settings `outgoing.proxies`)。
**请求:**
```bash
curl -sS -G 'http://127.0.0.1:18882/search' \
--data-urlencode 'q=best bluetooth earbuds 2026 reddit' \
--data-urlencode 'format=json' \
--data-urlencode 'language=en-US' \
--data-urlencode 'categories=general'
```
**断言:**
1. HTTP 200,`results.length >= 5`。
2. `results[*].engine` 去重 ≥ 2(多源聚合)。
3. `results[*].url` host 去重 ≥ 3。
4. 至少 1 条 URL host 为 `reddit.com` 或 `redd.it`,**或** `title|content` 含 `reddit`(小写)。
说明:S3a 对 `https://www.reddit.com/` 裸 GET 为 **403**,本模板走元搜索,不直接打 reddit 首页。
5. 记录 `unresponsive_engines`。Google 单独 403/验证码 → `note=google_captcha`,只要其余引擎撑住断言 1–4 仍算通过。
**失败分类:** `proxy_down`(先 `curl -x http://127.0.0.1:17890 https://www.google.com/generate_204` 期望 204)/ `single_engine` / `no_reddit_ref`。
---
### T4 国外 JS 渲染页提取 — `lightpanda` 主测,`chrome-headless-shell` 对照
**页面(按序,记录每一档):**
| 档 | URL | 目的 |
|---|---|---|
| T4-ctrl | `https://quotes.toscrape.com/js/` | 无 WAF 的 JS 对照:至少 3 条 quote 文本 |
| T4-shop | `https://www.allbirds.com/products/mens-wool-runners` | Shopify 公开商品:标题 + 价格字段 |
| T4-amz | `https://www.amazon.com/dp/B08N5WRWNW` | Amazon 公开商品页(Echo Dot 常见 ASIN)。**预期反爬,必须记。** |
不要用 Medium 登录墙当主断言。若 Allbirds 404,改 `https://www.allbirds.com/` 首页抽首个商品价。
**驱动约定(两方案同一脚本,只换 CDP 端点):**
- Lightpanda:`http://127.0.0.1:19222`(CDP)。
- shell:`http://127.0.0.1:19223`。
- 实现可用 **Playwright** `connectOverCDP` 或 **chromedp**;不要再在客户端拉起本地 Chrome。
- 导航超时 25s;`waitUntil=networkidle` 最多 8s,超时则退 `domcontentloaded` + 固定 sleep 2s。
- 代理:容器启动已带 `--proxy-server` / `HTTP_PROXY`,客户端不要套两层。
**抽取:**
```
title = document.title
text = document.body.innerText.slice(0, 8000)
# T4-shop / T4-amz:
price = meta[itemprop=price] || [class*="price"] 首个匹配的 innerText
jsonld = script[type="application/ld+json"] 里 Offers.price(有则优先)
```
**断言:**
| 档 | 通过条件 |
|---|---|
| T4-ctrl | `text` 含 `quote` 或作者名;或 ≥3 个 `.quote` 节点。失败 = 内核 JS 基本不可用。 |
| T4-shop | `title` 含 `Wool Runner` 或 `Allbirds`;`price` 匹配 `\$[0-9]` 或 `jsonld` 有数字价。 |
| T4-amz | 能解析 `title` 且不像验证码页;`price` 可选。若 title/text 匹配 `(?i)robot|captcha|sorry|click the button|continue shopping` → `blocked=true`,**不记方案失败**,记 `waf`。 |
**反爬记录(强制字段):** `http_status_in_page`, `blocked`, `challenge_vendor`(`cloudflare|datadome|akamai|amazon|unknown|none`), `final_url`。
评分:T4-ctrl 失败重创质量分;T4-amz 拦截只进 `note`,不把成功率打到 0。
---
### T5 60 会话 burst
**语义:** 60 = **同时涌入的请求数**(队列深度),不是 60 个 Chromium。浏览器方案 `CONCURRENT=1`(shell)或 Lightpanda 单进程多客户端;超槽位必须排队或 429,禁止把 60 个 Chrome 拉起来。
| 方案 | 涌入内容 | 并发模型 |
|---|---|---|
| searxng-cn | 60× T1 同 query | 直接打 SearXNG,无自研队列 |
| trafilatura-http | 60× T2 同 URL | 包装器内 semaphore=8(避免 60 条 TCP 打爆目标站) |
| searxng-global | 60× T3 同 query | 同 CN |
| lightpanda | 60× T4-ctrl | 客户端最多 4 个并行 CDP 导航,其余等(模拟网关 QUEUED) |
| chrome-headless-shell | 60× T4-ctrl | **并行度 1**,其余 FIFO;超时 30s/个则记 fail 继续 |
**发射:**
```bash
# 60 个后台 curl 同一秒涌入(搜索类)
seq 60 | xargs -P 60 -I{} bash bench/lib/curl_time.sh
```
Trafilatura / 浏览器不要用 `-P 60` 打目标站;用本机队列脚本:60 个任务入队,worker 池 = 上表并行度。
**记录:**
- 排队:首个完成 ttfb、最后完成 `t_total`、是否出现 429/503。
- 成功率:断言通过 / 60。
- p50 / p95:60 个 `t_total`。
- `burst_peak_mb`、是否 OOM(`docker inspect .State.OOMKilled`)。
- 浏览器方案若未实现网关,用脚本侧 FIFO 代替,`results.json.queue_impl=client_fifo`,并在 note 写「生产应是 SQLite WAL + CONCURRENT」。
**断言(方案级):**
1. 容器未退出、`OOMKilled=false`。
2. 搜索类成功率 ≥ 85%;读取/JS ≥ 70%(T4-ctrl)。
3. 峰值内存:搜索/trafilatura < 400 MB;lightpanda < 256 MB cgroup;shell < 400 MB cgroup。超限记 `mem_over`,综合评分按评分卡 0 档。
---
## 3. 批次安排(本机 ≤3 候选容器)
每方案:**up → idle 5 min → 单模板 5 次(取中位)→ 该方案 T5 → down**。不要跨方案复用未清的浏览器进程。
| 批次 | 候选容器 | 基建 | 做的模板 | 预计墙钟 |
|---|---|---|---|---|
| **A 国内** | `searxng-cn` + `trafilatura-http` | 无 | T1(cn)→ T2(用 T1 URL)→ T5 cn → T5 trafilatura | 40–50 min |
| **B 国外搜索+轻渲染** | `searxng-global` + `lightpanda` | `bench-proxy-mihomo` | T3 → T4 三档(panda)→ T5 global → T5 panda | 50–70 min |
| **C 保真对照** | `chrome-headless-shell` + `lightpanda`(已有数据可不再 idle 5 min,若刚 down 过则重做 idle) | 同一代理 | T4 三档(shell)→ T5 shell | 40–60 min |
| **D(可选)合成冒烟** | `searxng-cn` + `searxng-global` + `trafilatura-http` | 代理 | 各 1 次 T1/T2/T3,只看三容器 RSS 之和 | 15 min |
批次 A 与 B 不可与 C 重叠。D 不得加入 shell。
**代理:** 批次 B 开始时 `up.sh`,C 结束或当天收工时 `down.sh`。切换节点(可选):
```bash
curl -sS -X PUT "http://127.0.0.1:19090/proxies/%F0%9F%9A%80%E8%8A%82%E7%82%B9%E9%80%89%E6%8B%A9" \
-H 'Content-Type: application/json' \
-d '{"name":"🇺🇸美国圣何塞02 | 三网推荐"}'
```
节点名以 `bench/proxy/probe-results.json` 为准。
---
## 4. 各方案 compose / 请求要点
### 4.1 `bench/searxng-cn/compose.yml`
```yaml
services:
searxng:
image: docker.io/searxng/searxng:2026.8.29-d226b78bc
container_name: bench-searxng-cn
ports: ["18880:8080"]
volumes:
- ./settings.yml:/etc/searxng/settings.yml:ro
environment:
- SEARXNG_BASE_URL=http://localhost:18880/
mem_limit: 192m
restart: "no"
```
`request.sh t1|t5`。settings 见 `select.md` §1.1。
### 4.2 `bench/trafilatura-http/`
`Dockerfile`:
```
FROM python:3.12-slim-bookworm
RUN pip install --no-cache-dir trafilatura==2.2.0
WORKDIR /app
COPY app.py .
EXPOSE 8080
CMD ["python", "-u", "app.py"]
```
`app.py`:stdlib `http.server` 即可,避免再拉 Flask。只实现 `POST /v1/read`。`mem_limit: 128m`。
国外复用时加:
```yaml
environment:
HTTP_PROXY: http://host.docker.internal:17890
HTTPS_PROXY: http://host.docker.internal:17890
NO_PROXY: 127.0.0.1,localhost
```
本轮 T2 **不要**开代理。
### 4.3 `bench/searxng-global/compose.yml`
同 4.1,端口 18882,容器名 `bench-searxng-global`,`extra_hosts: ["host.docker.internal:host-gateway"]`,环境或 settings 走 17890。`mem_limit: 192m`。
### 4.4 `bench/lightpanda/compose.yml`
```yaml
services:
lightpanda:
image: docker.io/lightpanda/browser:0.3.7
container_name: bench-lightpanda
command: ["lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222"]
ports: ["19222:9222"]
extra_hosts: ["host.docker.internal:host-gateway"]
environment:
HTTP_PROXY: http://host.docker.internal:17890
HTTPS_PROXY: http://host.docker.internal:17890
mem_limit: 256m
```
`request.sh`:Node 或 Python Playwright `connectOverCDP('http://127.0.0.1:19222')`。
### 4.5 `bench/chrome-headless-shell/compose.yml`
```yaml
services:
shell:
image: docker.io/chromedp/headless-shell:151.0.7922.109
container_name: bench-headless-shell
ports: ["19223:9222"]
extra_hosts: ["host.docker.internal:host-gateway"]
command:
- --no-sandbox
- --remote-debugging-address=0.0.0.0
- --remote-debugging-port=9222
- --proxy-server=http://host.docker.internal:17890
shm_size: "128m"
mem_limit: 400m
```
镜像 ENTRYPOINT 已是 headless-shell 时,`command` 只追加 flag。S3b 若启动即退,把 `docker logs` 贴进 `results.json.note`(**未验证** Desktop 对 151.x arm64)。
---
## 5. `results.json` schema
每个方案一份:
```json
{
"scheme": "searxng-cn",
"group": "domestic",
"image": {"ref": "docker.io/searxng/searxng:2026.8.29-d226b78bc", "digest": "sha256:…"},
"probed_at": "2026-09-01T00:00:00+08:00",
"rss_source": "docker_stats",
"idle_mb_p50": null,
"session_delta_mb": null,
"burst_peak_mb": null,
"templates": {
"T1": {
"n": 5,
"success": 0,
"http_codes": [],
"p50_s": null,
"p95_s": null,
"assert_ok": false,
"note": ""
}
},
"t5": {
"n": 60,
"parallelism": 60,
"queue_impl": "none",
"success": 0,
"p50_s": null,
"p95_s": null,
"oom": false,
"http_429": 0,
"http_503": 0
},
"t4_waf": [],
"scorecard": {
"mem": null, "quality": null, "stability": null, "latency": null, "ops": null, "total": null
}
}
```
T4 的 `t4_waf` 元素:`{tier, url, blocked, vendor, http_status_in_page}`。
填完后把数字抄回 `select.md` §4.7。本文件不改 select 以外的架构文档。
---
## 6. S3b / S3c 执行顺序(可当 checklist)
**S3b(国内,可无代理)**
1. `docker pull` 或 `load`:`searxng/searxng:2026.8.29-d226b78bc`、`python:3.12-slim-bookworm`。
2. 批次 A:建 `searxng-cn` + `trafilatura-http` 目录与 compose。
3. 只起 `bench-searxng-cn` → idle 5 min → T1 ×5 → T5。
4. 保持 cn 或 down 后起 `bench-trafilatura`(同时 ≤2)→ idle 5 min → T2 ×5(URL 来自 T1)→ T5。
5. 写两份 `results.json`。两容器同时存在时再采 30s,看 RSS 之和(P0 冒烟)。
**S3c(国外,必须代理)**
1. `bash bench/proxy/up.sh`;`curl -x http://127.0.0.1:17890 -o /dev/null -w '%{http_code}\n' https://www.google.com/generate_204` 期望 204。
2. pull/load:`lightpanda/browser:0.3.7`、`chromedp/headless-shell:151.0.7922.109`。
3. 批次 B:global idle → T3 ×5 → T5;panda idle → T4 三档各 3 次 → T5-ctrl。
4. 批次 C:down panda(或留着对照但 **不要** 同时 T5)→ shell idle → T4 三档 → T5-ctrl。
5. `down.sh`。写 `results.json` + 回填评分卡。
密钥扫描(收工):`rg -n 'uuid:|password:|PROXY_SUB_URL=http' bench` 应无节点凭据。
---
## 7. 本机注意
- Apple Silicon 必须拉 **arm64** digest。`docker image inspect --format '{{.Architecture}}'` 若为 `amd64` 会走 qemu,内存与延迟作废,重拉 `--platform linux/arm64`。
- `host.docker.internal` 在 Docker Desktop 可用;若不通,改 `172.17.0.1` 或 `host-gateway`,记入 note。
- 目标站限流:T2/T4 单 URL 5 次 + T5 会打同一 host。Trafilatura T5 并行度 ≤8;Amazon T4 每方案最多 3 次,禁止 T5 打 Amazon。
- 合规:只打公开页;不登录、不绕验证码、不采集个人数据。拦截页当 `blocked`,不当成功正文。