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

压力测试

把各分片的 p95 求平均,比真实 p95 高出了 108 毫秒

在 TT Lab 中继续学习

目标

用两种方法亲手计算百分位数,确认值会分歧;测量各区间 p95 的简单平均和加权平均与整体 p95 偏差多少;测量把直方图桶相加来合并的方法及其误差;最后制作并提交一个能正确合并两次真实压力测试运行结果的工具。

为什么重要

百分位数与平均值不同,无法从部分得到整体。平均值由总和与个数构成,所以把各部分的总和相加就得到整体,而百分位数是排好序的位置,所以把各部分的位置相加,并不会得到整体的位置。然而,仪表板和报告却若无其事地对各区间的 p95 求平均。这个值可能比真值高,也可能比真值低,所以“取平均总归是往安全的一侧出错”这种直觉同样行不通。再加上计算方法还有两种,换一个工具,明明什么也没发生,p95 却看起来变差了。正确合并的路只有重新对原始值排序,或者把直方图的桶相加,而走桶这条路,边界就决定了误差。把这三件事亲手做一遍,以后就不会再丢弃原始输出文件了。

步骤

  1. 在 /opt/lab/lt/lt-percentiles/ 中有三个区间的延迟样本(每行一个毫秒值)。创建 /root/lt-percentiles/shards.tsv。共三行,每行是以制表符分隔的三个字段 <파일이름> <표본 수> <p95>(占位符依次为文件名、样本数、p95)。名称依次写 shard-a、shard-b、shard-c,p95 用最近秩(排序之后的第 ceil(0.95 x n) 个值)求出,保留到小数点后第一位。然后在 /root/lt-percentiles/01-note.txt 中写两行 total_n=<세 파일의 표본 수 합>(占位符为三个文件的样本数之和)和 slowest=<p95 가 가장 큰 구간 이름>(占位符为 p95 最大的区间名称)。
  2. 创建 /root/lt-percentiles/pct.py。以 python3 pct.py <표본파일> <분위> <nearest|linear>(占位符依次为样本文件、分位、nearest 或 linear)调用时,把一个百分位数保留到小数点后第四位,打印为一行。nearest 直接使用第 ceil(분위 x n)(即 ceil(分位 x n))小的值,linear 在前后两个样本之间,按比例构造 h = (n - 1) x 분위(即 h = (n - 1) x 分位)这个位置。用这个工具测量 /opt/lab/lt/lt-percentiles/tiny.txt(20 行),在 /root/lt-percentiles/methods.tsv 中写三行。每行是以制表符分隔的三个字段 <분위> <nearest> <linear>(占位符依次为分位、nearest、linear),分位依次为 0.50、0.95、0.99,值保留到小数点后第四位。然后在 /root/lt-percentiles/02-note.txt 中写一行 gap_p95=<0.95 에서 linear 빼기 nearest>(占位符为 0.95 分位上 linear 减去 nearest),保留到小数点后第四位。
  3. 在 /root/lt-percentiles/combine.txt 中写五行。mean_of_p95= 是第 1 步求得的三个 p95 的简单平均,weighted_mean_of_p95= 是以样本数为权重的平均,true_p95= 是把三个文件的原始值全部合并后,重新用最近秩求出的 p95。接着在 error_mean= 中写 mean_of_p95 减去 true_p95,在 error_weighted= 中写 weighted_mean_of_p95 减去 true_p95。五个值都保留到小数点后第一位,如果是负数,前面加上负号。
  4. 在 /root/lt-percentiles/bound.tsv 中写两行。每行是以制表符分隔的三个字段 <이름> <표본 수> <p95>(占位符依次为名称、样本数、p95),第一行的名称是 all(三个区间全部合并),第二行是 no-c(只合并 shard-a 和 shard-b)。p95 用最近秩,保留到小数点后第一位。然后在 /root/lt-percentiles/04-note.txt 中写三行 min_shard_p95=、max_shard_p95=、inside=<yes|no>(占位符为 yes 或 no)。如果 all 的 p95 位于各区间 p95 的最小值与最大值之间,inside 就是 yes。
  5. 创建 /root/lt-percentiles/hist.py。以 python3 hist.py <경계를 쉼표로> <표본파일...>(占位符依次为以逗号分隔的边界、样本文件)调用时,打印一张两个字段、以制表符分隔的表。每行是 <경계> <그 경계 이하인 표본의 누적 개수>(占位符依次为边界、不大于该边界的样本累计个数),最后一行是 +Inf <전체 개수>(占位符为总个数)。如果给出多个文件,就按文件分别统计,再把相同边界的值相加。用这个工具,给出边界 50,100,250,500,1000 和全部三个区间文件,把得到的表保存到 /root/lt-percentiles/hist-coarse.tsv。然后在 /root/lt-percentiles/hist-p95.txt 中写三行 est_p95=、true_p95=、abs_error=,保留到小数点后第一位。est_p95 是从该表中用线性插值估算的 p95——找到累计个数大于等于 0.95 x 전체(即 0.95 x 总数)的第一个边界,把它与前一个边界之间,按累计个数成比例划分(如果没有前一个边界,就视为 0)。
  6. 用密集的边界 50,75,100,125,150,200,250,300,400,500,750,1000,1500 重新统计同样的样本,保存到 /root/lt-percentiles/hist-fine.tsv。然后在 /root/lt-percentiles/error.tsv 中写两行。每行是以制表符分隔的三个字段 <이름> <추정 p95> <절대 오차>(占位符依次为名称、估计 p95、绝对误差),名称依次为 coarse、fine,值保留到小数点后第一位。误差是与第 3 步的 true_p95 之差的绝对值。最后在 /root/lt-percentiles/06-note.txt 中写两行 better=<coarse|fine>(占位符为 coarse 或 fine)和 reason=<40자 이상>(占位符为至少 40 个字符)。在 reason 中,请同时写明密集的边界并不是免费的原因。
  7. 把 /opt/lab/lt/lt-percentiles/target.py 启动在 8080 端口上(/fast 为 30 毫秒,/slow 为 250 毫秒)。运行 hey 两次,保留原始输出——把 hey -n 300 -c 10 -o csv http://127.0.0.1:8080/fast 保存为 /root/lt-percentiles/run-fast.csv,把 hey -n 100 -c 10 -o csv http://127.0.0.1:8080/slow 保存为 /root/lt-percentiles/run-slow.csv。然后在 /root/lt-percentiles/runs.txt 中写四行——p95_fast=、p95_slow=、mean_of_p95=(两个 p95 的简单平均)、merged_p95=(把两次运行的原始响应时间全部合并后重新求出的 p95)。全部以秒为单位,保留到小数点后第四位,只统计 status-code 为 200 的行。
  8. 创建 /root/lt-percentiles/merge_p95.py。以 python3 merge_p95.py <분위> <hey CSV...>(占位符依次为分位、hey CSV 文件)调用时,从这些 CSV 中 status-code 为 200 的行里取出第一个字段(响应时间,秒),全部收集到同一个桶里,用最近秩求出百分位数,保留到小数点后第四位,打印为一行。不能按每次运行分别求百分位数再取平均。把这个工具对第 7 步的两个 CSV 运行,在 /root/lt-percentiles/merged.txt 中写四行——q=0.95、merged_p95=、mean_of_p95=、gap=(merged 减去 mean)。最后在 /root/lt-percentiles/policy.txt 中,用以 rule= 开头、至少 60 个字符的一行,写下今后合并多次压力测试结果时团队应当遵守的规则。

