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

压力测试

服务器停顿了两秒,p99 却是60毫秒

在 TT Lab 中继续学习

目标

对同一个服务器,分别用闭环和开放模型测量一次,亲手做出 p99 相差多大,再亲手对闭环样本实现 HdrHistogram 式的校正,确认校正前后 SLA 判定被颠倒,并把测试设计规则留存为文件。

为什么重要

闭环负载生成器必须收到上一个请求的响应,才会发送下一个请求。所以在服务器停滞期间,它根本不发送请求,那个区间几乎不会留在延迟分布里。真实用户不会看服务器的情况来推迟请求,所以这种测量总是只朝着看起来比实际更好的一侧出错。它不是随机波动的误差,而是只往一个方向倾斜的误差,所以没有人怀疑,并且通过了上线评审。修复方法有两种——从一开始就用固定发送时刻的开放模型来测量,或者用预期间隔去弥补已经测好的样本。两者之中必须做一种,并且必须在报告中留下是哪一种。

步骤

  1. 把 /opt/lab/lt/lt-coordinated-omission/target.py 启动在端口 8080 上(正常响应 0.05 秒,停滞 2.0 秒,在第 150 个请求时停滞)。然后用 curl -s -o /dev/null -w '%{time_total}\n' 请求 / 六次,把这六行原样留在 /root/lt-coordinated-omission/01-probe.txt 中,并在 /root/lt-coordinated-omission/01-target.txt 中写四行 port=、base_ms=、stall_ms=、stall_at=。base_ms 是这六行的中位数,以毫秒整数表示,stall_ms 和 stall_at 是从服务器源码中读取的值。
  2. 重置请求编号之后(/reset),用 hey -n 200 -c 1 -q 12.5 -t 60 -o csv 测量,把原始 CSV 原样保留在 /root/lt-coordinated-omission/closed.csv 中。只使用该 CSV 的 response-time 字段,在 /root/lt-coordinated-omission/02-closed.txt 中写四行 samples=、p50_ms=、p99_ms=、max_ms=。请四舍五入成毫秒整数。百分位数按升序排序之后,取 인덱스 = int(백분위 ÷ 100 × 표본수)(即索引 = int(百分位 ÷ 100 × 样本数);从 0 开始计数,超过样本数 − 1 则取最后一个)位置的值来计算。与 hey 使用的方式相同。
  3. 在 /root/lt-coordinated-omission/schedule.txt 中生成 200 行。每行一个,写下预定发送请求 i 的时刻,以秒为单位,保留到小数点后第三位。第一行是 0.000,间隔固定为 0.080 秒。这个文件将在下一步成为校正延迟的基准。
  4. 请亲自编写 /root/lt-coordinated-omission/openloop.py。在 schedule.txt 的每个时刻发送一个请求,但不等待上一个请求。 结果以表头 i,intended,sent,received,code 和 200 行留在 /root/lt-coordinated-omission/open.csv 中(时刻是从测量开始起算的秒数),并在 /root/lt-coordinated-omission/04-open.txt 中写校正延迟(received − intended)的 samples=、p50_ms=、p99_ms=、max_ms= 四行。
  5. 编写 /root/lt-coordinated-omission/hdrfix.py,用 0.080 秒的预期间隔弥补 closed.csv 的延迟样本。如果一个样本的值大于预期间隔,就从该值中不断减去预期间隔,只要大于 0,就继续构造并放入样本(这就是 HdrHistogram 的 recordValueWithExpectedInterval 所做的事)。把弥补后的结果逐行以秒为单位留在 /root/lt-coordinated-omission/closed-corrected.txt 中,并在 /root/lt-coordinated-omission/05-hdr.txt 中写五行 expected_interval_ms=、samples=、p50_ms=、p99_ms=、max_ms=。
  6. 创建 /root/lt-coordinated-omission/compare.tsv。没有表头,共三行,每行是以制表符分隔的四个字段 <이름> <p50_ms> <p99_ms> <max_ms>(占位符依次为名称、p50_ms、p99_ms、max_ms)。名称依次为 closed(第 2 步)、hdr(第 5 步)、open(第 4 步),数字原样照搬前面几步写下的毫秒整数。
  7. 在 /root/lt-coordinated-omission/sla.txt 中写六行。sla_p99_ms= 写 100 到 1000 之间的整数阈值,closed_p99_ms= 和 corrected_p99_ms= 分别写闭环(第 2 步)和开放模型校正(第 4 步)的 p99,closed_verdict= 和 corrected_verdict= 在该 p99 小于等于阈值时写 pass,大于时写 fail,flipped= 在两个判定不同时写 yes,相同时写 no。
  8. 在 /root/lt-coordinated-omission/08-summary.txt 中写两行 worst_case_ms=(开放模型校正延迟的最大值)和 omitted_requests=(闭环在停滞期间未能发送的请求数 = 最大的延迟样本除以预期间隔 0.080 秒所得的商)。然后在 /root/lt-coordinated-omission/rules.md 中写四行形如 - <열쇠>: <설명>(占位符依次为键、说明)的规则。键依次为 rate、correct、report、gate,每条说明必须至少 40 个字符。

