Agent Engineering 课程阅读
首页/课程五 · LLMOps 生产运维/压测与并发:找到你的 QPS 天花板

在 GitHub 查看原文

本页目录

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 压测器,原因:

  1. 零新依赖:locust 引入 gevent/flask 等重依赖;自实现用 kb-qa 已有的 httpx + asyncio
  2. 教学优先:压测本质就是「并发发请求 + 统计延迟」,自己写一遍才懂 P95 怎么算
  3. 可立即跑:不用 pip install locust,脚本直接出报告
  4. 生产可替换:理解原理后,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 — 成本/质量权衡 用评估管线把模型选型变成数据决策。