跳到正文

Prometheus 抓取、存储与告警 ​

一句话:Prometheus 的可靠性由三件事决定——目标是否被正确抓取、样本是否按保留期落盘、告警是否能送到对的人;PromQL 只是查询层,前三步错了,查询写得再对也没用。

适用版本:Prometheus 2.5x / 3.x,Alertmanager 0.2x。默认值以官方文档为准。

为什么需要 ​

默认配置能跑起来,但生产上最常见的故障都很安静:服务发现拿到了新 Pod,relabel 却把地址改错,up 一直是 0;磁盘写满后 TSDB 只保留最近几天,回溯时才发现数据没了;告警规则在响,Alertmanager 的路由却把它送进了没人看的频道。这三类问题不会让进程崩溃,只会让观测在需要时失效。

核心机制 ​

抓取:一个 target 的一生 ​

  • relabel_configs 在抓取前执行,能改 __address__、丢弃整个 target,适合做环境过滤和地址规范化。
  • metric_relabel_configs 在抓取后执行,只影响入库,目标端仍然暴露这些指标;适合丢弃高基数或无关样本。
  • 两者都用 source_labels、regex、action、target_label、replacement 描述规则,常用动作是 keep、drop、replace、labelmap。
  • honor_labels 决定目标自带标签和服务端标签冲突时听谁的;sample_limit 为 0 表示不限制,生产上建议设上限防止单个目标打爆内存。

存储:WAL 保最近,block 保历史 ​

样本先写入内存中的 head block,同时顺序追加 WAL。进程崩溃后,Prometheus 重放 WAL 恢复最近的数据;每约 2 小时把 head 切成一个持久 block,后台再合并小 block。保留期按整块删除,所以实际保留时间可能比配置多出最多一个 block 的跨度。

  • WAL 是崩溃恢复手段,不是备份:磁盘损坏、误删目录都会一起丢。
  • --storage.tsdb.retention.time=15d 是默认值(以官方文档为准);--storage.tsdb.retention.size=0 表示不按大小限制。两者同时设置时先到先删。
  • 长期留存、跨集群查询、多副本去重不在单机 Prometheus 的责任范围内,要接远程写或 Thanos、Mimir 这类系统。

告警:规则负责判定,Alertmanager 负责送达 ​

规则按 evaluation_interval 周期执行,表达式为真后进入 pending,持续 for 指定的时间才变 firing。Alertmanager 收到告警后做三件事:分组、抑制、路由。

配置默认值作用
group_wait30s(以官方文档为准)新分组第一次发送前等多久,用来合并同一批告警
group_interval5m(以官方文档为准)同一分组有变化时的最短发送间隔
repeat_interval4h(以官方文档为准)告警未恢复时的重复提醒间隔
for规则级,无全局默认连续满足多久才 firing,过滤瞬时抖动

路由是一棵树:子路由用 matchers 匹配标签,命中后默认停止,continue: true 才继续匹配兄弟路由。抑制(inhibition)用「源告警存在时压掉目标告警」表达因果关系,比如机房宕机时压掉其下所有实例告警;静默(silence)是按标签临时屏蔽,必须设过期时间。两者都能让告警消失,排障时要先确认不是被它们藏住了。

PromQL 易错点 ​

写法问题正确做法
rate(http_requests_total[1m])窗口内样本太少,结果抖动窗口至少是 scrape_interval 的 4 倍,常用 [5m]
rate(sum(http_requests_total)[5m])先求和再 rate,丢失每条序列的重启信息sum(rate(http_requests_total[5m]))
histogram_quantile(0.99, http_request_duration_seconds)少了 _bucket 和 by (le)histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
直接用 Counter 原始值做减法进程重启后计数器归零,差值为负用 rate() / increase(),它们会处理 reset
除法的分母可能为 0得到 NaN,面板出现空洞加 > 0 过滤,或用 or vector(0) 兜底
序列消失后查询为空分不清「值为 0」和「没有数据」用 absent() / absent_over_time() 单独告警

实战场景 ​

一、一个带 userId 的指标把 Prometheus 打爆 ​

