「平均响应 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《接口工作台:组织请求、环境与变量》,我们把请求收进能切换环境、能传参数的集合。