Prometheus 直方图保留分桶后的分布信息
延迟平均值可能隐藏一部分特别慢的请求。直方图通过记录落入不同桶的观测数量,保留更多分布信息,但仍然丢弃桶内部的细节。Prometheus 可以从这些计数估计分位数,无需保存每次请求的原始耗时。估计结果取决于桶边界与聚合方式,不能称为完整恢复了原始延迟分布,也不能因为输出包含小数就认为测量具有同样精度。 经典直方图由多条相关序列组成 经典直方图导出累计的 _bucket 计数器,以及 _count 与 _sum。le 标签表示每个桶包含的上界。一点五秒的观测会增加两秒桶和所有更大的桶,所以这些桶不是互不重叠的区间,不能直接相加得到总请求数。正无穷桶包含全部观测,与总计数对应。一个经典直方图因此不是一条可以单独理解的普通序列。 对于非负延迟观测,分别对总和与总计数应用 rate,可以得到每秒累计的耗时和每秒观测数量;两者相除就是以秒为单位的平均延迟。桶速率则描述选定时间范围内的分布。应该先对各条计数器求速率,再合并实例,这样各序列已经观测到的重置能够独立处理,不会在求和时被另一实例的增长掩盖。 先合并桶,再估计总体分位数 需要整个服务的一个分位数时,应在保留 le 的前提下合并桶速率,再调用 histogram_quantile。查询需要区分服务等维度时,也应保留相应标签。参与合并的桶边界要兼容,事件定义也要一致。如果先把 le 消掉,就丢失了经典分位数函数所需的桶身份,留下的数值无法继续表达累计分布。 不要平均各实例的分位数。分位数是分布中的位置,不是可相加的计数,即使各实例请求量完全相同,平均分位数也未必等于总体分位数。合并桶曲线可以具有与任何单个实例都不同的形状。如果问题是两秒以内完成的请求比例,恰好以两秒为边界的桶除以总计数,就可以直接回答这个阈值问题,不必绕到分位数再反推。 摘要指标可以导出客户端预先计算的分位数,这些数值也不能通过求和或平均合成总体分位数。不过,只要事件定义一致,它的总计数与总和仍可用于计算合并平均值。选择指标类型时,应从所需聚合能力出发,而不是仅看面板希望直接显示什么字段。 坏掉的样子 关注的分位数落在很宽的桶内时,输出容易显得比实际测量更精确。经典分位数计算会在选中的桶内插值,却不知道每个观测在桶内的真实位置。应围绕服务目标设置有用的边界,并在升级前审阅桶定义,避免滚动发布期间不同实例混用不兼容方案。事后加密桶边界也不会恢复旧观测已经丢失的细节。 桶或导出器缺失,与没有观测不同。面板上看似改善的分位数,也可能只是最慢实例的指标丢了。解释改善之前,应检查抓取完整性、参与计算的序列和样本数量。观测太稀疏或完全缺失时,分位数可能没有可用含义;把这种结果替换成零延迟,会制造虚假的成功信号,而不是补全测量。 快速验证 保存为 histogram-query.py,运行 python3 histogram-query.py。环境需要已经安装的 promtool,可以从 PATH 找到,或用 PROMTOOL 指定其可执行文件路径。本轮使用 Python 3.12.14 与 promtool 3.14.0。真实查询引擎在临时目录中评估合成样本,不启动服务器、监听端口或容器,也不联系外部服务。 from pathlib import Path from tempfile import TemporaryDirectory import os import shutil import subprocess # Uses a real, installed promtool; no server or Docker runtime is started. promtool = shutil.which(os.environ.get("PROMTOOL", "promtool")) if not promtool: raise SystemExit("Set PROMTOOL to an installed promtool executable") tests = r"""evaluation_interval: 1m tests: - interval: 1m input_series: - series: "api_duration_seconds_bucket{instance=\"fast\",le=\"1\"}" values: "0+95x5" - series: "api_duration_seconds_bucket{instance=\"fast\",le=\"2\"}" values: "0+100x5" - series: "api_duration_seconds_bucket{instance=\"fast\",le=\"5\"}" values: "0+100x5" - series: "api_duration_seconds_bucket{instance=\"fast\",le=\"+Inf\"}" values: "0+100x5" - series: "api_duration_seconds_count{instance=\"fast\"}" values: "0+100x5" - series: "api_duration_seconds_sum{instance=\"fast\"}" values: "0+17x5" - series: "api_duration_seconds_bucket{instance=\"slow\",le=\"1\"}" values: "0+0x5" - series: "api_duration_seconds_bucket{instance=\"slow\",le=\"2\"}" values: "0+0x5" - series: "api_duration_seconds_bucket{instance=\"slow\",le=\"5\"}" values: "0+100x5" - series: "api_duration_seconds_bucket{instance=\"slow\",le=\"+Inf\"}" values: "0+100x5" - series: "api_duration_seconds_count{instance=\"slow\"}" values: "0+100x5" - series: "api_duration_seconds_sum{instance=\"slow\"}" values: "0+400x5" - series: 'tail_duration_seconds_bucket{le="1"}' values: "0+0x5" - series: 'tail_duration_seconds_bucket{le="5"}' values: "0+90x5" - series: 'tail_duration_seconds_bucket{le="+Inf"}' values: "0+100x5" promql_expr_test: - expr: "abs(histogram_quantile(0.99, sum by (le) (rate(api_duration_seconds_bucket[2m]))) - 4.94) < bool 1e-12" eval_time: 5m exp_samples: - labels: "{}" value: 1 - expr: "abs(avg(histogram_quantile(0.99, rate(api_duration_seconds_bucket[2m]))) - 3.385) < bool 1e-12" eval_time: 5m exp_samples: - labels: "{}" value: 1 - expr: "sum(rate(api_duration_seconds_bucket{le=\"2\"}[2m])) / sum(rate(api_duration_seconds_count[2m]))" eval_time: 5m exp_samples: - labels: "{}" value: 0.5 - expr: "abs(sum(rate(api_duration_seconds_sum[2m])) / sum(rate(api_duration_seconds_count[2m])) - 2.085) < bool 1e-12" eval_time: 5m exp_samples: - labels: "{}" value: 1 - expr: 'histogram_quantile(0.99, rate(tail_duration_seconds_bucket[2m]))' eval_time: 5m exp_samples: - labels: '{}' value: 5 """ rules = r"""""" with TemporaryDirectory(prefix="prometheus-query-check-") as directory: root = Path(directory) (root / "tests.yml").write_text(tests, encoding="utf-8") if rules: (root / "rules.yml").write_text(rules, encoding="utf-8") result = subprocess.run([promtool, "test", "rules", "tests.yml"], cwd=root, text=True, capture_output=True, timeout=45) print(result.stdout, end="") if result.returncode: print(result.stderr, end="") raise SystemExit(result.returncode) 成功时输出 SUCCESS。正确合并后的百分之九十九分位数估计是四点九四秒;分别计算两个实例后再平均,得到三点三八五秒,并不是总体结果。满足两秒阈值的观测占一半,合并平均延迟为二点零八五秒。断言对浮点结果采用很小的数值容差,避免要求十进制小数必须具有完全相同的二进制表示。 ...