现象:接入新服务的第二天,Prometheus 内存从 6GB 涨到 22GB 后被 OOM kill,重启后 2 小时再次发生;抓取开始报 context deadline exceeded,up 指标抖动,多个大盘查询超过 20 秒。

排查:序列数由标签组合决定,抓取侧的“一个指标”在服务端可能是几十万条序列。

定位层取证据判定
总序列数prometheus_tsdb_head_series 走势与新服务接入时间对齐即可疑
谁贡献的按 __name__ 聚合序列数 Top10单指标占大头即命中
标签基数逐标签统计 distinct 值(userId、orderId、pod 随机名)无上限的标签即元凶
抓取样本量目标 /metrics 响应字节数、prometheus_target_scrapes_exceeded_sample_limit单目标样本爆炸
relabel 是否生效relabel_configs 与 metric_relabel_configs 的实际规则写错前缀等于没写
抓取周期与超时scrape_interval / scrape_timeout大 payload 会让超时误判为“服务挂了”
bash
# 找出序列数最多的指标(在 Prometheus 侧查询)
topk(10, count by (__name__)({__name__=~".+"}))
# 找出某个指标里基数最高的标签
count by (user_id) (http_requests_total)

对症做法:

yaml
# 1. 抓取后丢弃高基数样本:只影响入库,目标端仍然暴露(这是关键区别)
scrape_configs:
  - job_name: order-service
    metric_relabel_configs:
      - source_labels: [__name__]
        regex: "app_http_requests_total"
        action: drop
      # 或者只保留必要维度:把 user_id 这类标签扔掉
      - regex: "user_id"
        action: labeldrop

# 2. 给单个目标设样本上限,宁可拒绝也不打爆内存(0 表示不限制,生产不建议)
sample_limit: 2000000

# 3. relabel_configs 在抓取前用:过滤环境、规范化地址,
#    把不该抓的 target 直接 drop 掉,成本比抓到再丢更低
text
4. 治理原则放在应用侧:高基数值(userId、orderId、原始 URL、异常 message)
   一律进日志或追踪,不进指标标签;
   需要从指标跳到具体请求时用 exemplar,而不是加标签。
5. 新服务接入要过“序列数预算”评审:
   单服务序列数上限(例如 5 万,经验值需校准)+ 标签 distinct 值清单。

复盘要点:判据是“prometheus_tsdb_head_series 的增长曲线可解释”——每次增长都能对应到某个服务或某次埋点。把序列数按服务维度做周报,超预算的埋点直接回滚;这类问题的成本是持续性的,不会随着时间消失。

二、WAL 不是备份:机器坏了,两周监控数据一起没了 ​

现象:Prometheus 所在主机磁盘故障,Pod 调度到新节点后数据为空。团队以为“保留 15 天”,实际这台单机 Prometheus 的 TSDB 目录在本地盘,没有任何备份或远程写;同一时间告警也中断了 40 分钟,因为 Alertmanager 也在同一台机器上。

排查:WAL 与 block 的分工要说清。

定位层取证据判定
WAL 的作用进程崩溃后重放 WAL 恢复最近数据它是崩溃恢复,不是备份
保留策略--storage.tsdb.retention.time(默认 15d)、retention.size(0 表示不限)按整块删除,实际保留可比配置多最多一个 block 跨度
block 周期每约 2 小时把 head 切成持久 block磁盘/目录损坏时全部丢
存储位置本地盘 vs PVC/网络存储本地盘 + 无备份 = 单点
远程写是否配置 remote_write 到 Thanos/Mimir未配置就没有长期留存与跨集群查询
告警链路Alertmanager 是否与 Prometheus 同故障域同机部署会一起失联
yaml
# 存储与告警分域部署 + 远程写
spec:
  containers:
    - name: prometheus
      args:
        - --storage.tsdb.retention.time=15d
        - --storage.tsdb.path=/data
        - --storage.tsdb.wal-compression
      volumeMounts:
        - { name: tsdb, mountPath: /data }        # PVC,且独立于计算节点
      replicas: 2                                  # 多副本抓取同一目标,查询侧去重
