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

压力测试

最糟的时刻从结果里消失了

在 TT Lab 中继续学习

一句话总结

闭环负载生成器在服务器停滞期间,根本不发送请求。 没有发送的请求不会留在延迟分布中,所以最糟糕的时刻会从结果里整个消失。这称为协调遗漏(coordinated omission,CO)。

为什么需要它

在一个支付 API 前,我曾收到过这样的报告:“1 分钟内发送了 12,000 次,p99 是 60 毫秒。远低于 500 毫秒的目标。”然而同一时段,服务器日志里记录着一次 2 秒的停顿。两句话出自同一份数据。

这并不奇怪。如果负载生成器是以并发度 1 运行的,那么在服务器停滞 2 秒期间,那个生成器为了等响应,什么也没有发送。 只有 2 秒结束后返回的那一个响应,会被记录为 2 秒。200 个样本中占 1 个。200 个中的 1 个是第 99.5 百分位,所以 p99 的位置,坐的是它旁边完好无损的 50 毫秒。

真实用户不会这样行动。用户不会因为服务器停了就推迟请求。在那 2 秒里,请求也会不断进来,这些请求全部堆积在队列后面,等待接近 2 秒的时间。也就是说,报告是在实际上体验很糟的几十个请求没有被测量到的情况下做出来的。

这个缺陷之所以可怕,是因为它出错的方向总是相同。协调遗漏不是随机地扰动结果,而是总是朝着看起来更好的一侧出错。所以没有人怀疑它,它通过了上线评审,直到出了故障之后,才会听到“测试的时候明明没问题”这句话。

工作原理

施加负载的方式大体有两种。

模型 发送下一个请求的时机 服务器变慢时
闭环(closed) 收到上一个请求的响应之后 发送量会自己减少
开放模型(open) 到了预先确定的时刻 发送量不变,队列变长

闭环是模拟并发用户数时合适的模型。问题在于,它的这一特性会原样渗入测量。服务器一变慢,负载也随之减少,所以生成器和服务器串通(coordinated)起来,一起跳过糟糕的区间。名称就来源于此。

修复方法有两条路。第一是从一开始就用开放模型来测量。预先固定好请求 i 的预定发送时刻,把延迟测量为 응답 시각 − 의도한 발사 시각(即响应时刻减去预定发送时刻)。这个值称为校正延迟(corrected latency)。如果发送晚了,晚了多久,就会原样加到延迟上。

第二是事后弥补已经测好的闭环样本。HdrHistogram 的 recordValueWithExpectedInterval 做的就是这件事——如果一个样本的值大于预期间隔,就把在这段时间里本该发送却没能发送的请求,按预期间隔逐个递减地构造出来并放进去。一个延迟为 2.0 秒的样本,在预期间隔为 80 毫秒的情况下,会以 1.92 秒、1.84 秒……这样的方式再生成 25 个样本。wrk2 之所以从 wrk 分叉出来,也是为了加入这种校正。

两种方法的结果指向同一侧。所以无论哪一种,必须做其中之一。不过事后校正有个局限,必须知道预期间隔才能使用。如果是设定了目标速率来施加负载的测试,预期间隔就是该速率的倒数,但如果是没有速率、只设定了并发度来施加负载的测试,从一开始就没有预期间隔这种东西。这样的测试结果无法事后修正,只能重新测量。

在现场相遇的样子

最常见的情形是“只有最大值很大,而 p99 很小”的结果。看到这种组合,首先应该怀疑协调遗漏。最大值是 p99 的三十倍,意味着糟糕区间内只捕获到一两个样本,而这通常是因为在那个区间里没能发送请求。

第二种是“提高并发度之后,p99 反而变好了”的结果。并发度高时,即使在停滞的区间,其他 worker 的请求也会有几个进来排队,所以会产生一些样本,但仍然远少于实际的到达率。不是数字变好了,而是错得少了一点。

第三种是换了工具之后数字变差的报告。从闭环工具换到开放模型工具,p99 会跳高好几倍。当时曾真的有人得出“新工具不准确”的结论,并换了回去。换回去的那一方是错的——新的数字才是第一次对的数字。

最后,做了校正之后,SLA 的判定经常会被颠倒。校正前是通过,校正后是不达标。此时需要的不是去调整阈值,而是重新就“按哪个数字来承诺”达成一致。 如果不在测试报告里写明是否做了校正,半年之后就没有人知道那个数字是哪一种了。

下一项实验要做什么

启动一个只在第 150 个请求时停滞 2 秒的 Python 服务器,对同一个服务器测量两次。一次用 hey 做闭环,一次用预先写好 80 毫秒间隔 200 个请求的固定计划做开放模型。从两份原始文件中亲手重新计算 p50、p99、最大值并放进表里,再亲手对闭环样本实现 HdrHistogram 式的校正,得到第三个分布。最后设定一个 SLA 阈值,确认校正前后判定被颠倒,并把测试设计规则留存为文件,以免再次落入这个陷阱。