参考

每个区间有多少次,p95 是多少

在 /opt/lab/lt/lt-percentiles/ 中有三个区间的延迟样本(每行一个毫秒值)。创建 /root/lt-percentiles/shards.tsv。共三行,每行是以制表符分隔的三个字段 <파일이름> <표본 수> <p95>(占位符依次为文件名、样本数、p95)。名称依次写 shard-a、shard-b、shard-c,p95 用最近秩(排序之后的第 ceil(0.95 x n) 个值)求出,保留到小数点后第一位。然后在 /root/lt-percentiles/01-note.txt 中写两行 total_n=<세 파일의 표본 수 합>(占位符为三个文件的样本数之和)和 slowest=<p95 가 가장 큰 구간 이름>(占位符为 p95 最大的区间名称)。

排序用 sort -n,行数用 wc -l。最近秩的位置编号是 ceil(0.95 x n),在 awk 中用 int(0.95*n) + (0.95*n > int(0.95*n) ? 1 : 0) 来得到。请留意三个区间的样本数互不相同——在后面的步骤中,这个差别会改变答案。

同样的样本,两种计算方法

创建 /root/lt-percentiles/pct.py。以 python3 pct.py <표본파일> <분위> <nearest|linear>(占位符依次为样本文件、分位、nearest 或 linear)调用时,把一个百分位数保留到小数点后第四位,打印为一行。nearest 直接使用第 ceil(분위 x n)(即 ceil(分位 x n))小的值,linear 在前后两个样本之间,按比例构造 h = (n - 1) x 분위(即 h = (n - 1) x 分位)这个位置。用这个工具测量 /opt/lab/lt/lt-percentiles/tiny.txt(20 行),在 /root/lt-percentiles/methods.tsv 中写三行。每行是以制表符分隔的三个字段 <분위> <nearest> <linear>(占位符依次为分位、nearest、linear),分位依次为 0.50、0.95、0.99,值保留到小数点后第四位。然后在 /root/lt-percentiles/02-note.txt 中写一行 gap_p95=<0.95 에서 linear 빼기 nearest>(占位符为 0.95 分位上 linear 减去 nearest),保留到小数点后第四位。