volumes:
  - name: tsdb
    persistentVolumeClaim: { claimName: prometheus-tsdb }
yaml
remote_write:
  - url: http://thos-receive.monitoring:9091/receive
    queue_config: { max_samples_per_send: 10000, capacity: 20000 }
    # 远程写积压要单独看指标,否则磁盘满时才发现

复盘要点:判据是“单台 Prometheus 故障时,历史数据可用性与告警连续性都不受影响”,用一次“删掉 Pod 重建”的演练验证。范围也要写清:长期留存、跨集群查询、多副本去重不在单机 Prometheus 的责任范围内,需要接远程写或 Thanos/Mimir;把保留期当备份是常见误解,按整块删除还会带来“实际保留比配置长”的偏差,容量规划要按此预留。

三、告警一直 pending 不触发,触发时又成风暴 ​

现象:两条相反的抱怨在同一天出现。业务方:数据库主从延迟超过阈值 8 分钟,值班没收到通知——规则里 for: 15m,还没到持续时间。平台方:一次网络抖动导致 60 个节点同时不可达,收到 400 多条告警短信,其中 380 条是同一根因的衍生告警。

排查:规则负责判定,Alertmanager 负责送达,两边都要看。

定位层取证据判定
pending 到 firing规则的 for 时长 vs 故障持续时长for 过长就漏报短时故障
评估频率evaluation_interval 与表达式求值耗时求值慢会让告警延迟一个周期
表达式本身用瞬时向量还是区间向量;分母为 0 时的行为比率类表达式在分母为 0 时不产生序列
分组窗口group_wait(默认 30s)、group_interval(5m)、repeat_interval(4h)分组粒度决定“一条还是多条”
路由树matchers 是否命中预期路由,有没有落到默认 receiver漏配会让告警静默进默认渠道
抑制规则inhibit_rules 的 source_matchers/target_matchers/equal没有抑制就有风暴
维护窗口是否有静默(silence)覆盖发布窗口静默要留记录
yaml
# 1. 判定:短时故障用短 for + 高灵敏度指标,长趋势用长 for
- alert: ReplicationLagHigh
  expr: mysql_slave_status_seconds_behind > 10
  for: 30s                 # 原为 15m,短时故障永远不触发
  labels: { severity: p1 }

# 2. 送达:按根因分组 + 抑制衍生告警
route:
  group_by: [alertname, cluster, instance_group]
  group_wait: 15s
  group_interval: 2m
  repeat_interval: 1h
  routes:
    - matchers: [ 'severity="p1"' ]
      receiver: oncall-pager
inhibit_rules:
  - source_matchers: [ 'alertname="NodeDown"' ]
    target_matchers: [ 'severity=~"p2|p3"' ]
    equal: [cluster, instance]        # 节点挂了,节点上的资源类告警全部抑制
text
3. 告警按症状与 SLO 分级:错误率、P99、饱和度对应 p1/p2;
   CPU、GC 是解释性指标,进 p3 或只做面板,不进叫醒人的通道。
4. 每条告警必须带 runbook 链接:看哪个看板、查哪个端点、找谁。
5. 规则的“可触发性”要验证:
   定期注入一次阈值突破,确认从 pending 到 firing 到送达的时间符合预期。
6. 比率类表达式加保护:
   sum(rate(a[5m])) / sum(rate(b[5m])) 在 b 无流量时不产生序列,
   需要 or vector(0) 或改用绝对阈值兜底。

复盘要点:判据是两条时间:“故障发生 → 值班收到”的 P99,和“同一根因的通知条数”。前者由 for + evaluation_interval + group_wait 累加得出,后者由分组与抑制决定。多集群与联邦场景下的告警去重、跨集群查询能力不在单机 Prometheus 内,需要 Thanos/Mimir 这类系统,见 企业级可观测性与故障定位。

最小上手 ​

三个文件:抓取配置、告警规则、Alertmanager 路由。

yaml
# prometheus.yml
global:
  scrape_interval: 30s
  evaluation_interval: 30s
rule_files:
  - rules.yml
alerting:
  alertmanagers:
    - static_configs:
        - targets: ["am:9093"]
scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ["prometheus:9090"]
        labels: {env: prod}
    relabel_configs:
      - {source_labels: [__address__], regex: '([^:]+):.*', target_label: instance, replacement: '$1'}
    metric_relabel_configs:
      - {source_labels: [__name__], regex: 'go_.*', action: drop}
yaml
# rules.yml
groups:
  - name: slo
    rules:
      - alert: HighErrorRate
        expr: |
          sum(rate(http_requests_total{code=~"5.."}[5m]))
            / sum(rate(http_requests_total[5m])) > 0.05
        for: 5m
        labels: {severity: critical}
        annotations: {summary: "5xx 比例超过 5%"}
yaml
route:
  receiver: default
  group_by: [alertname, env]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - matchers: ['severity="critical"']
      receiver: oncall
receivers:
  - name: default
    webhook_configs: [{url: http://127.0.0.1:5001/}]
  - name: oncall
    webhook_configs: [{url: http://127.0.0.1:5002/}]
inhibit_rules:
  - source_matchers: ['alertname="NodeDown"']
    target_matchers: ['severity=~"warning|critical"']
    equal: [env, instance]
bash
promtool check config prometheus.yml
amtool check-config alertmanager.yml
docker network create prom-net
docker run -d --name am --network prom-net -p 9093:9093 \
  -v "$PWD/alertmanager.yml:/etc/alertmanager/alertmanager.yml" \
  prom/alertmanager:v0.27.0
docker run -d --name prom --network prom-net -p 9090:9090 \
  -v "$PWD:/etc/prometheus" prom/prometheus:v2.55.1
curl -s localhost:9090/api/v1/targets | jq '.data.activeTargets[] | {job: .labels.job, health}'

生产动作 ​

  • 抓取覆盖:用 /api/v1/targets 核对每个 job,up == 0 必须为空;scrape_duration_seconds / scrape_interval > 0.8 要告警。
  • 规则执行:prometheus_rule_group_last_duration_seconds 必须小于评估周期;prometheus_rule_evaluation_failures_total 持续增长意味着告警会延迟或漏报;面板上的重查询落成 recording rule。
  • 告警送达:看 alertmanager_notifications_failed_total 和 Alertmanager 的 UI;amtool alert query 查当前告警,amtool silence query 查静默。
  • 保留期与变更:磁盘使用率 70% 预警、85% 处置;扩盘或调小 retention,不要手工删除 block 目录。改配置先 promtool / amtool 校验;--cluster.peer 只解决多副本去重,不解决通知渠道故障。

边界 ​

  • WAL 不是备份:它只覆盖最近未落盘的数据,磁盘损坏或目录误删无法恢复;需要长期数据就配远程写或定期快照。
  • relabel 丢弃不可逆:metric_relabel_configs 丢掉样本后,查询层再也找不回来;先确认没有面板和告警依赖它。
  • Alertmanager 不保证送达:它只负责去重、分组和路由,渠道超时或下游故障会丢通知;关键告警要有外部巡检或第二通道。
  • 抑制和静默会隐藏问题:抑制规则写宽了会压掉真实故障,静默到期前告警一直不出现;变更后要复查。
  • 单机不是高可用:两台 Prometheus 各自采集会得到两份数据,查询层需要去重;单机重启期间告警也不保证准确。

讲清楚 ​

抓取阶段先由服务发现生成目标,relabel 决定抓谁、抓到哪个地址,metric relabel 决定哪些样本入库。样本进内存 head 的同时写 WAL,崩溃后靠 WAL 恢复,约两小时切一个 block,保留期按块删除,所以默认 15 天只是近似。

告警分两段:规则按评估周期判断并等待 for,Alertmanager 再按路由树分组、抑制、发送,默认 repeat_interval 是 4 小时。路由配错时告警确实在响,只是没人收到。

PromQL 最常见的错是先聚合再算速率,以及分位数漏掉 _bucket 和 sum by (le)。记住:Counter 的重启信息只有逐条序列才算得出来,先 rate 再 sum。

把一门技术讲透到既懂也会用