指标缺失不等于数值为零。Prometheus 会区分具有近期值的序列、已经标记失效的序列,以及找不到可用样本的选择器;告警又在查询结果之上使用自己的评估和等待规则。如果把这些机制统称为一个宽限时间,就可能漏掉被移除的目标,或者没有弄清到底缺少什么数据便宣布服务故障。准确的缺失告警首先需要准确描述观测对象。
失效标记不是统一的五分钟计时器 即时选择器通常使用回看范围内最新的合格样本,默认回看范围是五分钟。失效标记会告诉引擎:在标记之后,不再让这条序列出现在即时选择结果中。它不要求旧值再保留五分钟。历史样本仍然存在于存储中,也可能继续参与范围查询;因此,即时查询已经看不到某条序列,与历史窗口里完全没有样本,并不是同一件事。
具体采集路径会影响行为。导出器不再输出某个指标、目标从服务发现消失,以及导出器自己提供样本时间戳,不一定遵循相同的时间过程。显式时间戳和失效跟踪配置也会改变结果。在依据抓取周期推测消失时刻之前,应先确认导出器使用哪条路径。可配置的回看范围本质上是查询选择规则,不是能够统一判定所有服务故障的计时器。
先确定要询问哪一种缺失 absent(selector) 在选择器没有任何元素时返回一;只要仍有匹配数据,即使数值为零,也返回空向量。absent_over_time(selector[3m]) 则询问所选三分钟范围内是否存在样本。失效标记之前的真实样本,可能继续留在这个范围内,使范围缺失表达式保持为空,直到该样本移出窗口。这不是查询互相矛盾,而是问题的时间范围不同。
告警规则的 for 又是一层机制:相关标签集必须在连续的评估中持续由表达式返回,经过等待时长后才进入触发状态。它不会配置序列失效,也不会延长查询回看范围。把三分钟缺失窗口再配上额外等待时间,会进一步延后通知,评估节奏和采集时机也影响最终延迟。选择这些参数时,应从允许的发现时间反推,而不是只把它们当作降噪旋钮。
坏掉的样子 absent(up{job="api"}) 不是抓取失败告警。若已配置目标仍存在,只是 up=0,选择器仍有元素,缺失条件便不成立。与零比较才能检查这个场景。但如果目标彻底从发现清单消失,只检查零又可能在其序列消失之后漏报。对目标是否应当存在有要求时,需要另行监测预期成员关系,不能让一个表达式混合承担所有职责。
宽泛的选择器也不会自动列举所有缺少的成员。一个副本仍有数据时,对整个任务求 absent 仍为空,即使其他几个预期副本已消失。可以匹配明确已知的身份,或者将观测序列与权威清单比较。标签被改名同样可能使旧匹配条件看起来像发生故障,而进程其实正在以新身份正常工作,所以规则与标签变更需要一起审阅。
快速验证 把程序保存为 stale.py,在 Docker 可用时执行 python3 stale.py。程序使用公开的 Prometheus 3.5.1 镜像和真实 promtool 查询;镜像未缓存时,Docker 可能先进行下载。输入由显式失效标记、缺失位置和正常零值组成。本实验验证查询语义,不测量真实服务发现系统在墙上时钟中需要多久才移除一个目标。
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: "up{job=\"api\",instance=\"gone\"}" values: "1 0 0 0 0" - series: "service_events_total{job=\"api\",instance=\"gone\"}" values: "10 stale _ _ _" - series: "service_events_total{job=\"api\",instance=\"zero\"}" values: "0 0 0 0 0" promql_expr_test: - expr: "service_events_total{job=\"api\",instance=\"gone\"}" eval_time: 1m exp_samples: [] - expr: "up{job=\"api\",instance=\"gone\"}" eval_time: 1m exp_samples: - labels: "{__name__=\"up\",job=\"api\",instance=\"gone\"}" value: 0 - expr: "absent(service_events_total{job=\"api\",instance=\"gone\"})" eval_time: 1m exp_samples: - labels: "{job=\"api\",instance=\"gone\"}" value: 1 - expr: "absent_over_time(service_events_total{job=\"api\",instance=\"gone\"}[3m])" eval_time: 2m exp_samples: [] - expr: "absent_over_time(service_events_total{job=\"api\",instance=\"gone\"}[3m])" eval_time: 4m exp_samples: - labels: "{job=\"api\",instance=\"gone\"}" value: 1 - expr: "absent(service_events_total{job=\"api\",instance=\"zero\"})" eval_time: 4m exp_samples: [] - expr: "absent(up{job=\"api\"})" eval_time: 4m 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。第一分钟,已经标记失效的业务序列不再被即时选择,但单独提供的 up 序列仍存在,数值为零;即时 absent 返回一。第二分钟,三分钟范围仍包含最初的业务样本,因此 absent_over_time 不返回元素;到了第四分钟,这个样本已不在范围内,表达式才返回一。程序分别断言每种结果,而不是只检查命令退出。
...