指标缺失不等于数值为零。Prometheus 会区分具有近期值的序列、已经标记失效的序列,以及找不到可用样本的选择器;告警又在查询结果之上使用自己的评估和等待规则。如果把这些机制统称为一个宽限时间,就可能漏掉被移除的目标,或者没有弄清到底缺少什么数据便宣布服务故障。准确的缺失告警首先需要准确描述观测对象。

失效标记不是统一的五分钟计时器

即时选择器通常使用回看范围内最新的合格样本,默认回看范围是五分钟。失效标记会告诉引擎:在标记之后,不再让这条序列出现在即时选择结果中。它不要求旧值再保留五分钟。历史样本仍然存在于存储中,也可能继续参与范围查询;因此,即时查询已经看不到某条序列,与历史窗口里完全没有样本,并不是同一件事。

具体采集路径会影响行为。导出器不再输出某个指标、目标从服务发现消失,以及导出器自己提供样本时间戳,不一定遵循相同的时间过程。显式时间戳和失效跟踪配置也会改变结果。在依据抓取周期推测消失时刻之前,应先确认导出器使用哪条路径。可配置的回看范围本质上是查询选择规则,不是能够统一判定所有服务故障的计时器。

先确定要询问哪一种缺失

absent(selector) 在选择器没有任何元素时返回一;只要仍有匹配数据,即使数值为零,也返回空向量。absent_over_time(selector[3m]) 则询问所选三分钟范围内是否存在样本。失效标记之前的真实样本,可能继续留在这个范围内,使范围缺失表达式保持为空,直到该样本移出窗口。这不是查询互相矛盾,而是问题的时间范围不同。

告警规则的 for 又是一层机制:相关标签集必须在连续的评估中持续由表达式返回,经过等待时长后才进入触发状态。它不会配置序列失效,也不会延长查询回看范围。把三分钟缺失窗口再配上额外等待时间,会进一步延后通知,评估节奏和采集时机也影响最终延迟。选择这些参数时,应从允许的发现时间反推,而不是只把它们当作降噪旋钮。

坏掉的样子

absent(up{job="api"}) 不是抓取失败告警。若已配置目标仍存在,只是 up=0,选择器仍有元素,缺失条件便不成立。与零比较才能检查这个场景。但如果目标彻底从发现清单消失,只检查零又可能在其序列消失之后漏报。对目标是否应当存在有要求时,需要另行监测预期成员关系,不能让一个表达式混合承担所有职责。

宽泛的选择器也不会自动列举所有缺少的成员。一个副本仍有数据时,对整个任务求 absent 仍为空,即使其他几个预期副本已消失。可以匹配明确已知的身份,或者将观测序列与权威清单比较。标签被改名同样可能使旧匹配条件看起来像发生故障,而进程其实正在以新身份正常工作,所以规则与标签变更需要一起审阅。

快速验证

保存为 stale.py,运行 python3 stale.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
rule_files: [rules.yml]
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: []
      - expr: 'absent_over_time(service_events_total{job="api",instance="gone"}[3m])'
        eval_time: 3m
        exp_samples:
          - labels: '{job="api",instance="gone"}'
            value: 1
      - expr: 'ALERTS{alertname="BusinessMetricMissing",alertstate="pending"}'
        eval_time: 3m
        exp_samples:
          - labels: '{__name__="ALERTS",alertname="BusinessMetricMissing",alertstate="pending",job="api",instance="gone"}'
            value: 1
    alert_rule_test:
      - eval_time: 4m
        alertname: BusinessMetricMissing
        exp_alerts: []
      - eval_time: 5m
        alertname: BusinessMetricMissing
        exp_alerts:
          - exp_labels: {job: api, instance: gone}
            exp_annotations: {}
"""
rules = r"""groups:
  - name: absence
    rules:
      - alert: BusinessMetricMissing
        expr: absent_over_time(service_events_total{job="api",instance="gone"}[3m])
        for: 2m
"""

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。第一分钟,已经标记失效的业务序列不再被即时选择,但单独提供的 up 序列仍存在,数值为零;即时 absent 返回一。第二分钟,三分钟范围仍包含最初的业务样本,因此 absent_over_time 不返回元素;到了第三分钟,左开边界就已经排除最初样本,因此返回一,第四分钟仍返回一。程序分别断言每种结果,而不是只检查命令退出。

持续导出零值的业务序列从来不算缺失,整个任务的 up 选择器也不算缺失。这些断言把没有值与值表示失败区分开来,没有把两者都强行补零。临时测试文件会在退出时清理。新增规则在第三分钟进入等待,第四分钟仍未触发,第五分钟才经过独立的两分钟 for 时长而触发。断言同时检查等待状态和触发时点;真实抓取、目标移除及通知送达仍是独立的集成测试,不能从合成时间线推断实际发现延迟。

来源