Prometheus 指标采集、PromQL 与告警工具手册
up{job="api"} == 1 经常被误读成“服务健康”。它实际只说明最近一次抓取请求成功,目标返回的 exposition 文本也通过了解析。业务指标可能缺字段,label 可能已经失控,告警表达式也可能永远匹配不到。Prometheus 的价值不在于多一个绿色页面,而在于把目标发现、样本写入、查询和告警变成一条能逐段验证的链路。
本文实验固定使用 Prometheus 3.12.0、Alertmanager 0.28.1 与 node_exporter 1.9.1。这是可重复验证的组合,不是永久的“最新版”声明。升级前应查看 Prometheus releases 与 Alertmanager releases,再用本文的配置检查、规则测试、故障恢复和远端积压实验重放候选版本。
先让一个目标真正被抓到
下面的 Compose 环境只暴露本机回环地址。两个 Prometheus 独立抓取同一个 exporter,A 额外把样本异步发送到远端接收器。两个实例不共享数据目录,也不会彼此复制;这正好留下后面验证 HA 边界所需的反例。
# compose.yaml
services:
node-exporter:
image: prom/node-exporter:v1.9.1
ports: ["127.0.0.1:9100:9100"]
prometheus-a:
image: prom/prometheus:v3.12.0
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=7d
- --storage.tsdb.retention.size=2GB
- --web.enable-lifecycle
ports: ["127.0.0.1:9090:9090"]
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./rules:/etc/prometheus/rules:ro
- prom-a-data:/prometheus
prometheus-b:
image: prom/prometheus:v3.12.0
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
ports: ["127.0.0.1:9091:9090"]
volumes:
- ./prometheus-b.yml:/etc/prometheus/prometheus.yml:ro
- ./rules:/etc/prometheus/rules:ro
- prom-b-data:/prometheus
prometheus-remote:
image: prom/prometheus:v3.12.0
command:
- --config.file=/etc/prometheus/empty.yml
- --storage.tsdb.path=/prometheus
- --web.enable-remote-write-receiver
volumes:
- ./empty.yml:/etc/prometheus/empty.yml:ro
- prom-remote-data:/prometheus
alertmanager:
image: prom/alertmanager:v0.28.1
command: ["--config.file=/etc/alertmanager/alertmanager.yml"]
ports: ["127.0.0.1:9093:9093"]
volumes:
- ./alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
webhook:
image: mendhak/http-https-echo:37
ports: ["127.0.0.1:8088:8080"]
volumes:
prom-a-data: {}
prom-b-data: {}
prom-remote-data: {}Prometheus A 的 scrape_configs 决定抓谁,relabel_configs 决定候选目标是否进入抓取队列,metric_relabel_configs 则在样本写入 TSDB 前改写或丢弃标签。两者看起来相似,失败位置完全不同:前者配错会让 target 消失,后者配错时 target 仍为绿色,但所需样本已经被丢掉。
# prometheus.yml
global:
scrape_interval: 15s
scrape_timeout: 10s
evaluation_interval: 15s
external_labels:
cluster: metrics-lab
replica: a
rule_files:
- /etc/prometheus/rules/*.yml
alerting:
alert_relabel_configs:
- regex: replica
action: labeldrop
alertmanagers:
- static_configs:
- targets: ["alertmanager:9093"]
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]
- job_name: node
static_configs:
- targets: ["node-exporter:9100"]
remote_write:
- name: lab-remote
url: http://prometheus-remote:9090/api/v1/write
queue_config:
capacity: 10000
max_samples_per_send: 2000
max_shards: 4B 保留相同的抓取、规则和 Alertmanager 配置,把 external_labels.replica 改为 b,并删除 remote_write。生产环境通常通过配置模板为副本注入不同标签,不会复制两份长配置。远端接收器所用的 empty.yml 只需包含合法的空抓取段。
global:
scrape_interval: 1m
scrape_configs: []启动前用 promtool 拒绝语法错误,再等待 ready 与实际样本同时出现。只检查进程存在,会把“服务启动了”和“数据链可用”混成一件事。
docker compose run --rm --entrypoint promtool prometheus-a check config /etc/prometheus/prometheus.yml
docker compose run --rm --entrypoint promtool prometheus-a check rules /etc/prometheus/rules/app.yml
docker compose up -d
curl -fsS http://127.0.0.1:9090/-/ready
curl -fsS 'http://127.0.0.1:9090/api/v1/query?query=up%7Bjob%3D%22node%22%7D'响应中的值应为 1。如果 Targets 页面显示 connection refused,先检查 Compose 网络里的目标名和端口;宿主机的 localhost:9100 可访问,不代表 Prometheus 容器里的 localhost:9100 指向 exporter。
一个样本为什么会变成容量问题
Prometheus 以 metric name 与完整 label 集合作为时间序列身份,时间戳和值只是这条序列上的样本。Counter 适合用 rate() 或 increase() 观察增量;Gauge 可以上下变化;Classic Histogram 依靠 _bucket、_sum、_count 聚合分布,跨实例计算分位数时要保留 le;Summary 在客户端计算 quantile,通常不能把多个实例的 quantile 再求平均。类型选错之后,PromQL 写得再精巧也无法恢复原始语义。
先在 Table 模式执行几条简单 PromQL,直接查看返回标签,再决定怎样聚合。
up{job="node"}
rate(prometheus_tsdb_head_samples_appended_total[5m])
sum by (job) (scrape_samples_scraped)
job:scrape_samples_scraped:sumsum(rate(...)) 会丢掉所有定位标签,sum by (job, status) 才保留后续告警需要的维度。分子和分母分别聚合后相除,通常也不同于先按实例相除再求平均。遇到表达式争议时,不要在看板上猜,回到 PromQL basics 的 instant vector、range vector、函数与聚合规则,并用一组固定输入做规则单元测试。
基数是所有 label 值域组合后的序列数量。20 个路由、6 个状态、5 个方法和 30 个实例,理论上已经产生 18,000 条序列;再加一个无界 user_id,容量模型就失去上限。用户、订单、trace ID、完整 URL、异常文本和随机请求 ID 不该成为 label。路由应记录模板 /orders/{id},单次请求细节进入日志或 Trace,通过受控关联键跳转。
高基数实验只能放在隔离实例。临时把请求 ID 写入 label,观察 prometheus_tsdb_head_series、Head 内存和写入速率持续增长;删除 label 后,旧序列也不会立刻从已有块中消失。这解释了为什么一次错误埋点能在回滚后继续占用磁盘和查询成本。
规则要同时证明触发和恢复
recording rule 把反复计算的表达式固化为新序列,alerting rule 判断何时需要行动,Alertmanager 才负责分组、抑制、静默和通知。把通知路由写进 PromQL,或者把大量看板表达式原样复制成告警,都会让职责边界失去控制。
# rules/app.yml
groups:
- name: node.rules
interval: 15s
rules:
- record: job:scrape_samples_scraped:sum
expr: sum by (job) (scrape_samples_scraped)
- alert: NodeExporterDown
expr: up{job="node"} == 0
for: 30s
labels:
severity: page
team: platform
annotations:
summary: "node exporter 无法抓取"
runbook_url: "https://runbooks.example.invalid/node-exporter-down"# alertmanager.yml
route:
receiver: lab-webhook
group_by: [alertname, job]
group_wait: 5s
group_interval: 30s
repeat_interval: 4h
receivers:
- name: lab-webhook
webhook_configs:
- url: http://webhook:8080/prometheus
send_resolved: true停止 exporter 后,应依次看到下一次 scrape 把 up 置为 0,规则先进入 Pending,持续覆盖 for 后变成 Firing,最后 webhook 收到通知。恢复 exporter 后还要看到 up=1、活跃告警消失和 resolved 通知。固定睡眠无法说明卡在哪一段,实际脚本应为每个状态设置独立超时,并在超时时分别保存 Targets、Rules、Alerts、Alertmanagers 页面和两端日志。
规则变更除了运行 promtool check rules,还应使用 rule unit testing 固定输入序列、求值时刻、期望标签和告警状态。一次重构如果悄悄丢了 team 标签,语法仍然正确,通知却可能进入默认接收器。
WAL、Head 和块承担不同状态
新样本先进入内存 Head,同时写入 WAL 以便崩溃恢复;稳定数据随后形成块并在后台压缩。本地 TSDB 是单节点存储,不做集群复制,也不应把 NFS 或 EFS 当作可靠本地盘。官方 storage 文档 还提醒:不经 snapshot 直接复制数据目录,可能遗漏仍在 Head 和 WAL 中的数据。
同时设置 retention.time=7d 与 retention.size=2GB 时,先满足的条件触发块删除。size 不是硬磁盘上限,因为 WAL、memory-mapped chunks 与压缩临时空间仍会占盘。容量评估至少要把活跃序列、抓取间隔、每样本实测字节、保留期、WAL、Head、压缩峰值和安全余量放在一起。
每日样本数 = 活跃序列数 × 86400 ÷ scrape_interval_seconds
本地净数据量 ≈ 每日样本数 × 实测每样本字节 × 保留天数
磁盘预算 = 本地净数据量 + WAL + Head + 压缩临时空间 + 安全余量强制停止 A 再启动,日志应出现 WAL replay,/-/ready 在重放完成后恢复,近期序列仍可查询。数据卷无写权限、磁盘耗尽和损坏块会在这里暴露。发生损坏时先保全目录并从快照恢复;删除 WAL 或 block 会造成数据缺口,不能藏在自动启动脚本里。
remote_write 不提供同步副本
remote write 从 WAL 读取已接收样本,经过 series cache、队列和多个 shard 批量发送。远端限流或断网时,本地查询仍可能正常,这正是最容易漏掉的故障:用户看得见短期数据,远端长期库却已经积压。
先确认接收端能查到 A 的 replica="a" 序列,再停止 prometheus-remote。故障窗口内应观察 prometheus_remote_storage_samples_pending 上升、重试计数增长和带退避的日志。恢复远端后,pending 应在容量预算内回落。盲目放大 capacity 或 max_shards 会增加内存,也可能把已经限流的接收端压得更慢。
远端离线超过 WAL 可重放窗口时,旧样本仍会丢失;本地与远端查询也不保证每一时刻一致。迁移远端后端时应先双写,比较序列数、标签、延迟、典型 PromQL 和规则结果,等新端覆盖所需保留窗口后切查询,再停止旧写入。write_relabel_configs 在出口丢掉的样本无法由远端补回,必须把过滤规则也纳入迁移审查。
两个 Prometheus 仍不是完整高可用
A 和 B 独立抓取同一目标,停止 A 后 B 仍能计算规则,说明采集副本有效。但查询客户端如果固定连接 A,仍然会失败;两个副本写入远端会产生重复序列;两边发出的告警如果没有稳定的副本标签处理,也会重复通知。
生产架构还需要稳定查询入口或全局查询层、明确的 replica label 与去重规则、独立故障域、Alertmanager 集群以及远端存储策略。Alertmanager 的多个实例通过 gossip 协调通知状态,每个 Prometheus 应把告警发给全部 Alertmanager 实例,而不是藏在一个随机转发的负载均衡地址后。网络分区时系统倾向于重复通知而不是漏报,这项取舍必须由值班流程接受。
Prometheus 自身也不是多租户权限系统。管理接口、查询接口和 exporter 都不应直接暴露公网;TLS、身份认证、租户授权和网络策略应放在受控代理或平台边界。抓取密码、Bearer token 和 remote write 凭证由 Secret 注入,日志与配置输出必须避免泄露。
升级和退出靠证据,不靠页面仍能打开
升级先让隔离副本加载候选镜像,执行配置与规则测试,比较关键 PromQL、WAL 启动时间、规则求值、remote write 积压和 Alertmanager 通知。Prometheus 3.x 对稳定 API 有明确承诺,但服务发现、精确磁盘格式、内部指标和实验能力不都属于同一级别;依赖这些不稳定面时必须额外做兼容检查。
退出旧实例前,保存规则、配置、告警路由、快照和远端写入位置,确认查询端已经切换,随后停止抓取和写入。实验环境可以先执行 docker compose down 保留数据卷;只有确认目录没有共享数据且不再需要复盘时,才执行 docker compose down -v。真正可迁移的指标体系应保留开放 exposition、PromQL 规则和清晰标签语义,而不是只剩某张无法解释来源的看板。
