Files
gongxue-base/docs/refactor/performance-benchmark-runbook.md

235 lines
11 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-06-30
这份 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` 忽略,不应提交。
PostgreSQL 调参与运行证据:
```bash
npm run perf:postgres:evidence
```
该脚本会把关键 `pg_settings`、连接等待、缓存命中、大表大小和可选 `pg_stat_statements` Top SQL 输出到 `docs/refactor/launch-artifacts/`。调参前后各跑一次,配合 API 压测报告判断是否真正改善。
角色旅程烟测:
```bash
npm run smoke:launch-persona
```
该脚本从普通学生、租户管理员、平台管理员三个视角调用真实 API覆盖 SVIP 后刷题、收藏、错题复习入口、租户 dashboard/主题/学生/销售转化、平台租户/套餐/审计入口和越权拒绝。它会写入少量 `launch_persona_smoke` 测试练习与收藏记录,只建议在本地、预生产或灰度租户运行。
## 前置条件
本地真实迁移库压测:
```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
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` | 单请求超时 |
## 默认工作负载
默认混合读路径包含:
- `/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 起步值,再跑以下矩阵。每轮之间间隔 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 和真实网络重新跑。
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`
更高的 50/100 并发、写入混合场景和容量结论仍应作为人工容量报告归档;门禁只负责挡住明显不达标的基础读路径。
证据中只记录报告路径、并发矩阵、P95/P99、错误率和结论不保存真实 token、支付密钥、用户隐私或完整响应。