TT Lab
开始
学习 学习路径 课程

压力测试

拿到 200 并不等于真的干了活

在 TT Lab 中继续学习

一句话总结

压力测试的数字,只有在测试自己验证过测的是什么时,才有意义。只看状态码的测试,即使每秒产生 3000 个错误页面,也会说“通过”。

为什么需要它

新支付 API 的压力测试结果摆到了会议室里。每秒 3,012 次,p95 为 8 毫秒,错误率 0%。没有人提出异议,当周就发布了。然后发布 30 分钟后,支付就停了。

原因出在测试一侧。负载生成器发出的请求没有认证头,而网关对没有认证的请求,返回了 200,并附带 {"status":"error","message":"upstream unavailable"}。这是因为旧的前端处理不了 4xx,所以才这样设置的。负载生成器只看状态码,于是把 30 万次全部算作了成功。

数字之所以好看,也是同一个原因。错误路径既不访问数据库,也不经过队列,所以很快就结束。因此,混进的错误越多,平均值和百分位数就越好看。测试坏掉的信号,不是“变慢了”,而是以“变快了”的形式到来。这是压力测试中最常踩的地雷。

工作原理

负载生成器只知道三件事——发送了多少个请求、响应的状态码是什么、响应花了多长时间到达。hey 的摘要所显示的 Status code distribution,正好就是这些。响应体是什么,服务器是否真的做了工作,生成器是无从知道的。

所以必须亲自给测试加上验证层。三样要一起看。

层 检查内容 遗漏时的后果
状态码 非 2xx 的响应有多少 把 502 风暴读成“扛住了负载”
响应体 响应是否真的是预期的样子 把装在 200 里的错误算作成功
响应数 服务器统计的请求数与生成器统计的响应数是否相同 中途断开的请求从统计中消失

k6 把这一层以 checks 的名称放进了标准功能,并且可以把结果挂到 thresholds 上,变成退出码。hey 没有这样的功能,所以必须由人亲手加上。选择工具的标准在这里又多了一条——有没有验证响应的位置。

加上验证并没有结束。测试条件是否能代表生产,也必须另外追问。有三处特别容易出现偏差。

连接复用。hey 默认会复用 TCP 连接。加上 -disable-keepalive,每个请求都会打开新连接。并发度为 10、发送 200 次时,前者是 10 个连接,后者是 200 个。如果生产的客户端使用连接池,而测试却每次都新开连接,那么测试测量的就不是服务,而是 TCP 握手和防火墙状态表。

响应大小。如果测试用的响应是 1KB,而生产的响应是 64KB,即使每秒请求数相近,每秒字节数也会相差 64 倍。对于带宽或序列化是瓶颈的服务,这个差别就直接成了错误的容量估算。吞吐量不仅要看每秒请求数,也要看每秒字节数。

缓存键。如果只反复请求同一个 URL,第一次请求之后就全部是缓存命中。用命中率 99.5% 的测试结果,去确定命中率 20% 的生产容量,缓存后面的数据库在测试中一次负载也没受过,就这样被部署上去了。

在现场相遇的样子

最常见的样子是“测试结果比生产好”。而原因几乎总是测试做的事比生产更容易。跳过了认证、只读了同样的数据、响应很小,或者走进了错误路径。

也有相反的方向。某个团队因为测试结果只有生产的三分之一,就把服务器增加到了三倍。后来才发现,负载生成器是在关闭了连接复用的状态下运行的。增加的服务器费用,原封不动地留在了账单上。

第三种样子更安静。测试运行了好几个月都很正常,某一天开始结果变好了。通常是在这期间,对象一侧有什么东西变了——认证方式改变,测试用的令牌过期了;路径被移动,路由器返回的是默认响应;功能开关(feature flag)关闭,实际计算被跳过了。测试没变,而对象开始做另一件事,如果没有验证层,这种变化会被记录为“性能得到了改善”。所以验证不是加上一次就完事,而是必须与测试一起持续运行。

因此在压力测试报告中,有一件事必须先于数字写下来。这个测试验证了什么,没有验证什么。只要有一项没有验证,那个数字就既不是上限,也不是下限。而且这种验证不应是由人在读报告时确认,而应留在脚本的退出码里——因为人不会怀疑好的数字。

下一项实验要做什么

启动一个状态码始终是 200、但每三次就有一次响应体是错误的对象,先确认只看 hey 的摘要,就显得完美无缺。然后用服务器留下的访问日志,统计实际发出的是什么,并用中位数确认错误响应比正常响应更快。开关连接复用,看连接数分化成 10 个和 200 个;把响应大小改为 1KB 和 64KB,比较每秒字节数;用固定的键和轮换的键,看缓存命中率分化成 99.5% 和 0%。最后制作一个检查这三项的检查清单脚本,并让评分器用它自己制作的输入把这个脚本运行四次,确认通过和失败两种情况。