在排好序的列表中,nearest 取索引 k-1,linear 取 values[lo] + (h-lo) x (values[hi]-values[lo])。20 个的样本,是为了让两种方法的差别肉眼可见而选的——尤其是最上面的两个值相距很远。评分器会用它自己制作的样本文件来直接运行这个工具。

各区间 p95 的平均值不是整体 p95

在 /root/lt-percentiles/combine.txt 中写五行。mean_of_p95= 是第 1 步求得的三个 p95 的简单平均,weighted_mean_of_p95= 是以样本数为权重的平均,true_p95= 是把三个文件的原始值全部合并后,重新用最近秩求出的 p95。接着在 error_mean= 中写 mean_of_p95 减去 true_p95,在 error_weighted= 中写 weighted_mean_of_p95 减去 true_p95。五个值都保留到小数点后第一位,如果是负数,前面加上负号。

合并原始值,只要一次 cat /opt/lab/lt/lt-percentiles/shard-*.txt 即可。请直接使用第 2 步做好的 pct.py。这一步的关键在于两个误差的符号互不相同——平均值并不只往一个方向出错。

合并后的 p95 可能落在的界限

在 /root/lt-percentiles/bound.tsv 中写两行。每行是以制表符分隔的三个字段 <이름> <표본 수> <p95>(占位符依次为名称、样本数、p95),第一行的名称是 all(三个区间全部合并),第二行是 no-c(只合并 shard-a 和 shard-b)。p95 用最近秩,保留到小数点后第一位。然后在 /root/lt-percentiles/04-note.txt 中写三行 min_shard_p95=、max_shard_p95=、inside=<yes|no>(占位符为 yes 或 no)。如果 all 的 p95 位于各区间 p95 的最小值与最大值之间,inside 就是 yes。

请看一看去掉仅占流量 10% 的一个缓慢区间后,整体 p95 会降低多少。再想一想界限——如果在所有区间中,95% 都在某个值以内,那么混在一起之后,95% 也在那个值以内。所以“合并之后比所有区间都大”这样的报告,应该怀疑的是计算,而不是数据。

桶是可以相加的

创建 /root/lt-percentiles/hist.py。以 python3 hist.py <경계를 쉼표로> <표본파일...>(占位符依次为以逗号分隔的边界、样本文件)调用时,打印一张两个字段、以制表符分隔的表。每行是 <경계> <그 경계 이하인 표본의 누적 개수>(占位符依次为边界、不大于该边界的样本累计个数),最后一行是 +Inf <전체 개수>(占位符为总个数)。如果给出多个文件,就按文件分别统计,再把相同边界的值相加。用这个工具,给出边界 50,100,250,500,1000 和全部三个区间文件,把得到的表保存到 /root/lt-percentiles/hist-coarse.tsv。然后在 /root/lt-percentiles/hist-p95.txt 中写三行 est_p95=、true_p95=、abs_error=,保留到小数点后第一位。est_p95 是从该表中用线性插值估算的 p95——找到累计个数大于等于 0.95 x 전체(即 0.95 x 总数)的第一个边界,把它与前一个边界之间,按累计个数成比例划分(如果没有前一个边界,就视为 0)。

