本页目录
Lesson 11 — 压测与并发:找到你的 QPS 天花板
本课目标:用压测工具量化 kb-qa 的并发能力和瓶颈,理解异步/并发对 LLM 服务吞吐的影响,用并发信号量做保护。
学完你能回答面试官那句:「你这服务能扛多少并发?瓶颈在哪?」——不是拍脑袋,是压出数据来。
1. 为什么要压测?
你以为服务上线就能扛流量,但「能跑」和「能扛」之间差一个压测:
| 不压测的后果 | 实际发生 |
|---|---|
| 🚫 不知道 QPS 天花板 | 突然来 100 并发就全超时,你才知道只能扛 10 |
| 🚫 不知道瓶颈在哪 | 慢了,是检索慢?生成慢?还是上游限流?两眼一抹黑 |
| 🚫 没有容量规划依据 | 运营问「能扛双 11 吗」你答不上来 |
🎯 核心认知:压测是上线前的必经环节——用数据回答「能扛多少、慢在哪、何时该扩」。这和 L03 线上评估一样,是把「感觉」变成「数字」。
2. 压测核心指标:QPS / P50 / P95 / P99
压测产出一组指标,理解它们是看懂压测报告的前提:
| 指标 | 含义 | 怎么读 |
|---|---|---|
| QPS (Queries Per Second) | 每秒处理请求数 | 吞吐能力。QPS 上不去就是瓶颈 |
| P50 | 50% 请求快于这个延迟 | 中位数体验,「正常情况下多快」 |
| P95 | 95% 请求快于这个延迟 | 生产关键指标——大部分用户的体验下限 |
| P99 | 99% 请求快于这个延迟 | 长尾,「最慢的那 1% 多惨」 |
| 错误率 | 失败请求占比 | 压垮的信号——超并发后错误率飙升 |
延迟分布(为什么看 P95 不看平均):
假设 100 个请求:99 个 1s,1 个 100s
平均延迟 = (99×1 + 1×100)/100 = 1.99s ← 被一个长尾拉高,看不出问题
P50 = 1s ← 一半用户秒回
P95 = 1s ← 95% 用户秒回
P99 = 100s ← 但有 1% 用户等了 100s!
💡 为什么 P95 比 P50/平均重要? 因为平均会被长尾拉偏,P50 看不出尾部问题。生产 SLA 通常用 P95/P99(「95% 请求 < 2s」)。压测盯 P95——它是大部分用户的真实体验下限。
3. LLM 服务的瓶颈在哪?(通常不是本地 CPU)
传统 Web 服务压测,瓶颈常在本地 CPU/数据库。LLM 服务不一样——瓶颈几乎总在上游 API 限流:
一次 /api/ask 的耗时构成:
┌──────────────────────────────────────────────┐
│ 检索(BM25+向量+rerank) ~300-500ms │ ← 本地计算 + rerank API
│ 生成(glm-4 流式) ~1500-3000ms │ ← 智谱 API(主要瓶颈!)
└──────────────────────────────────────────────┘
总 ~2-3.5s,其中 70%+ 在等智谱 API
所以 kb-qa 的 QPS 天花板主要取决于智谱 API 的并发限额,而不是本地机器。压测时会看到:并发加到一定值,错误率突然飙升(智谱返 429 限流)——那就是天花板。
🎯 这个认知很值钱:很多团队压 LLM 服务,发现 QPS 上不去就去优化本地代码(多线程/缓存查询),但瓶颈在上游 API,本地优化无效。正确做法是:本地用并发信号量限流保护,避免打爆上游被全面封禁。
4. 并发信号量:保护上游 API
既然瓶颈在上游,就该主动限流——用信号量(Semaphore)控制同时在飞的请求数,避免打爆智谱 API:
# 同时最多 N 个请求在调智谱,超了的排队/快速拒绝
sem = asyncio.Semaphore(max_concurrent)
async def ask_with_limit(q):
async with sem: # 拿到令牌才调
return await call_zhipu(q)
research-assistant 已经用了这个模式(tools.py 的 _search_semaphore 限 DDGS 并发)。kb-qa 同理——给生成层加信号量,超过并发上限的请求快速返回 429/排队,而不是全堆到智谱被全面限流。
💡 信号量 vs L04 的限流器区别:L04 的限流是「按 key 每分钟 N 次」(防滥用,应用层);信号量是「同时最多 N 个在飞」(防打爆上游,资源层)。两者互补:限流挡恶意用户,信号量保护后端资源。
5. 压测方法:本课为什么自实现而非用 locust?
任务书提到 locust,但本课选择自实现一个 asyncio 压测器,原因:
- 零新依赖:locust 引入 gevent/flask 等重依赖;自实现用 kb-qa 已有的 httpx + asyncio
- 教学优先:压测本质就是「并发发请求 + 统计延迟」,自己写一遍才懂 P95 怎么算
- 可立即跑:不用
pip install locust,脚本直接出报告 - 生产可替换:理解原理后,README 给 locustfile.py 对照(一行
locust起可视化)
和 L04 自实现限流器、L10 自实现缓存一个思路:关键能力先手写理解,再用库提效。
6. 本课代码会做什么
code.py(教学,零依赖)
- 实现压测器核心:并发发请求 + 收集延迟 + 算 QPS/P50/P95/P99/错误率
- 用 mock server(本地起一个延迟随机的服务)演示压测,看清指标怎么算、瓶颈怎么暴露
落地到 kb-qa:loadtest/
run_loadtest.py:自实现的 asyncio 压测器,压 kb-qa 的/api/ask(带鉴权头),产出基线报告locustfile.py:locust 版本(生产对照,需pip install locust)REPORT.md:压测报告模板 + 解读
7. 跑起来
教学代码(零依赖,mock server)
cd ops-lessons/11_loadtest
python code.py
预期:起一个 mock server,用不同并发数压它,打印 QPS/P50/P95/P99,演示「并发加到某值后延迟飙升/错误率上升」的拐点。
落地压测(kb-qa,需先起服务)
cd portfolio-projects/knowledge-base-qa
# 1) 先起服务(配好 API key + 入库)
python -m uvicorn api.main:app --port 8001 &
# 2) 跑压测(带 API key,无 key 时 AUTH_ENABLED 效果见 L04)
python loadtest/run_loadtest.py --base-url http://localhost:8001 --concurrency 1 5 10
# → 打印不同并发下的 QPS/P95/错误率,找到天花板
🎯 面试话术
「我用自实现的 asyncio 压测器压出了 kb-qa 的 QPS 基线和 P95 延迟曲线,定位到瓶颈在智谱 API 限流而非本地 CPU——并发加到一定值错误率就飙升。所以我用并发信号量给生成层做了保护,超过上限的请求快速拒绝而非全堆到上游被全面封禁。压测盯 P95(不是平均),因为平均会被长尾拉偏,P95 是大部分用户的真实体验下限。」
落地清单
| 文件 | 内容 | 如何验证 |
|---|---|---|
loadtest/run_loadtest.py |
新增:asyncio 压测器(httpx 并发 + P50/P95/P99/QPS/错误率) | python loadtest/run_loadtest.py --help |
loadtest/locustfile.py |
新增:locust 版本(生产对照) | locust -f loadtest/locustfile.py(需装 locust) |
loadtest/REPORT.md |
新增:压测报告模板 + 解读 | — |
下一课 Lesson 12 — 成本/质量权衡 用评估管线把模型选型变成数据决策。