主题
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_wait | 30s(以官方文档为准) | 新分组第一次发送前等多久,用来合并同一批告警 |
group_interval | 5m(以官方文档为准) | 同一分组有变化时的最短发送间隔 |
repeat_interval | 4h(以官方文档为准) | 告警未恢复时的重复提醒间隔 |
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。