参考

启动一个会周期性停滞的压力对象

把 /opt/lab/lt/lt-coordinated-omission/target.py 启动在端口 8080 上(正常响应 0.05 秒,停滞 2.0 秒,在第 150 个请求时停滞)。然后用 curl -s -o /dev/null -w '%{time_total}\n' 请求 / 六次,把这六行原样留在 /root/lt-coordinated-omission/01-probe.txt 中,并在 /root/lt-coordinated-omission/01-target.txt 中写四行 port=、base_ms=、stall_ms=、stall_at=。base_ms 是这六行的中位数,以毫秒整数表示,stall_ms 和 stall_at 是从服务器源码中读取的值。

要在后台启动,用 nohup python3 ... > server.log 2>&1 &。请等 1–2 秒,直到它起来。如果 8080 已经被占用,就不能启动两次——如果 curl http://127.0.0.1:8080/reset 返回 reset,就说明已经在运行了。中位数只要在 sort -n 之后看中间那一行即可。

用闭环测量,并写下原始百分位数

重置请求编号之后(/reset),用 hey -n 200 -c 1 -q 12.5 -t 60 -o csv 测量,把原始 CSV 原样保留在 /root/lt-coordinated-omission/closed.csv 中。只使用该 CSV 的 response-time 字段,在 /root/lt-coordinated-omission/02-closed.txt 中写四行 samples=、p50_ms=、p99_ms=、max_ms=。请四舍五入成毫秒整数。百分位数按升序排序之后,取 인덱스 = int(백분위 ÷ 100 × 표본수)(即索引 = int(百分位 ÷ 100 × 样本数);从 0 开始计数,超过样本数 − 1 则取最后一个)位置的值来计算。与 hey 使用的方式相同。

-c 1 是一个 worker,-q 12.5 表示每个 worker 每秒的上限是 12.5 次(间隔 80 毫秒)。加上 -o csv,输出的就不是摘要,而是每个请求一行,第一行是表头。请看 p99 与最大值相差多少倍——这个差距就是本实验的主题。

提前钉死预定发送时刻

在 /root/lt-coordinated-omission/schedule.txt 中生成 200 行。每行一个,写下预定发送请求 i 的时刻,以秒为单位,保留到小数点后第三位。第一行是 0.000,间隔固定为 0.080 秒。这个文件将在下一步成为校正延迟的基准。

闭环里根本没有“预定时刻”这种东西——因为上一个响应一到,就立刻发送。开放模型则先把那个时刻定下来,不管服务器的情况如何都严格遵守。用 seq 或一行 awk 就能做出来。最后一行是 (200 − 1) × 0.08。

用开放模型重新测量,求出校正延迟

