接口性能 + AI 压测分析

「平均响应 143 毫秒,性能良好,可考虑上生产」------这句话有三个问题,其中一个是数字直接算错了。

而这三个问题背后是同一件事:「一个数」代表不了「一批请求」。

配套脚本在 examples/lab/day30/,pytest 3 passed。

📊 平均值会骗人

20 次请求,19 次 20 毫秒,1 次 2000 毫秒:

text 复制代码
平均值 :119.0 毫秒
中位数 :20.0 毫秒
P95  :119.0 毫秒
P99  :1623.8 毫秒
最慢那一个:2000 毫秒

平均值 119,P99 是 1624。

看平均值的报告,你会说「119 毫秒,没问题」。但那 1% 的用户等了 2 秒,而投诉的正是他们。

数字 它代表谁
平均值 一半用户比它快,一半比它慢
中位数 中间的那个用户
P95 100 个里 95 个比它快
P99 100 个里 99 个比它快
最大值 最倒霉的那个

用户不会感受到「平均」,他感受到的是「我最慢的那一次」。 statistics 的用法见 Python 官方文档,分位数与 GIL 的说明见 Python 官方术语表。

判断平均值能不能用

能用:所有请求耗时都差不多。

不能用:分布很散(大部分很快,少量很慢)。

判断方法:看 P95 和平均值的差距。 差得远说明有长尾,平均值就不代表任何人。

这个判断值得固化成一条断言:

python 复制代码
assert 分位点[94] <= 分位点[98]        # 分位数不能倒挂
assert 分位点[98] < 平均 * 2           # 有长尾时,平均值不可信

分位数的下标容易记错

python 复制代码
分位点 = statistics.quantiles(耗时, n=100, method="inclusive")
print(分位点[94])   # P95
print(分位点[98])   # P99

quantiles 返回的是「切分点」不是「分位数本身」。 记错下标,P95 和 P99 会整个反过来,而你不会发现------因为数值看起来都很合理。

🔢 「并发用户数 50」这句话没有信息量

概念 含义
并发用户数 同时有多少人在用
请求速率 每秒钟进来多少个请求

同一个实验,同一个并发数:

text 复制代码
20 个人同时点          →  36.8 请求/秒
20 个人每隔 50 毫秒点  →  19.6 请求/秒

并发数一样,速率差了近一倍。

所以看一份压测报告,「并发用户数 50」这个数字,你还得追问:每个用户多久点一次?

服务端能不能扛并发,是另一回事

让每个请求在服务端花 50 毫秒:

text 复制代码
单线程服务端:总耗时 1.581 秒,最慢那个请求 1559 毫秒
多线程服务端:总耗时 0.101 秒,最慢那个请求   69 毫秒

两段服务端代码完全一样,只差一个能不能同时处理多个请求。

单线程那个,20 个请求排着队。最慢的用户等了 1.5 秒,而服务端自始至终只忙着一个请求。

这就是「我的接口很快,但压测一上并发就崩」的来源------快的那次是单并发测的。

压测脚本本身可能是瓶颈

我第一版实验没加延迟,两个服务端结果几乎一样(36.9 vs 35.7 请求/秒)。

原因不在服务端,在客户端。 Python 的 GIL 让同一时刻只有一个线程在跑 Python 代码,20 个客户端线程自己就串行了。

压测工具和被压服务,最好别在同一个进程里。 不然你测的是自己的压测脚本。

🤖 AI 读压测报告最容易错的四处

markdown 复制代码
1. 本次压测并发用户数 50,总请求数 500,系统表现良好。
2. 平均响应时间约 143 毫秒,处于可接受范围。
3. 有 12 个请求失败,错误率约 2.4%。
4. 建议:接口性能可以满足当前业务需求,可考虑上生产。

实算:平均 100.2、P99 921、最大 3000、速率 40 请求/秒、错误率 2.4%。

第 1 条:数字对,但没信息量。

第 2 条 :数字就错了,实算 100.2。它大概拿「总耗时 ÷ 请求数」算的。而「处于可接受范围」这个判断,它没有你的 SLA。

第 3 条:数字对,但没说那 12 个错误全是 500。500 是服务端崩了,不是「有点慢」。

第 4 条:最危险。压测报告不能作为上生产的依据------数据是合成的、压测脚本和被压服务同机、没有真实数据分布、没有算上下游。

所以给 AI 的正确做法是:把原始数据给它,让它算,不要让它总结。

📌 压测报告该给什么

不是一个数,是一组:

text 复制代码
请求速率(每秒多少个)
P50 / P95 / P99 / 最大值
错误率,并且按类型拆开(5xx / 4xx / 超时 / 连接失败)

AI 天然倾向于给你一个数,因为一句话比一张表好写。 这就是它最容易出错的地方。

🧪 把「报告该有一组数」做成检查

「报告写全了」这件事,也可以自动化:

python 复制代码
必需要有的 = ["速率", "P50", "P95", "P99", "最大值", "错误率"]
必须拆开的 = ["5xx", "4xx", "超时"]


def 查报告缺什么(报告):
    缺 = [项 for 项 in 必需要有的 if 项 not in 报告]
    没拆 = [项 for 项 in 必须拆开的 if 项 not in 报告]
    return 缺, 没拆

这个检查对 AI 的输出也管用------把它的总结当成一份报告来查,缺什么一眼看到。

📌 一个必须说清的边界

本文所有数字都是本机合成数据,不能作为容量结论。三个原因:

  • 压测脚本和被压服务在同一台机器上,客户端和服务端抢 CPU
  • 数据是造的,真实业务的数据量和分布完全不同
  • 假服务没有数据库、没有缓存、没有下游依赖

