16 KiB
API 压测与 4 核 16G 容量评估 Runbook
更新时间:2026-07-01
这份 runbook 用于在本地 Docker/Supabase 或未来 4 核 16G 云服务器上,对 SaaS 题库 API 做可重复压测。目标不是一次性给出永久容量承诺,而是建立一套可以随着题库、用户量、SQL 和硬件变化持续复跑的基线。
工具
压测脚本:
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:
$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 调参与运行证据:
npm run perf:postgres:evidence
该脚本会把关键 pg_settings、连接等待、缓存命中、大表大小和可选 pg_stat_statements Top SQL 输出到 docs/refactor/launch-artifacts/。调参前后各跑一次,配合 API 压测报告判断是否真正改善。
生产上线前必须用严格模式跑一次,并把摘要填入 production-launch-evidence.json 的 postgres.tuning-evidence:
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 时使用:
npm run perf:postgres:sql -- --profile=shared-host
角色旅程烟测:
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 引用。
前置条件
本地真实迁移库压测:
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 压测:
$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 已经在运行:
$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,并准备验证独立排行榜页或活动页容量时,才打开:
$env:PERF_INCLUDE_LEADERBOARD="true"
npm run perf:api:local
写入路径默认关闭。只有在专门的测试库或可丢弃预生产库中,才建议打开:
$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 受限资源压测入口:
$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。
$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 容器便于排查:
$env:PERF_KEEP_DOCKER_API="true"
npm run perf:api:docker-4c16g
Remove-Item Env:\PERF_KEEP_DOCKER_API
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 明显升高,按下面顺序排查:
- 查看报告的接口明细,先定位慢接口。
- 打开 PostgreSQL 慢 SQL 日志,或在生产启用
pg_stat_statements。 - 对慢接口涉及 SQL 执行
explain (analyze, buffers)。 - 检查 API/worker 连接池是否过大导致数据库活跃连接超过 4 核可承受范围。
- 检查
content_nodes path、question_collection_items、answer_records、practice_sessions、vocabulary_progress等大表索引。 - 对 dashboard、排行榜、销售转化、分佣报表这类聚合接口,优先做日/小时预聚合,而不是无限放大 SQL 超时。
结果归档
本地生成的报告目录不入 Git。正式上云验收时,可以把代表性结果摘要写入 production-launch-evidence.json 的本地证据文件,再运行:
npm run launch:gate
launch:gate 会强制检查真实数据只读压测和混合读写压测两条证据。只读证据用于基础上线门禁:
{
"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 = 0errorRate <= 0.001p95Ms <= 300p99Ms <= 800concurrency >= 30durationSeconds >= 120includeWrites = false
建议用摘要工具从 perf:api:local JSON 报告自动提取门禁字段,避免手工抄错:
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 保留对应日志或报告路径。
写入混合场景默认不会通过只读门禁。如果只是做容量观察,可以显式允许写入并使用写入场景阈值:
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 -> 拉题 -> 答题 -> 交卷 -> 报告”的真实写入链路在上线容量基线内:
{
"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 = 0errorRate <= 0.001p95Ms <= 500p99Ms <= 1200concurrency >= 50durationSeconds >= 60includeWrites = true
更高的 100/150 并发和更长时间压测仍应作为人工容量报告归档;门禁负责挡住基础读路径和刷题写入闭环明显不达标的情况。
证据中只记录报告路径、并发矩阵、P95/P99、错误率和结论,不保存真实 token、支付密钥、用户隐私或完整响应。