服务器停顿了两秒,p99 却是60毫秒
目标
对同一个服务器,分别用闭环和开放模型测量一次,亲手做出 p99 相差多大,再亲手对闭环样本实现 HdrHistogram 式的校正,确认校正前后 SLA 判定被颠倒,并把测试设计规则留存为文件。
为什么重要
闭环负载生成器必须收到上一个请求的响应,才会发送下一个请求。所以在服务器停滞期间,它根本不发送请求,那个区间几乎不会留在延迟分布里。真实用户不会看服务器的情况来推迟请求,所以这种测量总是只朝着看起来比实际更好的一侧出错。它不是随机波动的误差,而是只往一个方向倾斜的误差,所以没有人怀疑,并且通过了上线评审。修复方法有两种——从一开始就用固定发送时刻的开放模型来测量,或者用预期间隔去弥补已经测好的样本。两者之中必须做一种,并且必须在报告中留下是哪一种。
步骤
- 把
/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是从服务器源码中读取的值。 - 重置请求编号之后(
/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 使用的方式相同。 - 在
/root/lt-coordinated-omission/schedule.txt中生成 200 行。每行一个,写下预定发送请求 i 的时刻,以秒为单位,保留到小数点后第三位。第一行是0.000,间隔固定为 0.080 秒。这个文件将在下一步成为校正延迟的基准。 - 请亲自编写
/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=四行。 - 编写
/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=。 - 创建
/root/lt-coordinated-omission/compare.tsv。没有表头,共三行,每行是以制表符分隔的四个字段<이름> <p50_ms> <p99_ms> <max_ms>(占位符依次为名称、p50_ms、p99_ms、max_ms)。名称依次为closed(第 2 步)、hdr(第 5 步)、open(第 4 步),数字原样照搬前面几步写下的毫秒整数。 - 在
/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 个字符。
参考
- 工作目录是
/root/lt-coordinated-omission。如果不存在,请先创建。 - 压力对象和流量数据在
/opt/lab/lt/lt-coordinated-omission/中——只有一个target.py。Pod 中无法启动容器,所以压力对象直接在 127.0.0.1 上启动 Python 标准库的 HTTP 服务器来使用。 - 开始测量之前,请用
curl http://127.0.0.1:8080/reset重置请求编号。如果不重置,停滞的请求位置就会不同。 - 常见错误:因为最大值很大,就猜想 p99 也会很大。在 200 个样本中,只有一个坏样本,是捕获不到 p99 里的。请始终同时看这两个数字。
- 常见错误:把开放模型脚本写成等待上一个响应。如果
open.csv中的sent与intended相距越来越远,那就不是开放模型,而是一个缓慢的闭环。 - hey 用法与选项、wrk2 — coordinated omission 校正、HdrHistogram、k6 — open vs closed model、vegeta — 固定速率攻击
启动一个会周期性停滞的压力对象
把 /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)。每行用自己的话写,但要以前面几步看到的数字为依据。