forked from wangziqi/gongxue-base
235 lines
11 KiB
Markdown
235 lines
11 KiB
Markdown
# 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 小于 300ms,P99 小于 800ms,错误率小于 0.1%。
|
||
- 加入少量 session 写入:P95 小于 500ms,P99 小于 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、支付密钥、用户隐私或完整响应。
|