请亲自编写 /root/lt-coordinated-omission/openloop.py。在 schedule.txt 的每个时刻发送一个请求,但不等待上一个请求。 结果以表头 i,intended,sent,received,code 和 200 行留在 /root/lt-coordinated-omission/open.csv 中(时刻是从测量开始起算的秒数),并在 /root/lt-coordinated-omission/04-open.txt 中写校正延迟(received − intended)的 samples=、p50_ms=、p99_ms=、max_ms= 四行。

为每个请求创建一个线程,让该线程 time.sleep 到自己的时刻再发送即可。用 urllib.request.urlopen 就足够了。只使用标准库(无法 pip 安装)。sent 和 intended 几乎相同,才是开放模型——如果相差很大,就是生成器跟不上了。

亲手实现 HdrHistogram 的校正

编写 /root/lt-coordinated-omission/hdrfix.py,用 0.080 秒的预期间隔弥补 closed.csv 的延迟样本。如果一个样本的值大于预期间隔,就从该值中不断减去预期间隔,只要大于 0,就继续构造并放入样本(这就是 HdrHistogram 的 recordValueWithExpectedInterval 所做的事)。把弥补后的结果逐行以秒为单位留在 /root/lt-coordinated-omission/closed-corrected.txt 中,并在 /root/lt-coordinated-omission/05-hdr.txt 中写五行 expected_interval_ms=、samples=、p50_ms=、p99_ms=、max_ms=。

一个延迟为 2.0 秒的样本,在预期间隔 0.08 秒下,会以 1.92、1.84、…… 构造出多个样本。原来的样本保持不动,把构造出来的加进去。预期间隔就是第 2 步中用 -q 确定的间隔(1 ÷ 12.5 = 0.08 秒)。百分位数的计算方式与前面的步骤相同。

把三个分布放进一张表里

创建 /root/lt-coordinated-omission/compare.tsv。没有表头,共三行,每行是以制表符分隔的四个字段 <이름> <p50_ms> <p99_ms> <max_ms>(占位符依次为名称、p50_ms、p99_ms、max_ms)。名称依次为 closed(第 2 步)、hdr(第 5 步)、open(第 4 步),数字原样照搬前面几步写下的毫秒整数。

关键在于,三行的 p50 几乎相同,只有 p99 分开了。p50 相同,意味着服务器平时运行良好;p99 分开,意味着糟糕区间的样本在某一边有,在另一边没有。评分器会从三份原始文件中重新计算值来比对。

同样的测试,被颠倒的 SLA 判定

在 /root/lt-coordinated-omission/sla.txt 中写六行。sla_p99_ms= 写 100 到 1000 之间的整数阈值,closed_p99_ms= 和 corrected_p99_ms= 分别写闭环(第 2 步)和开放模型校正(第 4 步)的 p99,closed_verdict= 和 corrected_verdict= 在该 p99 小于等于阈值时写 pass,大于时写 fail,flipped= 在两个判定不同时写 yes,相同时写 no。

阈值设在哪里都可以,但如果选在两个判定分开的位置,这一步的要点就会显现出来。如果判定被颠倒,需要修正的不是阈值,而是“按哪个数字来承诺”。如果报告中不写明是否做了校正,半年后就没有人能区分。

避免再次被骗的测试设计规则

在 /root/lt-coordinated-omission/08-summary.txt 中写两行 worst_case_ms=(开放模型校正延迟的最大值)和 omitted_requests=(闭环在停滞期间未能发送的请求数 = 最大的延迟样本除以预期间隔 0.080 秒所得的商)。然后在 /root/lt-coordinated-omission/rules.md 中写四行形如 - <열쇠>: <설명>(占位符依次为键、说明)的规则。键依次为 rate、correct、report、gate,每条说明必须至少 40 个字符。

这四条规则要回答的问题是这样的——用什么模型施加负载(rate)、如何校正已经测好的样本(correct)、在报告中一并写下什么(report)、以哪个数字来判定通过(gate)。每行用自己的话写,但要以前面几步看到的数字为依据。