docs: 联网搜索服务架构方案全套(plan-final/design-arch/选型决策/整合导览/MCP文档/部署预设/联调手册) bench: 5 方案 + 代理 + 站点矩阵本机实测工程(无密钥) 部署目标:primary mgr1 先行测试(待批准后执行)
246 lines
7.9 KiB
Bash
Executable file
246 lines
7.9 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
# lightpanda S3c: up → idle 5min → T4 tiers ×3 → T5 (worker=4) → summarize
|
||
set -euo pipefail
|
||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||
# shellcheck disable=SC1091
|
||
source "$ROOT/_lib.sh"
|
||
NAME="bench-s3c-headless-shell"
|
||
IMG="docker.io/chromedp/headless-shell:151.0.7922.109"
|
||
CDP_HTTP="${CDP_HTTP:-http://127.0.0.1:19223}"
|
||
SCHEME="${SCHEME:-chrome-headless-shell}"
|
||
COMPOSE_PROJ="${COMPOSE_PROJ:-bench-s3c-headless-shell}"
|
||
T5_PAR="${T5_PAR:-1}"
|
||
RAW="$ROOT/raw"
|
||
SAMP="$ROOT/samples"
|
||
mkdir -p "$RAW" "$SAMP"
|
||
export CDP_HTTP SCHEME
|
||
exec > >(tee -a "$RAW/run.log") 2>&1
|
||
|
||
log "=== $SCHEME S3c start ==="
|
||
ensure_image "$IMG" "$RAW/pull.how"
|
||
PULL_HOW="$(cat "$RAW/pull.how" 2>/dev/null || echo unknown)"
|
||
|
||
if [[ "${SKIP_UP:-}" != "1" ]]; then
|
||
docker rm -f "$NAME" >/dev/null 2>&1 || true
|
||
log "docker compose up -d project=$COMPOSE_PROJ"
|
||
(cd "$ROOT" && docker compose -p "$COMPOSE_PROJ" up -d --force-recreate)
|
||
fi
|
||
|
||
log "wait CDP $CDP_HTTP/json/version"
|
||
if ! wait_http "$CDP_HTTP/json/version" 40; then
|
||
log "json/version not ready; try /json/list"
|
||
wait_http "$CDP_HTTP/json/list" 20 || true
|
||
docker logs --tail 80 "$NAME" | tee "$RAW/container-start.log" || true
|
||
fi
|
||
curl -sS --max-time 5 "$CDP_HTTP/json/version" | tee "$RAW/cdp-version.json" || true
|
||
echo
|
||
|
||
ARCH="$(docker image inspect --format '{{.Architecture}}' "$IMG" 2>/dev/null || echo '?')"
|
||
DIGEST="$(docker inspect --format '{{index .RepoDigests 0}}' "$NAME" 2>/dev/null || true)"
|
||
IID="$(docker inspect --format '{{.Image}}' "$NAME" 2>/dev/null || true)"
|
||
log "image arch=$ARCH digest=$DIGEST pull=$PULL_HOW"
|
||
|
||
PROXY_CODE="$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 10 --max-time 20 \
|
||
-x http://127.0.0.1:17890 -A onesvm-bench/s3c \
|
||
https://www.google.com/generate_204 || true)"
|
||
log "proxy generate_204 http=$PROXY_CODE"
|
||
HOST_RES="$(docker exec "$NAME" sh -c 'getent hosts host.docker.internal || true' 2>/dev/null || echo unresolved)"
|
||
log "host.docker.internal => $HOST_RES"
|
||
|
||
if [[ "${SKIP_IDLE:-}" != "1" ]]; then
|
||
log "idle 300s interval=5"
|
||
sample_stats "$RAW/idle.tsv" 5 300 "$NAME"
|
||
log "idle done"
|
||
else
|
||
log "SKIP_IDLE=1"
|
||
fi
|
||
|
||
T4_CTRL="https://quotes.toscrape.com/js/"
|
||
T4_MEDIUM="https://medium.com/airbnb-engineering/rearchitecting-airbnbs-frontend-b5da7e3e8395"
|
||
T4_SHOP="https://www.allbirds.com/products/mens-wool-runners"
|
||
T4_AMZ="https://www.amazon.com/dp/B08N5WRWNW"
|
||
T4_T3X="https://duckduckgo.com/?q=best+bluetooth+earbuds+2026+reddit"
|
||
|
||
SENT="$RAW/session.done"
|
||
rm -f "$SENT"
|
||
sample_until_file "$RAW/session.tsv" 1 "$SENT" 10 "$NAME" &
|
||
STAT_PID=$!
|
||
sleep 10
|
||
|
||
: > "$RAW/t4.jsonl"
|
||
run_tier() {
|
||
local tier="$1" url="$2" n="$3"
|
||
local i
|
||
for i in $(seq 1 "$n"); do
|
||
log "$tier trial $i url=$url"
|
||
set +e
|
||
line="$(bash "$ROOT/request.sh" "$tier" "$url" 25 2>"$RAW/${tier}-${i}.excerpt")"
|
||
rc=$?
|
||
set -e
|
||
echo "$line" | tee -a "$RAW/t4.jsonl"
|
||
if [[ "$i" -eq 1 ]]; then
|
||
cp "$RAW/${tier}-${i}.excerpt" "$SAMP/${tier}.json" 2>/dev/null || true
|
||
fi
|
||
log "$tier trial $i rc=$rc"
|
||
sleep 1
|
||
done
|
||
}
|
||
|
||
run_tier T4-ctrl "$T4_CTRL" 3
|
||
run_tier T4-medium "$T4_MEDIUM" 3
|
||
run_tier T4-shop "$T4_SHOP" 3
|
||
run_tier T4-amz "$T4_AMZ" 3
|
||
run_tier T4-t3x "$T4_T3X" 1
|
||
|
||
touch "$SENT"
|
||
wait "$STAT_PID" || true
|
||
|
||
log "T5 60x T4-ctrl parallelism=$T5_PAR queue=client_fifo"
|
||
SENT5="$RAW/t5.done"
|
||
rm -f "$SENT5"
|
||
sample_until_file "$RAW/t5-stats.tsv" 1 "$SENT5" 15 "$NAME" &
|
||
STAT5_PID=$!
|
||
T5_START_MS=$(python3 -c 'import time;print(int(time.time()*1000))')
|
||
: > "$RAW/t5.jsonl"
|
||
: > "$RAW/t5-queue.jsonl"
|
||
|
||
python3 - "$ROOT" "$T4_CTRL" "$T5_PAR" "$T5_START_MS" "$RAW/t5.jsonl" "$RAW/t5-queue.jsonl" <<'PY'
|
||
import subprocess, time, json, os, sys
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
root, url, par, t0, outp, qpath = sys.argv[1:7]
|
||
par = int(par)
|
||
t0 = int(t0) / 1000.0
|
||
n = 60
|
||
req = os.path.join(root, "request.sh")
|
||
|
||
def one(i):
|
||
enq = time.time()
|
||
p = subprocess.run(["bash", req, "T5", url, "30"], capture_output=True, text=True)
|
||
end = time.time()
|
||
line = p.stdout.strip().splitlines()[-1] if p.stdout.strip() else "{}"
|
||
try:
|
||
row = json.loads(line)
|
||
except Exception:
|
||
row = {"assert_ok": False, "t_total": end - enq, "note": "parse", "http_code": 0}
|
||
start = end - float(row.get("t_total") or 0)
|
||
q = {
|
||
"i": i,
|
||
"enqueue_s": enq - t0,
|
||
"start_s": start - t0,
|
||
"end_s": end - t0,
|
||
"queue_wait_s": max(0.0, start - enq),
|
||
"assert_ok": row.get("assert_ok"),
|
||
"t_total": row.get("t_total"),
|
||
}
|
||
return row, q
|
||
|
||
rows = []
|
||
qs = []
|
||
with ThreadPoolExecutor(max_workers=par) as ex:
|
||
futs = [ex.submit(one, i) for i in range(1, n + 1)]
|
||
for f in as_completed(futs):
|
||
row, q = f.result()
|
||
rows.append(row)
|
||
qs.append(q)
|
||
with open(outp, "w", encoding="utf-8") as f:
|
||
for r in rows:
|
||
f.write(json.dumps(r, ensure_ascii=False) + "\n")
|
||
with open(qpath, "w", encoding="utf-8") as f:
|
||
for q in qs:
|
||
f.write(json.dumps(q) + "\n")
|
||
print(f"t5 done n={len(rows)} ok={sum(1 for r in rows if r.get('assert_ok'))}")
|
||
PY
|
||
|
||
T5_END=$(date +%s)
|
||
# wall from tsv start roughly
|
||
T5_WALL=$(python3 - "$RAW/t5-queue.jsonl" <<'PY'
|
||
import json,sys
|
||
from pathlib import Path
|
||
qs=[json.loads(l) for l in Path(sys.argv[1]).read_text().splitlines() if l.strip()]
|
||
if not qs:
|
||
print(0); raise SystemExit
|
||
print(round(max(q["end_s"] for q in qs), 3))
|
||
PY
|
||
)
|
||
log "T5 wall=${T5_WALL}s"
|
||
touch "$SENT5"
|
||
wait "$STAT5_PID" || true
|
||
|
||
python3 - "$RAW/t5.jsonl" "$SAMP" <<'PY'
|
||
import json
|
||
from pathlib import Path
|
||
import sys
|
||
p=Path(sys.argv[1]); d=Path(sys.argv[2])
|
||
rows=[json.loads(l) for l in p.read_text().splitlines() if l.strip()]
|
||
for i,r in enumerate(rows[:2],1):
|
||
(d/f"t5-{i}.json").write_text(json.dumps(r,ensure_ascii=False,indent=2)+"\n")
|
||
print("saved t5 samples", len(rows))
|
||
PY
|
||
|
||
# html vs text note from first ctrl excerpt
|
||
python3 - "$SAMP" "$RAW" <<'PY'
|
||
import json
|
||
from pathlib import Path
|
||
samp=Path("$SAMP".replace("$SAMP", __import__("os").environ.get("SAMP","")))
|
||
PY
|
||
|
||
OOM="$(docker inspect --format '{{.State.OOMKilled}}' "$NAME" 2>/dev/null || echo false)"
|
||
STATUS="$(docker inspect --format '{{.State.Status}}' "$NAME" 2>/dev/null || echo unknown)"
|
||
log "status=$STATUS oom=$OOM"
|
||
docker logs --tail 50 "$NAME" | tee "$RAW/container-tail.log" || true
|
||
|
||
python3 - <<PY
|
||
import json, datetime, statistics, math
|
||
from pathlib import Path
|
||
raw = Path("$RAW")
|
||
qs = []
|
||
qp = raw / "t5-queue.jsonl"
|
||
if qp.exists():
|
||
qs = [json.loads(l) for l in qp.read_text().splitlines() if l.strip()]
|
||
def pct(xs, p):
|
||
if not xs: return None
|
||
s=sorted(xs)
|
||
k=(len(s)-1)*p/100
|
||
f=int(k); c=int(math.ceil(k))
|
||
if f==c: return round(s[f],4)
|
||
return round(s[f]+(s[c]-s[f])*(k-f),4)
|
||
waits=[q.get("queue_wait_s") or 0 for q in qs]
|
||
ends=[q.get("end_s") or 0 for q in qs]
|
||
meta = {
|
||
"ref": "$IMG",
|
||
"digest": None if "$DIGEST" in ("<no value>", "") else "$DIGEST",
|
||
"id": "$IID",
|
||
"arch": "$ARCH",
|
||
"pull": "$PULL_HOW",
|
||
"probed_at": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
|
||
"oom": str("$OOM").lower()=="true",
|
||
"container_status": "$STATUS",
|
||
"t5_wall_s": float("$T5_WALL" or 0),
|
||
"t5_parallelism": int("$T5_PAR"),
|
||
"queue_impl": "client_fifo",
|
||
"queue": {
|
||
"first_complete_s": round(min(ends),4) if ends else None,
|
||
"last_complete_s": round(max(ends),4) if ends else None,
|
||
"queue_wait_p50_s": pct(waits,50),
|
||
"queue_wait_p95_s": pct(waits,95),
|
||
"rejected": 0,
|
||
},
|
||
"proxy": {
|
||
"mixed": "http://127.0.0.1:17890",
|
||
"container_via": "http://host.docker.internal:17890",
|
||
"generate_204_http": "$PROXY_CODE",
|
||
"host_docker_internal": """$HOST_RES""".strip(),
|
||
"egress_note": "HTTP(S)_PROXY + extra_hosts host-gateway",
|
||
},
|
||
"commands": [
|
||
"docker compose -p $COMPOSE_PROJ up -d",
|
||
"node cdp_fetch.mjs $CDP_HTTP <url>",
|
||
"T5 ThreadPoolExecutor workers=$T5_PAR n=60",
|
||
],
|
||
"notes": ["queue_impl=client_fifo; 生产应是 SQLite WAL + CONCURRENT"],
|
||
}
|
||
(raw/"image-meta.json").write_text(json.dumps(meta, ensure_ascii=False, indent=2)+"\n")
|
||
PY
|
||
|
||
python3 "$ROOT/summarize.py"
|
||
log "=== $SCHEME S3c done ==="
|