Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HwMNZEzdzmrbPhuRNmZe3C
10 KiB
10 KiB
TASK · 内测容量:按用户限流、重计算满时短排队、10–20 并发冒烟压测(2026-10-14)
- 日期:2026-10-14
- 来源:产品 10-14 拍板「内测跑生产」;内测体检(运维一路,
scratchpad/beta-ops-20261014.md):重计算并发 2、满了立即 429;对话与校正没有按用户限流;仓库里没有任何并发压测结果。 - 执行方:Grok(Windows + Docker),分支
codex/beta-capacity-20261014,只推这个分支 - 验收:Claude(Linux)
- BUG 编号:开工时核对
docs/BUG_HISTORY.md最大号顺延(写作时最大号 BUG-1317;#56 sync7 从 1318 起占号,#58 已预留 BUG-1328,开工时以实际最大号为准) - 串行:本单 T1 改
frontend/src/lib/request-rate-limit.ts,#58(TASK-node-runtime-resources-20261013)也改这个文件 → #58 合入 staging 后再开 T1/T2。T4 压测基线要在 #57(TASK-rect-scoring-perf-20261013,校正打分提速)合入后再量,否则测的是旧的打分耗时。T0 可以先做。
1. 基线
- 基线 commit:
origin/staging48a2fadd(开工时以最新 staging 为准,进度记录写明)。 - staging 主机
118.26.111.127,生产118.194.235.34(2 vCPU / 4 GB,deploy/README.md);T0 先核对 staging 规格是否与生产相同,不同时压测结论要按比例说明,不得直接套到生产。
2. 事故实证(行号按符号定位)
| 位置 | 现状 |
|---|---|
scripts/api_heavy_compute_gate.py |
进程级 BoundedSemaphore,DEFAULT_CONCURRENCY = 2,HeavyComputeBusy 抛出后服务器立即回 429 ERR_COMPUTE_BUSY + Retry-After: 2,不排队。HEAVY_COMPUTE_PATHS 含 /api/consultation_workflow(普通对话每轮取证)和全部校正打分端点:对话与校正抢同两个位 |
scripts/jyotish_api_server.py POST 分发 |
acquire_heavy_compute_slot(path);只有 consult_card_warm_only 预热不占位 |
frontend/src/mastra/consultation-workflow.ts httpFailureCode |
429 → workflow_rate_limited,没有按 Retry-After 重试,这一轮对话直接失败 |
frontend/src/lib/rectification-agentic/v9/engine-client.ts |
校正已按 busy 读 Retry-After 退避(BUG-723),不在本单改动范围 |
frontend/src/lib/request-rate-limit.ts requestRateLimits |
只有 locationSearch / locationTimezone / dailyStarlanguage / ephemeris / synastry / sessionWrite 六个桶;/api/consult、校正回合、报告生成都没有按用户限流(报告只有每日上限 PERSONAL_REPORT_DAILY_LIMIT,默认 5) |
| 压测 | 仓库只有 6 月的压测计划与 9 月单会话容量 / 首屏性能记录,没有并发结果 |
推算:内测 20–50 人,只要同一时刻有 3 个以上请求在跑取证或校正打分(单次 2–5 s,#57 之后目标 < 2 s),第三个人的对话就失败。
3. 根因
重计算闸当初是为防止 2 vCPU 被校正扫描打满而设的「宁可拒绝」策略(见文件头注释),后来普通对话的取证也进了同一个闸,但对话侧没有任何等待或重试;按用户限流只给了轻量接口。
4. 决策记录(产品 2026-10-14)
- 内测跑生产(产品 10-14)。容量问题必须在内测前处理,不能只靠「人少」。
- 重计算并发上限保持 2(主机 2 vCPU,不放大);改为「满了先等一小会儿」,等不到再明确告诉用户稍后再试。
- 对话、校正回合、报告生成加按用户限流;阈值见 §6 建议值,产品可改,写进
requestRateLimits常量,环境变量不新增(减少配置面)。 - 内测发点数用兑换码(
redemption_codes,一码一次)当邀请码;内测期间不开支付(EPAY_CHAT_ENABLED=false保持)。每码点数由产品定(§10)。 - 压测在 staging 做,不压生产;登录用 staging 测试邮箱 + 固定验证码(产品在会话里提供),脚本从环境变量
STAGING_TEST_OTP读,不得写进任何文件、日志或提交。
5. 硬红线
- 不改生时校正打分、
decision_policy、闸门与题库;不改校正侧 BUG-723 的退避逻辑(服务端排队后它自然少碰 429)。 scripts/jyotish_api_server.py不得增长(tests/test_api_server_growth_contract.py):排队逻辑写在scripts/api_heavy_compute_gate.py内,主文件只允许改调用处一两行。- 排队有上界:等待者数量与等待时长都有上限;等不到仍回 429 +
Retry-After,不得无限挂起请求线程。健康检查与轻量路由不得进闸(文件头约定保持)。 - 限流放行 / 拒绝语义与
retryAfterSeconds计算沿用consumeRequestRateLimit;不改 #58 加的过期清理。 - 前端红线(AGENTS §7):tsc 0、lint 0 error、npm test 失败名单与基线逐名相同、build
/Static、首屏 gzip ±2%;新文案对照frontend/docs/VOICE.md,不写「系统繁忙」「请稍后重试」这类客服腔,参照现有「这边没接上……」口吻。 - 压测不得读写真实用户数据;只用测试账号;压测结束清理测试账号会话(或写明保留原因);模型调用花费写进进度记录。
- 不改数据库结构(本单不需要迁移);不改 workflow;不推 staging。
6. 任务分解
| 项 | 内容 | 验收 |
|---|---|---|
| T0 | 核对 staging 与生产主机规格(vCPU、内存、JYOTISH_HEAVY_COMPUTE_CONCURRENCY 实际值);在 staging 用现有代码跑一次 T4 的压测作为改前基线(若 #57 未合入,记清楚) |
进度记录有规格对照与改前数字 |
| T1 | 按用户限流:requestRateLimits 新增 consult(10 次 / 分钟、120 次 / 小时两档,用两个桶)、rectificationTurn(5 次 / 分钟、40 次 / 小时两档)、reportGenerate(建议 3 次 / 小时,与每日 5 份并存)。接入 /api/consult、校正回合路由(frontend/src/app/api/rectification/agent/route.ts,开案 cases/open 不计入)、报告生成 frontend/src/app/api/reports/route.ts POST。超限回 429 + code: "rate_limited" + Retry-After,在扣点 / 预留之前判断,不扣点 |
单元测试:各桶边界放行 / 拒绝、拒绝时不调用扣点 RPC;路由测试断言 429 体与头 |
| T2 | 重计算闸短排队:acquire_heavy_compute_slot 改为带超时等待(建议最多等 8 s,等待者上限 = 4),超时或等待者满仍抛 HeavyComputeBusy;Retry-After 不变。前端 /api/consult 对 workflow_rate_limited 按 Retry-After 最多重试 2 次、总等待 ≤ 15 s,期间进度行显示一句「人有点多,排一下队」(文案进 VOICE);仍失败时用一句话说明并退回本轮预留点数(沿用现有失败结算路径,核实不重复退款) |
Python 测试:并发 3 个请求,第 3 个在前一个释放后拿到位;等待者满时立刻 429;超时 429。前端测试:429 → 重试 → 成功;三次都 429 → 失败文案 + 退点(复用现有结算断言) |
| T3 | 内测点数上限:核实兑换码表能否设单码点数与批量生成(后台「兑换码」页),写一页操作说明 docs/operations/beta-invite-codes.md:怎么批量生成邀请码、每码点数、怎么查谁用了、怎么作废。不写真实码 |
文档;若后台缺批量生成或作废能力,写进进度记录与 BLOCKED,不在本单扩功能 |
| T4 | 冒烟压测脚本(放 frontend/scripts/research/beta-load-smoke.mts,不进镜像,确认不被 frontend/src 引用——参照 BUG-1140 / runtime-sources-no-test-imports.test.ts):N 个虚拟用户(10、20 两档),每人:OTP 登录 → 首轮咨询 → 一次追问;其中 3 人改为开一次校正并答 2 题。记录:每请求状态码、首字时间、整轮完成时间、429 比例;同时在服务器侧采样 API / web 容器内存与 CPU(docker stats 每 5 s,执行方没有 staging SSH 时写成环境缺口,只报客户端侧) |
进度记录给改前 / 改后两组数字。通过线(10 并发):429(最终失败,不含重试成功)≤ 2%;5xx = 0;咨询首字 P95 ≤ 15 s、整轮 P95 ≤ 90 s;API 容器内存峰值 < 1.5 GB。20 并发只记录不设门,用来定内测同时在线上限 |
| T5 | 文档:BUG_HISTORY(每项一个号)、CHANGELOG、VOICE(排队与限流文案)、DESIGN(进度行状态)、docs/testing/beta-capacity-20261014.md 真机两条(快速连发触发限流的提示;排队提示出现与消失) |
齐 |
7. 让步顺序
T2 后端排队不可让;T1 不可让(防刷与成本)。T2 前端重试文案可简化为不显示「排队」而只多等。T4 20 并发档可让;10 并发档不可让(没有数就不能拉人)。T3 文档可让为「后台已有能力清单」。
8. 验收门
- Python:
ruff、py_compile、新测试、tests/test_api_server_growth_contract.py、快速门全量(failures []或与基线同名)。 - 前端:tsc / lint / npm test(与基线逐名比对,看
# cancelled与退出码)/ build Static / gzip。 - 压测:脚本可在 Claude 侧(Linux)复跑 10 并发一次,数字量级一致。
9. 开工前置
git fetch origin --prune
git status -sb
git worktree add -b codex/beta-capacity-20261014 .worktrees/beta-capacity-20261014 origin/staging
# 确认 #58 已合入 staging(request-rate-limit.ts 有过期清理)再开 T1/T2;T0 可先做
10. 需产品提供 / 拍板
- 限流阈值:对话 6 次 / 分钟 + 60 次 / 小时、校正 10 次 / 分钟、报告 3 次 / 小时(+ 每日 5 份)——同意或改数。
- 排队上限:最多等 8 s、最多 4 人排队——同意或改。
- 每个邀请码给多少点;内测第一批多少人(压测 20 并发的结果出来后再定同时在线上限)。
- 压测会花模型调用费用(约 20 人 × 2 轮 × 两次),同意在 staging 跑。
- 若 Grok 侧拿不到 staging 主机 SSH,服务器内存 / CPU 采样由谁跑(Claude 也没有主机权限时写成环境缺口)。
11. 产品答复(2026-10-14,覆盖上文对应建议值)
- 限流:产品要「对话多一些、校正少一些」→ 对话 10 次 / 分钟 + 120 次 / 小时;校正回合 5 次 / 分钟 + 40 次 / 小时;报告 3 次 / 小时(+ 每日 5 份)不变。T1 表格已按此改。
- 压测花模型费:同意(staging 10 / 20 并发各一轮,按 T4)。
- 其余(排队长度与等待时间、每码点数、首批人数)按任务书建议默认值执行,产品后续可改。