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

19 KiB
Raw Blame History

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 实现时按此抄):

# 用法: 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):

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 计时

# 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,启动后:

docker inspect --format '{{.Name}} {{.Image}} {{index .RepoDigests 0}}' "$c"

写入 results.json.image。拉不到 Hub 时用 docker load(见 select.md §5),不要改 pin 成 latest。

1.5 清理

每批次结束:

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

请求(可执行):

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。)

请求:

# 约定包装 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)。

请求:

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

反爬记录(强制字段): 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 继续

发射:

# 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。切换节点(可选):

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

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。

国外复用时加:

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

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

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

每个方案一份:

{
  "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,不当成功正文。