Files
gongxue-base/docs/refactor/performance-benchmark-runbook.md
2026-07-12 19:26:57 +08:00

340 lines
16 KiB
Markdown
Raw 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.

# API 压测与 4 核 16G 容量评估 Runbook
更新时间2026-07-01
这份 runbook 用于在本地 Docker/Supabase 或未来 4 核 16G 云服务器上,对 SaaS 题库 API 做可重复压测。目标不是一次性给出永久容量承诺而是建立一套可以随着题库、用户量、SQL 和硬件变化持续复跑的基线。
## 工具
压测脚本:
```bash
npm run perf:api:local
```
脚本会:
- 自动从 `DATABASE_URL` 指向的 PostgreSQL 中发现一个活跃租户、学生、题库入口、分类节点、合集、练习蓝图、单词单元和知识手册。
- 默认启动本地 API 服务,并使用真实 API 请求做混合读路径压测。
- 默认不创建练习 session不写业务数据。
- 输出 JSON 和 Markdown 报告到 `docs/refactor/performance-reports/`。该目录已被 `.gitignore` 忽略,不应提交。
默认本地兼容模式使用 `x-user-id` 作为压测身份,只适合开发机。更接近 Taro/Supabase 接入方式的压测应启用迁移期 Bearer session
```powershell
$env:PERF_AUTH_MODE="app_session"
npm run perf:api:local
Remove-Item Env:\PERF_AUTH_MODE
```
`app_session` 模式会在测试库中为压测学生创建 2 小时 `tk_` 会话,然后用 `Authorization: Bearer <tk_...>` 请求业务接口API 可以关闭 `ALLOW_LEGACY_AUTH_HEADERS`。生产远程压测如果已经有真实 Supabase access token也可以用 `PERF_AUTH_MODE=bearer``PERF_BEARER_TOKEN`
PostgreSQL 调参与运行证据:
```bash
npm run perf:postgres:evidence
```
该脚本会把关键 `pg_settings`、连接等待、缓存命中、大表大小和可选 `pg_stat_statements` Top SQL 输出到 `docs/refactor/launch-artifacts/`。调参前后各跑一次,配合 API 压测报告判断是否真正改善。
生产上线前必须用严格模式跑一次,并把摘要填入 `production-launch-evidence.json``postgres.tuning-evidence`
```bash
PG_TUNING_PROFILE=shared-host npm run perf:postgres:evidence -- --strict --json
```
严格模式要求 4c16g profile 范围内、无 `pending_restart``pg_stat_statements` 可用、`jit=off`,且 API 请求相关超时不为 0。需要生成 `ALTER SYSTEM` SQL 时使用:
```bash
npm run perf:postgres:sql -- --profile=shared-host
```
角色旅程烟测:
```bash
npm run smoke:launch-persona -- --write docs/refactor/launch-artifacts/launch-persona-smoke.json --write-md docs/refactor/launch-artifacts/launch-persona-smoke.md
```
该脚本从普通学生、租户管理员、平台管理员三个视角调用真实 API覆盖 SVIP 后刷题、收藏、错题复习入口、租户 dashboard/主题/学生/销售转化、平台租户/套餐/审计入口和越权拒绝。它会写入少量 `launch_persona_smoke` 测试练习与收藏记录,只建议在本地、预生产或灰度租户运行。默认会生成带时间戳的报告;生产证据必须额外带 `--write` 生成稳定 JSON`production-launch-evidence.json``launch:gate` 引用。
## 前置条件
本地真实迁移库压测:
```powershell
npx supabase db reset
$env:DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:54322/postgres"
npm run pb:import:json
npm run pb:import:validate
npm run perf:api:local
```
烟测 seed 压测:
```powershell
$env:DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:54322/postgres"
npm run db:smoke-seed -- --confirm=SMOKE_SEED_LOCAL_OR_CI_ONLY
npm run perf:api:local
```
如果 API 已经在运行:
```powershell
$env:PERF_START_SERVER="false"
$env:PERF_API_BASE="http://127.0.0.1:8787"
npm run perf:api:local
```
## 环境变量
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `DATABASE_URL` | `postgresql://postgres:postgres@127.0.0.1:54322/postgres` | 压测使用的数据源 |
| `PERF_API_BASE` | 空 | 已运行 API 地址;为空时默认启动本地 API |
| `PERF_START_SERVER` | 根据 `PERF_API_BASE` 推断 | 是否由脚本启动 API |
| `PERF_API_PORT` | 随机端口 | 脚本启动 API 时使用的端口 |
| `PERF_TENANT_CODE` | `master` | 优先选择的租户 slug |
| `PERF_DURATION_SECONDS` | `15` | 压测持续时间 |
| `PERF_CONCURRENCY` | `6` | 并发 worker 数 |
| `PERF_RAMP_SECONDS` | `3` | 并发爬坡时间 |
| `PERF_QUESTION_LIMIT` | `20` | 单次合集题目请求数量 |
| `PERF_INCLUDE_WRITES` | `false` | 是否加入真实刷题读写闭环 |
| `PERF_PRACTICE_FLOW_RATIO` | `0.15` | 开启写入后worker 触发刷题闭环的概率,建议 0.05 到 0.15 |
| `PERF_PRACTICE_FLOW_ANSWERS` | `3` | 每个刷题闭环提交的题目数量 |
| `PERF_ENSURE_SVIP_FOR_WRITES` | `true` | 写压测时如果压测学生没有 SVIP自动创建 1 天测试权益,避免免费额度污染结果 |
| `PERF_INCLUDE_LEADERBOARD` | `false` | 是否加入排行榜接口;排行榜租户默认关闭,仅在租户明确开启并需要专项压测时打开 |
| `PERF_OUTPUT_DIR` | `docs/refactor/performance-reports` | 报告输出目录 |
| `PERF_REQUEST_TIMEOUT_MS` | `15000` | 单请求超时 |
| `PERF_AUTH_MODE` | `legacy` | `legacy` 使用本地 `x-user-id``app_session` 创建 `tk_` Bearer session`bearer` 使用 `PERF_BEARER_TOKEN``none` 只适合公开接口 |
| `PERF_BEARER_TOKEN` | 空 | `PERF_AUTH_MODE=bearer` 时使用 |
## 默认工作负载
默认混合读路径包含:
- `/health`
- `/api/tenant/resolve`
- `/api/catalog/regions`
- `/api/catalog/content-entries`
- `/api/catalog/content-nodes?mode=flat`
- `/api/catalog/question-collections`
- `/api/catalog/question-collections/questions`
- `/api/catalog/practice-blueprints`
- `/api/learning/stats`
- `/api/learning/trend`
- `/api/catalog/vocabulary-words`
- `/api/learning/vocabulary/stats`
- `/api/catalog/handbook-chapters`
- `/api/catalog/handbook-entries?includeContent=true`
排行榜租户默认关闭,不进入默认压测负载。只有在租户已经开启 `feature_flags.enableLeaderboard=true`,并准备验证独立排行榜页或活动页容量时,才打开:
```powershell
$env:PERF_INCLUDE_LEADERBOARD="true"
npm run perf:api:local
```
写入路径默认关闭。只有在专门的测试库或可丢弃预生产库中,才建议打开:
```powershell
$env:PERF_INCLUDE_WRITES="true"
npm run perf:api:local
```
开启后会按配置比例执行完整刷题闭环:`POST /api/learning/practice-sessions` 创建 session`GET /api/learning/practice-sessions/detail` 拉题,`POST /api/learning/answers` 提交若干题答案,`POST /api/learning/practice-sessions/submit` 交卷,再 `GET /api/learning/practice-sessions/report` 读取报告。脚本会优先选择已有 SVIP 权益学生;若测试库中没有权益且 `PERF_ENSURE_SVIP_FOR_WRITES=true`,会给压测学生创建 1 天 `performance_benchmark` 来源的测试权益,避免每日免费额度导致大量 403 干扰容量结论。
压测并发 worker 是“无停顿请求流”,不能直接等同于真实在线学生数。真实学生在线刷题会有读题、思考、翻页和网络间隔。容量估算建议先用报告的成功 RPS再按前端真实埋点得到的单人 RPS 折算。例如每个真实学生平均 0.05 到 0.2 请求/秒,则 700 req/s 理论上约对应 3500 到 14000 名活跃在线学生的请求吞吐;正式承诺仍要以上云 4 核 16G 环境、CDN、对象存储和真实前端埋点复测为准。
## 4 核 16G 阶梯压测建议
在云服务器上先按 `docs/refactor/postgresql-4c16g-tuning.md` 配置 shared-host 起步值,执行 `perf:postgres:evidence -- --strict` 通过,再跑以下矩阵。每轮之间间隔 2 到 5 分钟,观察 CPU、内存、磁盘 I/O、连接数和慢 SQL。
| 场景 | 并发 | 时长 | 写入 | 用途 |
| --- | ---: | ---: | --- | --- |
| smoke | 6 | 30s | 否 | 确认部署和数据可访问 |
| baseline-10 | 10 | 2min | 否 | 学生正常浏览/刷题入口 |
| baseline-30 | 30 | 5min | 否 | 中小租户晚高峰 |
| baseline-50 | 50 | 5min | 否 | 单机读路径压力观察 |
| mixed-30 | 30 | 5min | 是 | 加入完整刷题闭环,观察组卷、答题、交卷和报告 |
| mixed-50 | 50 | 5min | 是 | 观察写入、锁和连接池 |
| spike-100 | 100 | 2min | 否 | 短峰值和缓存命中观察 |
| spike-100-write | 100 | 2min | 是 | 短峰值刷题闭环,找 P95/P99 拐点 |
本地 Docker Desktop 可以先用同一矩阵做跑分,但只能证明代码、索引和本机 Docker 环境的趋势。正式容量承诺必须以目标云服务器、生产 PostgreSQL 参数、生产 API/worker 连接池、对象存储/CDN 和真实网络重新跑。
## 本地 Docker 4c16g 模拟入口
仓库提供一个 Docker API 受限资源压测入口:
```powershell
$env:DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:54322/postgres"
$env:BENCHMARK_API_CPUS="2.0"
$env:BENCHMARK_API_MEMORY="4g"
$env:DB_POOL_MAX="10"
npm run perf:api:docker-4c16g
Remove-Item Env:\BENCHMARK_API_CPUS
Remove-Item Env:\BENCHMARK_API_MEMORY
Remove-Item Env:\DB_POOL_MAX
```
它会使用 `docker-compose.api.yml` + `docker-compose.api.benchmark.yml` 构建并启动 API 容器,默认限制 API 容器为 2 CPU/4G关闭 `ALLOW_LEGACY_AUTH_HEADERS`,然后用 `PERF_AUTH_MODE=app_session` 跑:
- 30 worker / 120s / 只读上线门禁。
- 50 worker / 60s / 10% 刷题闭环写入。
- 100 worker / 60s / 8% 刷题闭环写入。
- 150 worker / 60s / 6% 刷题闭环写入。
这个入口模拟的是“4 核 16G shared-host 中 API 容器的资源约束”,不是完整云服务器复刻。当前本地 Supabase/PostgreSQL 仍跑在 Docker Desktop 的 Supabase stack 中,除非额外手工限制 DB 容器资源,否则数据库容器仍可能使用 Docker Desktop 的全局资源。正式容量承诺必须在目标 4 核 16G 云服务器、生产 PostgreSQL shared-host 参数、真实对象存储/CDN 和真实网络下复跑。
每次运行都会额外生成 `docker-4c16g-resource-evidence-*.json/md`,记录 Docker Desktop 总 CPU/内存、API 容器实际 CPU/内存限制、DB 容器是否被限制、`DB_POOL_MAX` 和每个压测报告的摘要。该文件位于已忽略的 `docs/refactor/performance-reports/`,用于防止后续把“只限制 API 容器”的本地结果误写成完整 4 核 16G 生产容量;它不能替代目标云服务器正式压测证据。
如需要在本地显式限制 Supabase PostgreSQL 容器资源,可以打开下面开关。脚本默认会在结束后恢复 DB 容器原始限制;如果原始容器是 unlimited而当前 Docker Desktop 无法用 `docker update --memory 0` 清掉 live memory limit脚本会恢复到 Docker Desktop 当前 CPU/内存上限附近并在资源证据中写明。只有你希望保留限制用于排查时,才设置 `BENCHMARK_KEEP_DB_LIMIT=true`
```powershell
$env:BENCHMARK_LIMIT_DB_RESOURCES="true"
$env:BENCHMARK_DB_CONTAINER="supabase_db_tiku-saas-local"
$env:BENCHMARK_DB_CPUS="2.0"
$env:BENCHMARK_DB_MEMORY="8g"
npm run perf:api:docker-4c16g
Remove-Item Env:\BENCHMARK_LIMIT_DB_RESOURCES
Remove-Item Env:\BENCHMARK_DB_CONTAINER
Remove-Item Env:\BENCHMARK_DB_CPUS
Remove-Item Env:\BENCHMARK_DB_MEMORY
```
注意Docker `update` 只能限制已有容器的 cgroup 资源,不会替代 PostgreSQL 参数调优。限制 DB 容器后仍必须跑 `PG_TUNING_PROFILE=shared-host npm run perf:postgres:evidence -- --strict --json` 检查 `shared_buffers``work_mem`、超时、JIT、`pg_stat_statements``pending_restart`
如需保留 API 容器便于排查:
```powershell
$env:PERF_KEEP_DOCKER_API="true"
npm run perf:api:docker-4c16g
Remove-Item Env:\PERF_KEEP_DOCKER_API
```
PowerShell 示例:
```powershell
$env:DATABASE_URL="postgresql://postgres:***@127.0.0.1:5432/postgres"
$env:PERF_API_BASE="https://api.example.com"
$env:PERF_START_SERVER="false"
$env:PERF_DURATION_SECONDS="300"
$env:PERF_CONCURRENCY="30"
$env:PERF_RAMP_SECONDS="30"
$env:PERF_INCLUDE_WRITES="false"
npm run perf:api:local
```
## 初步验收线
上云测试早期建议先用保守指标:
- 只读 mixed catalog 工作负载P95 小于 300msP99 小于 800ms错误率小于 0.1%。
- 加入少量 session 写入P95 小于 500msP99 小于 1200ms错误率小于 0.5%。
- 数据库无连接耗尽、无 OOM、无长时间 `idle in transaction`
- `pg_stat_database``xact_rollback` 不应快速增长。
- `pg_stat_activity` 不应长期堆积锁等待或 I/O wait。
这些不是最终 SLA。正式 SLA 要结合真实云服务器、真实 CDN、真实对象存储、真实支付回调和前端 Web Vitals 重新制定。
## 问题定位
如果 P95 或 P99 明显升高,按下面顺序排查:
1. 查看报告的接口明细,先定位慢接口。
2. 打开 PostgreSQL 慢 SQL 日志,或在生产启用 `pg_stat_statements`
3. 对慢接口涉及 SQL 执行 `explain (analyze, buffers)`
4. 检查 API/worker 连接池是否过大导致数据库活跃连接超过 4 核可承受范围。
5. 检查 `content_nodes path``question_collection_items``answer_records``practice_sessions``vocabulary_progress` 等大表索引。
6. 对 dashboard、排行榜、销售转化、分佣报表这类聚合接口优先做日/小时预聚合,而不是无限放大 SQL 超时。
## 结果归档
本地生成的报告目录不入 Git。正式上云验收时可以把代表性结果摘要写入 `production-launch-evidence.json` 的本地证据文件,再运行:
```bash
npm run launch:gate
```
`launch:gate` 会强制检查真实数据只读压测和混合读写压测两条证据。只读证据用于基础上线门禁:
```json
{
"id": "performance.api-real-data-read",
"status": "pass",
"command": "PERF_START_SERVER=false PERF_API_BASE=https://api.example.com PERF_DURATION_SECONDS=300 PERF_CONCURRENCY=30 PERF_RAMP_SECONDS=30 PERF_INCLUDE_WRITES=false npm run perf:api:local > docs/refactor/launch-artifacts/api-real-data-read-benchmark.log",
"completedAt": "2026-06-30T10:48:00+08:00",
"artifact": "launch-artifacts/api-real-data-read-benchmark.log",
"summary": {
"errors": 0,
"errorRate": 0,
"p95Ms": 0,
"p99Ms": 0,
"concurrency": 30,
"durationSeconds": 300,
"includeWrites": false
}
}
```
上线门禁的最低机器阈值:
- `errors = 0`
- `errorRate <= 0.001`
- `p95Ms <= 300`
- `p99Ms <= 800`
- `concurrency >= 30`
- `durationSeconds >= 120`
- `includeWrites = false`
建议用摘要工具从 `perf:api:local` JSON 报告自动提取门禁字段,避免手工抄错:
```powershell
npm run perf:summary -- --input docs/refactor/performance-reports/api-benchmark-20260630-xxxxxx.json --json
```
工具会从 `summary.errors``summary.errorRate``summary.latencyOk.p95Ms``summary.latencyOk.p99Ms``config.concurrency``config.durationSeconds``config.includeWrites` 生成 `launchGateCheck.summary`,并按上线门禁阈值返回退出码。通过后,把 `launchGateCheck.summary` 转写到 `production-launch-evidence.json``artifact` 保留对应日志或报告路径。
写入混合场景默认不会通过只读门禁。如果只是做容量观察,可以显式允许写入并使用写入场景阈值:
```powershell
npm run perf:summary -- --input docs/refactor/performance-reports/api-benchmark-20260630-xxxxxx.json --json --allow-writes --min-duration-seconds=60 --min-concurrency=50 --max-p95-ms=500 --max-p99-ms=1200
```
使用 `--allow-writes` 时,摘要工具输出 `capacityObservation`,不输出 `launchGateCheck`;不要把写入场景误填进生产上线门禁的 `performance.api-real-data-read`
混合读写证据用于确认“创建练习 session -> 拉题 -> 答题 -> 交卷 -> 报告”的真实写入链路在上线容量基线内:
```json
{
"id": "performance.api-real-data-mixed",
"status": "pass",
"command": "PERF_START_SERVER=false PERF_API_BASE=https://api.example.com PERF_DURATION_SECONDS=120 PERF_CONCURRENCY=50 PERF_RAMP_SECONDS=15 PERF_INCLUDE_WRITES=true PERF_PRACTICE_FLOW_RATIO=0.1 PERF_AUTH_MODE=app_session npm run perf:api:local > docs/refactor/launch-artifacts/api-real-data-mixed-benchmark.log",
"completedAt": "2026-06-30T10:49:00+08:00",
"artifact": "launch-artifacts/api-real-data-mixed-benchmark.log",
"summary": {
"errors": 0,
"errorRate": 0,
"p95Ms": 0,
"p99Ms": 0,
"concurrency": 50,
"durationSeconds": 120,
"includeWrites": true
}
}
```
混合读写上线门槛:
- `errors = 0`
- `errorRate <= 0.001`
- `p95Ms <= 500`
- `p99Ms <= 1200`
- `concurrency >= 50`
- `durationSeconds >= 60`
- `includeWrites = true`
更高的 100/150 并发和更长时间压测仍应作为人工容量报告归档;门禁负责挡住基础读路径和刷题写入闭环明显不达标的情况。
证据中只记录报告路径、并发矩阵、P95/P99、错误率和结论不保存真实 token、支付密钥、用户隐私或完整响应。