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

压力测试

平均每秒 100 个请求说明不了负载

在 TT Lab 中继续学习

一句话总结

“每秒 25 次”只说明了一半负载。即使平均值相同,到达是均匀还是集中,队列和延迟也会不同,而在闭环中,甚至根本无法设定请求速率。

为什么需要它

容量估算通常是这样开始的——峰值是每秒 25 次,每次需要 40 毫秒,所以需要的 worker 是 25 乘以 0.04 得 1 个。宽裕一点,放 4 个。计算没有错。可是一部署,队列就飙升到几十,p95 跳高了十倍。

错的不是算术,而是假设。那个计算假设请求每隔 40 毫秒均匀地来一个。真实流量并不是这样到来的。由人使用的服务,会在一条通知之后集中涌入;由机器使用的服务,会因为 cron 在整点醒来而集中涌入。平均值相同,瞬间的并发请求数却不同,而队列响应的不是平均值,而是瞬间。

还有相反方向的误解也很常见。在压力测试工具里填入“并发用户 100 人”,并预先写下每秒会有多少次。在闭环中,请求速率不是输入,而是结果。服务器一变慢,用户提交下一个请求就会更晚,请求速率自己就降下来了,所以不管服务器坏到什么程度,测试画出来的都是“扛得很好”的图景。

工作原理

到达过程,就是“请求何时到来”的形态。只需要了解三种。

形态 间隔 变异系数(CV) 出现在哪里
等间隔 始终为 1/请求率 0 合成压力测试的默认值
指数分布(泊松) 平均值为 1/请求率的指数分布 1 大量相互独立的用户
突发(burst) 多个请求同时到达,然后长时间停歇 远大于 1 cron、重试风暴、通知发送

变异系数是间隔的标准差除以平均值所得的值。即使平均请求率相同,这个值越大,等待就越长。排队论的近似公式(Kingman)所说的方向也相同——等待时间不仅与利用率成正比,还与到达和服务的变动性成正比。所以“利用率 25%,所以安全”这句话,不说明到达的形态,就没有任何意义。

闭环一侧有另一条定律。N 个用户发送一个请求后,停歇思考时间 Z,如果响应时间是 R,有效请求率 X 就是 N / (R + Z)。这是把利特尔法则应用到单个用户的一个往返上的形式,意思是只要确定了用户数和思考时间,请求率就由服务器来决定。如果漏掉 R,用 N / Z 来预测,结果总会比实际高,而且服务器越慢,这个误差越大。

k6 文档把这两者按执行器(executor)分开称呼。以请求率作为输入的到达率执行器是开放模型,以虚拟用户数作为输入的执行器是封闭模型。用哪一种,不取决于偏好,而取决于要测量的对象是什么。

在现场相遇的样子

最常见的事故是重试风暴。平时均匀到来的请求,一旦出现一次超时,客户端就会在同一时刻重试,到达就变成了突发。平均请求率只增加了几个百分点,队列却变成十倍,而这些排队又会引发新的超时。禁止不带抖动(jitter)的重试,原因就在这里。

整点醒来的批处理也是如此。如果 cron 表达式全都是 0 * * * *,即使一天的平均负载很低,每个整点的并发请求数也会远远超过 worker 数。这类服务的容量必须按突发的规模,而不是按平均值来定。

还有一种测试工具一侧的事故。对本该用到达率执行器来测试的服务,却用虚拟用户数来测试,服务器一变慢,负载也随之减少,就看不到饱和点。报告里写着“扛到了每秒多少次”,但那个数字不是服务器的极限,而是测试设置的极限。

所以在文档中写负载时,必须在平均值旁边同时写下两样东西。一是到达的形态——均匀、相互独立,还是突发。二是如果是突发,一次有多少个。没有这两样,“每秒 25 次”就是一个没法用来确定 worker 数的数字。从观测中提取这个值的方法也很简单。把访问日志的时刻按短窗口(例如 100 毫秒)归组统计,均匀的流量每个窗口的数量相近,而突发的流量大部分窗口是 0,只有少数窗口突然变得很大。那个突起的窗口的大小,就是应该放进容量计算的数字。

剩下的问题是留出多少余量。答案取决于服务承诺了什么。如果是用户在等待的请求,就必须放置足以原样承接突发的 worker;如果是后台运行的工作,就可以把队列设得长一些,慢慢消化。重要的是在已测得的突发规模之上做这个选择,而不是只看平均值。

下一项实验要做什么

启动一个 worker 数固定的服务器,以同样的平均请求率,亲手生成并发送等间隔、指数分布、突发三种到达。测量三份计划的间隔变异系数,确认平均值相同、只有形态不同之后,比较服务器记录的队列长度和响应时间分布。接着制作闭环生成器,用定律和实测对照思考时间如何决定有效请求率,最后用数字展示假定均匀到达的容量估算,在真实的突发到达下会偏差多少倍,再连同依据一起确定这个服务的到达模型和 worker 数。