仪表盘上的 p90 是 0.48 秒,日志里是 0.42 秒
目标
用桶边界不同的两个版本聚合同样的请求,亲自测量 histogram_quantile 的估算误差,确认丢掉 le 的表达式、混用版本的表达式、去掉 rate 的表达式分别会得出什么数字,然后设计符合 SLO 的桶。
为什么重要
直方图只用桶的计数来记住样本。因此分位数是假定桶内样本均匀分布后做线性插值的结果,误差的大小由排名所落入的那个桶的宽度决定。落在边界之外的尾部则完全看不到。只有能够判断仪表板上的数字与日志不一致时,该怀疑查询还是该怀疑桶设计,才能信任 SLO。
已准备的环境
python3 /opt/fixtures/pca_histogram_lab.py init 会在 /root/pca-histogram/ 中放入 v2 Pod 的请求日志(access-log-v2.csv)和 README。时间序列是 http_request_duration_seconds_bucket/_count/_sum,标签有 route(/checkout、/search)、version(v1、v2)和 le。v1 的边界是 0.1、0.5、1,v2 的边界是 0.05、0.1、0.2、0.3、0.4、0.5、1、2.5。从第 0 分钟到第 60 分钟,每分钟抓取一次,当前时刻是 60m。
python3 /opt/fixtures/pca_histogram_lab.py eval -f 파일 --at 60m(占位符为文件名)使用 lab-k8s 镜像中 promtool 3.0.1 的 PromQL 引擎对表达式求值。不会启动 Prometheus 服务器,评分器也用同一个引擎重新计算你的表达式和基准表达式,并与你写下的数字核对。
步骤
- 从
/root/pca-histogram/access-log-v2.csv中选出 /checkout 第 56 分钟到第 60 分钟的 100 个请求,排序后取第ceil(q×N)个值(nearest-rank)来求 p90 和 p99。在/root/pca-histogram/01-raw.txt中用raw_p90=和raw_p99=两行(以秒为单位)写下。如果没有材料,先运行python3 /opt/fixtures/pca_histogram_lab.py init。 - 在
/root/pca-histogram/02-v1-p90.promql中写出 version="v1"、route="/checkout" 最近 5 分钟的 p90 表达式(使用histogram_quantile(0.9, ...)、rate(...[5m]),并保留le)。用python3 /opt/fixtures/pca_histogram_lab.py eval -f /root/pca-histogram/02-v1-p90.promql --at 60m查看结果,并在/root/pca-histogram/02-v1.txt中写下v1_p90=和error=(v1_p90 减去 raw_p90 的值)。 - 在
/root/pca-histogram/03-v2-p90.promql中用 version="v2" 写出同样的表达式并确认结果。在/root/pca-histogram/03-v2.txt中写下v2_p90=、error=(v2_p90 减去 raw_p90 的值)和closer=(更接近日志 p90 的版本,v1 或 v2)。 - 用 eval 确认 v1、v2 两个版本 /checkout 最近 5 分钟的 p99,并在
/root/pca-histogram/04-slow.promql中写出表达式:用increase(..._bucket{le="+Inf"}[5m])减去le="1"桶,得到 v1 /checkout 最近 5 分钟内超过 1 秒的请求数(两者的le值不同,所以需要匹配条件)。在/root/pca-histogram/04-p99.txt中写下v1_p99=、v2_p99=和slow_requests=。 - 在
/root/pca-histogram/05-broken.promql中写出用sum by (route)丢掉 le 的 v2 p90 表达式,在/root/pca-histogram/05-fixed.promql中写出同时保留 le 和 route 的 v2 p90 表达式。对这两个表达式执行 eval,并在/root/pca-histogram/05-le.txt中写下broken_series=(结果时间序列的数量)、fixed_series=和search_p90=(/search 的值)。 - 金丝雀(Canary)发布期间,仪表板不区分 version,而是把 /checkout 的桶合并在一起。在
/root/pca-histogram/06-mixed.promql中写出不选 version、只选 route="/checkout" 的 p90 表达式,并用count by (le) (http_request_duration_seconds_bucket{route="/checkout"})统计 le 值有多少种。在/root/pca-histogram/06-mixed.txt中写下mixed_p90=和distinct_le=。 - 在
/root/pca-histogram/07-lifetime.promql中写出不使用 rate、直接把 v2 /checkout 的累积桶相加的 p90 表达式。在/root/pca-histogram/07-lifetime.txt中写下lifetime_p90=和window_p90=(第 3 步中最近 5 分钟的 v2 p90),比较第 31 分钟开始变慢这件事在哪一边更容易看出来。 - 在
/root/pca-histogram/08-buckets.txt中用一行写出新的桶边界(升序,有限边界不超过 10 个,不写 +Inf)。条件有三个——包含 SLO 边界 0.3,最大的有限边界在最大延迟 1.8 秒以上,与日志 p90 的误差在 0.01 以下。运行python3 /opt/fixtures/pca_histogram_lab.py design查看用新边界重新聚合同样请求的结果,并在/root/pca-histogram/08-design.txt中写下designed_p90=和under_300ms_ratio=。
参考
histogram_quantile在桶内做线性插值。如果排名落在 +Inf 桶上,它会返回最大的有限边界。- 在
sum by (...)中去掉 le,不会报错,而是得到空结果。 - 常见错误:在第 1 步写下插值分位数(如 0.425 这样的值);在第 4 步不加
ignoring(le)直接相减,结果得到空结果。 - 这份数据是每分钟重复同一分布的合成数据。误差的大小是在这个分布上测得的值,不是一般规律。
- Histograms and summaries、histogram_quantile
从日志中数出真正的 p90、p99
从 /root/pca-histogram/access-log-v2.csv 中选出 /checkout 第 56 分钟到第 60 分钟的 100 个请求,排序后取第 ceil(q×N) 个值(nearest-rank)来求 p90 和 p99。在 /root/pca-histogram/01-raw.txt 中用 raw_p90= 和 raw_p99= 两行(以秒为单位)写下。如果没有材料,先运行 python3 /opt/fixtures/pca_histogram_lab.py init。
使用线性插值的工具(如 numpy 的默认值)会返回两个样本之间的值。这里要选出与排名对应的那一个实际样本。
粗桶(v1)给出的 p90
在 /root/pca-histogram/02-v1-p90.promql 中写出 version="v1"、route="/checkout" 最近 5 分钟的 p90 表达式(使用 histogram_quantile(0.9, ...)、rate(...[5m]),并保留 le)。用 python3 /opt/fixtures/pca_histogram_lab.py eval -f /root/pca-histogram/02-v1-p90.promql --at 60m 查看结果,并在 /root/pca-histogram/02-v1.txt 中写下 v1_p90= 和 error=(v1_p90 减去 raw_p90 的值)。
分位数在桶内做线性插值。v1 在 0.1 和 0.5 之间没有边界,所以会假定整个区间内的样本均匀分布。
与细桶(v2)比较
在 /root/pca-histogram/03-v2-p90.promql 中用 version="v2" 写出同样的表达式并确认结果。在 /root/pca-histogram/03-v2.txt 中写下 v2_p90=、error=(v2_p90 减去 raw_p90 的值)和 closer=(更接近日志 p90 的版本,v1 或 v2)。
v2 在 0.4 和 0.5 处有边界。排名所落入的桶越窄,插值误差的上限就越小。
被困在最后一个有限边界上的 p99
用 eval 确认 v1、v2 两个版本 /checkout 最近 5 分钟的 p99,并在 /root/pca-histogram/04-slow.promql 中写出表达式:用 increase(..._bucket{le="+Inf"}[5m]) 减去 le="1" 桶,得到 v1 /checkout 最近 5 分钟内超过 1 秒的请求数(两者的 le 值不同,所以需要匹配条件)。在 /root/pca-histogram/04-p99.txt 中写下 v1_p99=、v2_p99= 和 slow_requests=。
如果排名落在 +Inf 桶上,histogram_quantile 会返回最大的有限边界。两条桶时间序列只有 le 标签不同,所以需要 ignoring(le)。
修复丢掉 le 的仪表板
在 /root/pca-histogram/05-broken.promql 中写出用 sum by (route) 丢掉 le 的 v2 p90 表达式,在 /root/pca-histogram/05-fixed.promql 中写出同时保留 le 和 route 的 v2 p90 表达式。对这两个表达式执行 eval,并在 /root/pca-histogram/05-le.txt 中写下 broken_series=(结果时间序列的数量)、fixed_series= 和 search_p90=(/search 的值)。
histogram_quantile 只会把带有 le 标签的时间序列当作桶来读取。没有报错却得到空结果,是最危险的形式。
合并边界不同的两个版本
金丝雀(Canary)发布期间,仪表板不区分 version,而是把 /checkout 的桶合并在一起。在 /root/pca-histogram/06-mixed.promql 中写出不选 version、只选 route="/checkout" 的 p90 表达式,并用 count by (le) (http_request_duration_seconds_bucket{route="/checkout"}) 统计 le 值有多少种。在 /root/pca-histogram/06-mixed.txt 中写下 mixed_p90= 和 distinct_le=。
合并结果的 le 是两个版本边界的并集。在只有一边才有的边界处,累积计数会被扭曲,引擎只能强行让它保持单调。
不用 rate 直接读取累积桶
在 /root/pca-histogram/07-lifetime.promql 中写出不使用 rate、直接把 v2 /checkout 的累积桶相加的 p90 表达式。在 /root/pca-histogram/07-lifetime.txt 中写下 lifetime_p90= 和 window_p90=(第 3 步中最近 5 分钟的 v2 p90),比较第 31 分钟开始变慢这件事在哪一边更容易看出来。
计数器的累积值包含进程启动以来的全部请求。变慢之前的 30 分钟混进来,就会稀释最近的变化。
按 SLO 重新设计桶
在 /root/pca-histogram/08-buckets.txt 中用一行写出新的桶边界(升序,有限边界不超过 10 个,不写 +Inf)。条件有三个——包含 SLO 边界 0.3,最大的有限边界在最大延迟 1.8 秒以上,与日志 p90 的误差在 0.01 以下。运行 python3 /opt/fixtures/pca_histogram_lab.py design 查看用新边界重新聚合同样请求的结果,并在 /root/pca-histogram/08-design.txt 中写下 designed_p90= 和 under_300ms_ratio=。
误差来自排名所落入的桶的宽度。不必处处加密,只需把 p90 所在的区间和 SLO 边界收窄即可。