上篇博客讲了怎么把个人项目挂到公网:浏览器能登录、能上传、能问答,算是把主链路跑通了。
这篇主要是讲上线后的工程质量验收:护栏会不会拒、会不会误伤;异步索引是不是真在跑;慢的时候大概慢在哪。例子还是用我的开源项目 DocPaws,在 2 核 2G 演示机上做一轮。
先搞清请求怎么走,再按两条链路验收:
- 问答侧:准备脚本 → 验限流 → 抽延迟
- 上传/索引侧:上传 → ready → 改一处找瓶颈
一、一台机上,请求怎么走
- 问答:限流在真正开 SSE 之前就拦;拦不住才去调模型。
- 上传:接口很快返回,真正建索引的是后台 worker;页面变成 ready,才算这条链路完成。
为什么非要拦在开流之前?因为一旦已经出字,模型调用、连接占用都花出去了,超配额的请求还可能已经拿到内容——护栏等于白装。拦在前面,才是省成本、让限流真管用。
DocPaws 里常见配置(在后端.env):
| 含义 | 配置名(示例) | 演示机常用值 |
|---|---|---|
| 开不开限流 | CHAT_RATE_LIMIT_ENABLED | true |
| 每分钟最多问几次 | CHAT_RATE_LIMIT_PER_MINUTE | 日常20;测拒绝时临时改5 |
| 同时最多几路问答 | CHAT_CONCURRENT_LIMIT | 默认常2 |
改完.env一定要重启 API(DocPaws 用 systemd 时:sudo systemctl restart docpaws-api)。
二、问答侧验收:限流与延迟
这一侧共用同一套东西:登录态 + 固定题 + 小脚本。先准备好,再验限流、再抽延迟。
1. 准备
- 浏览器走通:登录 → 小 PDF 索引ready→ 能流式问答。
- 开发者工具里抄三样:
- Cookie 里的
session(登录态) - 知识库
kb_id - 一道固定问题
- Cookie 里的
为了做这轮验收,我让 AI 写了个小脚本(仓库里在eval/bench_chat_stream.py),对同一道题反复打问答接口:记首包(多久开始出字)和整段耗时(多久答完),撞上限流就拿 HTTP 429(Too Many Requests,表示「你问太勤,被拒了」)。用--mode切换几种验收场景就行——baseline抽延迟,burst/pace验限流拒与不误杀,verify改回日常频率后再确认。
2. 验限流:该拒则拒,正常节奏不误杀
为什么先验限流?它像小区门禁:太松,闲杂人员随便进,模型调用费可能被刷;太紧,业主自己也被挡在外面。
所以要验两件事——它们其实是同一个机制的两个相反失败方向:
- 拒绝(防漏):问太勤,该拦下来(常见是 HTTP 429)。漏了,护栏等于没装。
- 误杀(防滥):按正常人的节奏问,不该被拦。滥了,用户用不了,这肯定也不行。
临时把「每分钟次数」调严
编辑后端.env,例如改成每分钟只允许 5 次,然后重启 API:
sudosystemctl restart docpaws-api狂打:看会不会拒
本机:
python3 bench_chat_stream.py--modeburst我在演示机上的结果(频率=5,连打 18 次):前5次成功,后面13次 429。
说明:超配额会被拦——护栏是活的。
然后仍保持频率=5,但每次隔十几秒再问:
python3 bench_chat_stream.py--modepace我这边 8 次全成功、0 次 429。
说明节奏已经低于配额时,不会误伤。
改回日常频率,再验一遍
把.env改回例如20,重启 API,再跑:
python3 bench_chat_stream.py--modeverify几乎没有 429。
| 场景 | 频率 | 结果(我的演示机) |
|---|---|---|
| 狂打 | 5 | 18 次里 13 次 429 |
| 慢打 | 5 | 8 次全过,误杀 0 |
| 改回后再验 | 20 | 10 次全过 |
3. 抽一轮问答延迟
限流验完、频率回到日常后,再抽一轮体感基线——以后变慢了,才知道相对什么变了:
python3 bench_chat_stream.py--modebaseline脚本串行问二三十次,打出首包和整段的分位。首包(TTFT):多久出现第一条data:(感觉开始出字)。整段(E2E):多久答完,大头多半在模型生成。次数不多,看 P50 量级即可,别当 SLA;串行也说明不了多人同时问会怎样。
在测试时,我发现前二十几次还正常,到大约第 24 次附近脚本报 HTTP 500/系统繁忙。这和限流 429 不是一类事:429 是护栏在拦;这里是池子里借不到连接了。API 日志里能看到类似:
sqlalchemy.exc.TimeoutError: QueuePool limit of size 5 overflow 10 reached, connection timed out, timeout 30.00后面发现是问答路径把数据库连接握太久、没还干净——旧实现往往借一次就握到答完,等模型吐字也占着名额。串行都能打满,说明不是测太狠,是资源生命周期有问题。后来把后端改成短借短还,才能正常测完。
我这轮测的数据:25 次全过、无 429;首包中位约 42ms,整段中位约 11s。
三、上传/索引侧验收
1. 上传 → ready
为什么单独验这一条?上传接口返回「成功」和文档变成ready不是一回事——中间靠后台 worker。文件一直显示传输中,往往是异步链路没起来或任务挂了。
- SSH 确认 worker 在跑,上传前就挂上日志:
sudosystemctl status docpaws-workersudojournalctl-udocpaws-worker-f- 浏览器传一个不用太大的 PDF(大约 1~2MB 就够了)。
- 看两样:
- 页面/状态何时变成ready;
- 日志里是否出现任务
received→ 切块 / 向量 →succeeded。
可选再开一个窗口用htop瞄一眼内存 CPU。
我在 2C2G 上、约 2MB 样例:任务大约0.4~0.9 秒就成功,机器 Load 很低、内存大概用到 1G/1.57G 量级,没有明显积压。
2. 改一处再比:找瓶颈
只改一个配置项、同一路径前后对比,才能把快慢钉到具体原因上。我把 Celery--concurrency(同时能并行啃几条任务)从默认调到 4,再传差不多的小 PDF:
| 规模(chunks) | 改前 | 改后 |
|---|---|---|
| 约 11 | ≈0.98s | ≈0.75s |
基本持平。一次只传一个文件时,加 worker 数往往缩短不了这一单,时间主要花在切块和 embedding(文本转向量)上。
从而结论:瓶颈不在 concurrency,而在任务内 / 外部向量接口。
四、进阶做法(可选)
同时开几路问答(并发租约)
「租约」可以理解成:开一路问答就占一个名额,答完(或超时)再还回来。DocPaws 除了「每分钟次数」,还有「同时最多几路」。默认常常是 2。
同一账号同时开 3 路:应有人拿到 429(错误码里常见是CONCURRENT_LIMITED「同时开太多路」,和分钟桶的RATE_LIMITED「这一分钟问太勤」不同)。
三路都结束后马上再问一次:应能通过——说明名额有释放。
若只有「等很久再试」才恢复,那测的是过期兜底,不是「结束就释放」。两件事别混。
Redis 挂了会怎样
Redis 在这里常被限流拿来记「你已经问了几次 / 占了几路」。短停 Redis 再问:理想是明确失败(例如服务暂不可用),而不是限流悄悄失效、人人狂打。