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

压力测试

拒绝不是失败,是保护

在 TT Lab 中继续学习

一句话总结

设计问题不是“要不要设置队列”,而是哪条队列会最先达到极限,以及到那时该怎么办。

为什么需要了解这一点

一个请求被处理前,要依次经过多条队列:套接字缓冲区 → 线程池队列 → 连接池等待 → DB 锁等待 → 处理。任何一处先饱和,后面的步骤都会变成等待。因此,调查瓶颈就是按顺序检查这架梯子。

队列长度上限可以计算:允许等待时间除以平均处理时间。若目标为 200ms、处理耗时 20ms、工作线程 10 个,上限大约是 100。超过该值仍设置无限队列,只是把故障转化为延迟。拒绝不是失败,而是保护;尽快返回 503 反而更好。

工作原理

最著名的实际冲突来自两个默认值:HikariCP 的 maximum-pool-size 默认为 10,而 Tomcat 的 threads.max 默认为 200。流量增加后,大多数线程会陷入等待连接,Connection is not available, request timed out after 30000ms 开始大量出现。解决时要同时修改三项:把池扩大到 30,把 connection-timeout 从 30 秒降到 5 秒以快速失败,同时反而把 Tomcat threads.max 缩减到 50。防止复发的告警条件是 hikaricp.connections.pending > 0 持续超过 30 秒。

定位饱和点分五步。

  1. 用公式估算初始值。池大小 =(核心数 × 2)+ 磁盘轴数。8 核时约为 20。
  2. 在目标负载下记录池等待 p99 与吞吐量。
  3. 把池缩小一半。吞吐量若不变,说明原值过大。
  4. 逐步增大,直到吞吐量不再上升;刚开始趋平的值略低一点,就是合适值。
  5. 若该值下等待仍很长,应优化查询,而不是扩大池。

连接等待时间长,意味着连接被占用得久,通常源于慢查询或长事务。扩大池只能暂时掩盖症状。因此原则是:池大小应等于数据库能良好并行处理的数量,而不是应用想发送的数量。

实际工作中的表现

达到极限后继续增加并发,吞吐量 X 不再上升,响应时间 W 却会按比例增加。随后,上下文切换、接近并发平方增长的锁竞争、缓存命中率下降和重试负载叠加,吞吐量反而开始下降。公式传达的重点不是精确数字,而是数量级:答案不是数百,而是数十。

测量工具会隐藏瓶颈

找不到瓶颈时,最常见的原因不在服务器,而在测量端。如果压测工具收到响应后才发送下一请求,那么服务器停顿期间,根本不会发出请求。 这段停顿不会计入任何请求的响应时间,所以即使服务器整整冻结一秒,指标中也不会出现。这叫协调遗漏(coordinated omission)。真实用户不会体谅服务器而推迟请求,因此以这种方式测得的 p99 会比现实乐观得多。

有两种规避方式。第一,使用支持固定目标到达率的工具。它不等待响应,而按固定间隔发送请求,并把延迟部分计入响应时间。第二,如果工具不支持,就在服务器端单独测量队列等待时间。请求到达套接字与被工作线程取走之间的差值就是等待时间;它开始增长的时点才是真正的饱和点。

平均值也会以类似方式误导人。响应时间分布并不对称,而是向右拖着长尾;平均值往往不是大多数用户真正经历的值,所以应查看百分位。但把多个实例的 p99 求平均毫无意义。 百分位不能直接相加或相除。必须汇总直方图桶,重新构建整体分布,再计算百分位。

还需要注意一点:如果一个页面由十次内部调用构成,每次调用的 p99 都是 100ms,页面整体 p99 并不是 100ms。十次中任意一次落入慢尾部的概率高得多,用户实际经历的延迟会远高于它。增加一次内部调用,等于再叠加一条尾延迟,而不只是增加该调用的平均值。 因此,保护性能最有效的方法,不是把每次调用都稍微变快,而是减少调用次数,或让调用并行重叠。

下个练习将做什么

故意搭建一个每次只能处理一个请求的 Python HTTP Server。分别在并发 1 和 10 下测量,确认吞吐量不变而延迟增加这一串行化信号;按层次提出三个瓶颈假设,再换成可并发处理的服务器,比较吞吐量提高了多少倍。最后计算承受目标流量所需的实例数量。