Prometheus rate 估计计数器每秒的增长速度
请求计数器回答进程累计记录了多少事件,速率则回答这些事件到来得有多快。Prometheus 的 rate 估计指定范围内每秒平均增长量,修正能够观测到的计数器重置,并向范围边界进行外推。结果具有明确的时间单位,适合观察负载变化;但它不是两次抓取之间全部事件的精确还原,更不能凭空补回没有采到的重启历史。 采样决定能够推断多少事实 普通计数器在重置前持续增长,进程重启是常见的重置原因。相邻已观测样本出现下降时,计数器函数会按重置处理。不过,如果两次抓取之间已经重启并重新累计,而且新值超过旧样本,重置可能完全不可见。提高抓取频率可以提升观测分辨率,但任何查询都不能确定地恢复一段没有被记录的历史。 应先对每条计数器序列求 rate,然后聚合结果。如果先求和,一台实例的增长可能掩盖另一台实例的重置。标签决定序列身份,替换实例使用新标签时,是开始另一条序列,不是自动接续旧实例。队列长度等仪表值本来就允许正常下降,把它们交给计数器速率函数,会把真实下降错误解释为重置。 选择的范围也是指标含义的一部分 范围选择器左开右闭。在第五分钟求 [5m] 时,恰好位于第零分钟的样本不在范围内。对普通浮点计数器求速率至少需要两个可用样本;只有一个样本时得到空结果,并不表示业务流量为零。因此,范围不仅要覆盖正常抓取周期,还需要考虑偶发漏抓后的样本数量,不能仅凭希望曲线更灵敏就无限缩短。 较长窗口通常会平滑短促突发,也让突发离开平均值的时间更晚;较短窗口响应更快,但观测更少,而且可能变成空结果。应依据事件持续时间与检测目标选择范围,并在面板上明确标注。irate 使用最后两个样本,rate 使用所选范围,两者可以针对同一指标回答不同问题,只要面板清楚说明各自含义,不把数值当成同一种口径比较。 规则评估周期决定何时提出查询问题,不决定目标已经被抓取了多少次。如果目标每分钟才采集一次,即使面板每秒刷新,也不会凭空产生新的观测。分析告警响应速度时,需要分别检查采集与评估这两个周期,不能只缩短页面刷新时间。 坏掉的样子 刚创建的序列可能要等第二个样本到来后才出现在速率面板。把所有空值都补成零,可能掩盖采集故障。遇到空白,应先检查原始序列、时间戳和抓取状态,再判断业务是否空闲。另一种问题是窗口太长:短时间过载已经影响用户,但平均线被长窗口稀释,看起来始终平稳,告警因而没有达到阈值。 重置修正不代表缺口对准确性没有影响。漏抓可能隐藏突发或多次重启,范围边界的外推本身也属于估计。increase 以事件数量单位表达相应的范围估计,即使原始计数器只有整数,也可能返回小数。如果场景需要精确账务或计费总数,应依靠业务记账记录,不能把监控面板描述成可以完整重建交易流水的工具。 快速验证 把程序保存为 rate.py,执行 python3 rate.py。实验需要 Docker 与 Prometheus 3.5.1 镜像,镜像未缓存时可由 Docker 下载。程序调用真实 promtool 查询引擎,输入每分钟一份的合成样本。一次性容器没有外部网络,也不挂载宿主机目录,结束时会自动删除;运行结果由引擎断言决定,不是程序自己模拟查询公式。 import subprocess import uuid # Requires Docker and the public Prometheus 3.5.1 image. tests = r"""evaluation_interval: 1m tests: - interval: 1m input_series: - series: "demo_requests_total{instance=\"steady\"}" values: "0+60x5" - series: "demo_requests_total{instance=\"restart\"}" values: "100 160 220 20 80 140" - series: "demo_requests_total{instance=\"gaps\"}" values: "0 60 _ 180 _ 300" - series: "demo_requests_total{instance=\"one\"}" values: "_ _ _ _ _ 7" promql_expr_test: - expr: "rate(demo_requests_total{instance=\"steady\"}[5m])" eval_time: 5m exp_samples: - labels: "{instance=\"steady\"}" value: 1 - expr: "abs(rate(demo_requests_total{instance=\"restart\"}[5m]) - 5/6) < bool 1e-12" eval_time: 5m exp_samples: - labels: "{instance=\"restart\"}" value: 1 - expr: "increase(demo_requests_total{instance=\"restart\"}[5m])" eval_time: 5m exp_samples: - labels: "{instance=\"restart\"}" value: 250 - expr: "irate(demo_requests_total{instance=\"restart\"}[5m])" eval_time: 5m exp_samples: - labels: "{instance=\"restart\"}" value: 1 - expr: "rate(demo_requests_total{instance=\"gaps\"}[5m])" eval_time: 5m exp_samples: - labels: "{instance=\"gaps\"}" value: 1 - expr: "rate(demo_requests_total{instance=\"one\"}[5m])" eval_time: 5m exp_samples: [] """ rules = r"""""" script = "set -eu\ncat > /tmp/tests.yml <<'TEST_DATA'\n" + tests + "\nTEST_DATA\n" if rules: script += "cat > /tmp/rules.yml <<'RULE_DATA'\n" + rules + "\nRULE_DATA\n" script += "exec /bin/promtool test rules /tmp/tests.yml\n" name = "xbcatm-prom-unit-" + uuid.uuid4().hex[:12] try: result = subprocess.run( ["docker", "run", "--rm", "--name", name, "--network", "none", "-i", "--entrypoint", "/bin/sh", "prom/prometheus:v3.5.1", "-s"], input=script, text=True, capture_output=True, timeout=45, ) finally: subprocess.run(["docker", "rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) print(result.stdout, end="") if result.returncode: print(result.stderr, end="") raise SystemExit(result.returncode) 断言全部通过时输出 SUCCESS。稳定序列每秒增加一个请求。对于 100 160 220 20 80 140,五分钟 rate 为 5/6,increase 为二百五十,irate 为一。这些结果不同,是因为范围排除了最初样本,随后修正可见重置,再把所得到的平均增长外推到请求时长;二百五十不能被描述为原始样本直接观测到的增量。 ...