桶是累计的。le=100 表示“100 以内的有多少次”,而不是“50 到 100 之间”。估算公式与 Prometheus 的 histogram_quantile 相同——按比例来看目标个数落在桶内的哪个位置。评分器会用它自己制作的样本和边界来直接运行这个工具。

边界决定误差

用密集的边界 50,75,100,125,150,200,250,300,400,500,750,1000,1500 重新统计同样的样本,保存到 /root/lt-percentiles/hist-fine.tsv。然后在 /root/lt-percentiles/error.tsv 中写两行。每行是以制表符分隔的三个字段 <이름> <추정 p95> <절대 오차>(占位符依次为名称、估计 p95、绝对误差),名称依次为 coarse、fine,值保留到小数点后第一位。误差是与第 3 步的 true_p95 之差的绝对值。最后在 /root/lt-percentiles/06-note.txt 中写两行 better=<coarse|fine>(占位符为 coarse 或 fine)和 reason=<40자 이상>(占位符为至少 40 个字符)。在 reason 中,请同时写明密集的边界并不是免费的原因。

桶估计的误差,由“目标所在的桶的宽度”决定。请在 250 到 500 为一格的表,和 250 到 300 为一格的表中,分别找出同一个目标。不过边界一增加,时间序列也会相应增加——在 Prometheus 中,一个边界就是一条时间序列。

实际合并两次运行

把 /opt/lab/lt/lt-percentiles/target.py 启动在 8080 端口上(/fast 为 30 毫秒,/slow 为 250 毫秒)。运行 hey 两次,保留原始输出——把 hey -n 300 -c 10 -o csv http://127.0.0.1:8080/fast 保存为 /root/lt-percentiles/run-fast.csv,把 hey -n 100 -c 10 -o csv http://127.0.0.1:8080/slow 保存为 /root/lt-percentiles/run-slow.csv。然后在 /root/lt-percentiles/runs.txt 中写四行——p95_fast=、p95_slow=、mean_of_p95=(两个 p95 的简单平均)、merged_p95=(把两次运行的原始响应时间全部合并后重新求出的 p95)。全部以秒为单位,保留到小数点后第四位,只统计 status-code 为 200 的行。

hey -o csv 的第一个字段是响应时间(秒),第七个字段是状态码。请跳过表头那一行。要直接使用第 2 步的 pct.py,可以用 cut -d, -f1 只取出数字,收集到临时文件里。请注意两次运行的次数不同——可以看出合并后的结果偏向哪一边。

能正确合并多次运行的工具

创建 /root/lt-percentiles/merge_p95.py。以 python3 merge_p95.py <분위> <hey CSV...>(占位符依次为分位、hey CSV 文件)调用时,从这些 CSV 中 status-code 为 200 的行里取出第一个字段(响应时间,秒),全部收集到同一个桶里,用最近秩求出百分位数,保留到小数点后第四位,打印为一行。不能按每次运行分别求百分位数再取平均。把这个工具对第 7 步的两个 CSV 运行,在 /root/lt-percentiles/merged.txt 中写四行——q=0.95、merged_p95=、mean_of_p95=、gap=(merged 减去 mean)。最后在 /root/lt-percentiles/policy.txt 中,用以 rule= 开头、至少 60 个字符的一行,写下今后合并多次压力测试结果时团队应当遵守的规则。

请跳过表头那一行,并丢弃字段少于七个的行。如果不过滤状态码,失败请求的时间也会混进来,答案就会不同。评分器会用它自己制作的两个 CSV,以两种分位来运行这个工具——按每次运行求平均的实现,或者不过滤状态码的实现,会当场被淘汰。