真实压测的结论只有在真实环境、真实数据、真实依赖下才成立。

写报告时把这句写上,不是谦虚,是防止有人拿它当依据。

🔍 一个容易被忽略的细节:分位数下标

python 复制代码
分位点 = statistics.quantiles(耗时, n=100, method="inclusive")
print(分位点[94])   # P95
print(分位点[98])   # P99

quantiles 返回的是「切分点」不是「分位数本身」。 记错下标,P95 和 P99 会整个反过来,而你不会发现------因为数值看起来都很合理。

配一条断言兜底:

python 复制代码
assert 分位点[94] <= 分位点[98]
assert 分位点[98] < 平均 * 2      # 有长尾时,平均值不可信

🔬 压测里还有三个常被忽略的量

一、错误率和延迟要分开看。 一个接口可以「很快地失败」------10 毫秒就返回 500。延迟漂亮,业务是崩的。错误率必须和延迟一起报。

二、吞吐和延迟会互相牵制。 同样的机器,延迟降了吞吐可能没涨;也可能吞吐涨了延迟也涨了,因为服务在排队。只看一个都会得出错误结论。

三、连接池和线程池也是瓶颈。 客户端开 200 个线程、服务端只允许 100 个连接,多出来的 100 个全在等连接。很多「压测很慢」的结论,其实是连接池满了。

📊 一份报告该有的样子

text 复制代码
环境:单线程/多线程、CPU 核数、Python 版本
负载:并发数、每个用户的请求间隔、总请求数
速率:实际每秒多少请求(不是并发数)
延迟:P50 / P95 / P99 / 最大值
错误:总数 + 按类型拆开(5xx / 4xx / 超时 / 连接失败)
结论:能支撑多少请求/秒,以及这个结论在什么条件下成立

最后那行「在什么条件下成立」最容易被省掉,也最该写。 压测结论换个环境就不一定成立,把条件写清楚,别人才知道能不能拿它当依据。

🧾 一个可以立刻用的检查清单

text 复制代码
□ 压测脚本和被压服务在不同进程/机器上吗?
□ 报的是「实际速率」还是只有「并发数」?
□ 延迟报的是 P95 / P99,还是只有平均值?
□ 错误按类型拆开了吗?(5xx / 4xx / 超时 / 连接失败)
□ 慢请求和错误请求混在一起算平均了吗?
□ 结论里写清楚「在什么条件下成立」了吗?

第三、第四条最常漏,而它们恰好最能说明问题。

第五条值得单说:超时的请求不该混进延迟分布里。 如果脚本把超时记成「耗时 30000 毫秒」,平均值会被彻底带偏,超时应该单独统计。

🔍 一个容易混淆的:慢请求和错误请求

最后说一个实操细节。

压测脚本里,超时的请求该怎么记?

python 复制代码
# 错的:把超时也当成「耗时 30000 毫秒」记进去
try:
    响应 = requests.get(url, timeout=30)
    耗时列表.append((time.perf_counter() - 开始) * 1000)
except requests.exceptions.Timeout:
    耗时列表.append(30000)      # ← 这样平均会被彻底带偏

超时应该单独统计,不该混进延迟分布里。 因为超时是「没等到」,不是「等了很久」,两者的含义完全不同。

正确做法是分开记:

python 复制代码
# 对的:超时单独计数,不进耗时列表
超时次数 = 0
try:
    响应 = requests.get(url, timeout=30)
    耗时列表.append((time.perf_counter() - 开始) * 1000)
except requests.exceptions.Timeout:
    超时次数 += 1

这样 P99 才是 P99,超时率是超时率。 混在一起的话,报告里那个「P99 = 30 秒」就分不清是「真的慢」还是「超时了」。

🧮 错误率的分母,要写清楚

text 复制代码
分母 = 总请求数         → 2.4%
分母 = 成功的请求数     → 2.5%

错误率本来就低的时候,这个差别会被放大。而且很多报告只写「错误率 2.4%」,不写分母,读者只能猜。

建议直接写成「错误率 = 12 / 500 = 2.4%(分母为总请求数)」。多写七个字,少一次扯皮。

🔭 下一步

Day31《接口工作台:组织请求、环境与变量》,我们把请求收进能切换环境、能传参数的集合。

相关推荐
彭亚川Allen1 小时前
985 在职研究生到底怎么样?是不是水硕?是不是.....?
前端·后端·程序员
陈随易3 小时前
副业搞钱点子之独立开发
前端·后端·程序员
挖掘狂人4 小时前
Vue nextTick 到底在等什么?一篇讲透原理与那些年踩过的坑
javascript·程序员·编程语言
codigger4 小时前
Vue nextTick 到底在等什么?一篇讲透原理与那些年踩过的坑
程序员·vue·编程·异步更新·#人工智能
LV技术派5 小时前
【成为 AI Native Builder · 01】开篇:为什么 AI 总在返工?
人工智能·程序员·ai编程
深蓝AI14 小时前
总榜第一不等于最划算:Intelligence Index v5 换代后,模型选型要算的三笔账
程序员·架构
陈随易17 小时前
下载神器!Motrix 停更三年后重写回归
前端·后端·程序员
喜欢吃豆20 小时前
我把开源数字人项目升级了:xingyun3D 不只会念台词,还能陪你聊天、练面试
程序员
深蓝AI1 天前
Epic 摊牌了:Claude 和 Codex 在写 UE6 的代码,十年没修掉的 bug 被 AI 扫光
程序员·ai编程