整体架构与核心组件
上一章我们知道了 Prometheus 「主动去拉指标」。这一章拆开机箱,看看里面到底有哪些零件、数据是怎么一路流过去的。
先记住一个关键的设计哲学:Prometheus 生态是一堆各司其职的独立进程,而不是一个巨型单体。 Server 只管抓取、存储和查询;告警的路由与通知交给 Alertmanager;采集适配交给 Exporter。这种解耦让每个组件都能独立扩缩容和升级。
1. 全景图
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 你的应用 │ │ node_exporter │ │ mysqld_export│ ← 各种 target
│ /metrics │ │ /metrics │ │ /metrics │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└────────┬────────┴─────────────────┘
│ HTTP GET /metrics(Pull,默认 15s 一次)
▼
┌───────────────────────────────────────────┐
│ Prometheus Server │
│ ┌─────────────┐ │
│ │ Retrieval │ ← Service Discovery │
│ │ (抓取器) │ (K8s / Consul / file) │
│ └──────┬──────┘ │
│ ▼ │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ TSDB │◀──▶│ Rule Evaluator │ │
│ │ (存储) │ │ (规则求值) │ │
│ └──────┬──────┘ └────────┬─────────┘ │
│ ▼ │ │
│ ┌─────────────┐ │ │
│ │ HTTP API / │ │ │
│ │ Web UI │ │ │
│ └──────┬──────┘ │ │
└─────────┼────────────────────┼────────────┘
│ │ 告警(Alert)
▼ ▼
┌────────────┐ ┌──────────────┐
│ Grafana │ │ Alertmanager │
│ PromQL 查询│ │ 分组/抑制/静默│
└────────────┘ └──────┬───────┘
▼
邮件 / 钉钉 / Slack / Webhook
┌──────────────┐
│ 短任务/批处理 │──push──▶ Pushgateway ──被 Prometheus 抓取──┐
└──────────────┘ │
(汇入上方 Retrieval)只跑一个 Prometheus Server 就能工作(自己抓自己)。Exporter、Alertmanager、Pushgateway、Grafana 都是按需加入的。理解这一点能帮你在部署时不被复杂度吓到。
2. Prometheus Server 内部
Server 是单个 Go 二进制文件,内部由几个协作的模块组成。
2.1 Retrieval(抓取器)
抓取器是整个系统的心脏。它维护一份「目标列表(targets)」,为每个目标启动一个独立的抓取循环:
- 按
scrape_interval定时触发,并对每个目标做抖动(jitter),避免上千个目标在同一毫秒同时被抓。 - 发起
GET /metrics请求,带上scrape_timeout超时限制。 - 解析返回的文本格式,得到一批样本。
- 为每个样本自动附加两个标签:
job(来自配置里的job_name)和instance(目标的 host:port)。 - 执行
relabel_configs/metric_relabel_configs(改写、丢弃标签或整条序列)。 - 写入 TSDB,同时写入一条
up指标记录本次抓取是否成功。
除了 up,每次抓取还会自动生成这几个「元指标」,排查抓取问题时非常有用:
up # 1=成功 0=失败
scrape_duration_seconds # 本次抓取耗时
scrape_samples_scraped # 本次抓回多少个样本
scrape_samples_post_metric_relabeling # relabel 过滤后剩多少
scrape_series_added # 本次新增了多少条序列如果目标偶尔抓不到,先看 scrape_duration_seconds 是不是逼近了 scrape_timeout。一个暴露了几万条序列的应用,生成 /metrics 响应可能要好几秒。要么优化指标数量,要么调大超时(但超时必须小于抓取间隔)。
2.2 TSDB(本地时序数据库)
TSDB 是 Prometheus 自研的存储引擎,采用「内存 + WAL + 磁盘块」的经典组合:
- Head Block(内存):最近约 2 小时的数据放在内存里,写入极快,查询也最快。
- WAL(Write-Ahead Log):内存里的数据同时顺序追加写入磁盘的预写日志。进程崩溃重启时,通过重放 WAL 恢复内存数据,保证不丢。
- 持久化块(Block):每 2 小时,Head 里的数据被压实(compact)成一个不可变的磁盘块,目录以 ULID 命名,内含索引与压缩后的 chunk 文件。
- 压实(Compaction):后台会把多个小块合并成更大的块(2h → 6h → 24h …),减少文件数量、提升查询效率。
- 过期(Retention):超过保留期(默认 15 天)的整个块被直接删除——因为块是按时间切分的,删除是 O(1) 的目录删除,不需要扫描。
Prometheus 的 TSDB 只写本地磁盘,没有内置的集群或复制机制。做高可用的常规做法是「跑两个配置完全相同的实例」,做长期存储则要通过 remote_write 把数据同时写到 Thanos / Mimir / VictoriaMetrics。这是刻意的设计取舍:保证单机极其简单可靠。
2.3 Rule Evaluator(规则求值器)
它按 evaluation_interval(默认 15s)周期性地执行你定义的规则,规则分两类:
- Recording Rule(记录规则):把一个复杂查询的结果预计算成一条新序列存回 TSDB。用于加速看板——把每次都要算 30 秒的查询变成直接读一条现成序列。
- Alerting Rule(告警规则):求值一个布尔表达式,结果非空即视为触发。配合
for字段做「持续 N 分钟才真正告警」的去抖动。
groups:
- name: example
rules:
# 记录规则:预计算各 job 的 QPS
- record: job:http_requests:rate5m
expr: sum by (job) (rate(http_requests_total[5m]))
# 告警规则:实例宕机
- alert: InstanceDown
expr: up == 0
for: 5m # 持续 5 分钟才真正告警
labels:
severity: critical
annotations:
summary: "{{ $labels.instance }} 已宕机超过 5 分钟"
description: "job={{ $labels.job }} 的实例 {{ $labels.instance }} 抓取失败"瞬时抖动(一次网络超时)不该半夜把人叫醒。for: 5m 表示表达式必须连续 5 分钟都为真,告警才从 Pending 状态转为 Firing 并推送给 Alertmanager。这是最简单也最有效的降噪手段。
2.4 HTTP API 与 Web UI
Server 在 9090 端口暴露一个 HTTP 服务,主要包括:
| 路径 | 用途 |
|---|---|
/ 、/graph | Web UI,输入 PromQL 查询并画图 |
/targets | 查看所有抓取目标的状态(UP / DOWN、最后抓取时间、错误原因) |
/rules | 查看已加载的记录规则与告警规则 |
/alerts | 查看当前处于 Pending / Firing 的告警 |
/config | 查看当前生效的完整配置 |
/metrics | Prometheus 自己的指标(它会抓自己) |
/api/v1/query | 瞬时查询 API |
/api/v1/query_range | 区间查询 API(Grafana 画图用的就是它) |
/-/reload | 热加载配置(需启动时加 --web.enable-lifecycle) |
# 瞬时查询:当前有哪些目标挂了
curl -s 'http://localhost:9090/api/v1/query?query=up==0' | jq
# 区间查询:最近 1 小时的 QPS,每 60 秒一个点
curl -s 'http://localhost:9090/api/v1/query_range' \
--data-urlencode 'query=sum(rate(http_requests_total[5m]))' \
--data-urlencode "start=$(date -u -v-1H +%s)" \
--data-urlencode "end=$(date -u +%s)" \
--data-urlencode 'step=60' | jq原生 Web UI 功能很朴素:没有仪表盘、没有权限、图表也简陋。它的定位是调试 PromQL 和排查抓取问题。生产环境的展示层交给 Grafana。
3. Exporter:把「别人的指标」翻译成 Prometheus 格式
Prometheus 只认一种输入:一个返回特定文本格式的 HTTP 接口。可现实世界里 MySQL、Redis、Nginx、Linux 内核都不会主动说这门语言。
Exporter 就是翻译官:它去用目标系统自己的方式(执行 SQL、读 /proc、调 Redis INFO 命令)取数据,转换成 Prometheus 文本格式,暴露在自己的 /metrics 上。
┌──────────┐ SHOW GLOBAL STATUS ┌───────────────┐ /metrics ┌────────────┐
│ MySQL │◀──────────────────────│ mysqld_exporter│◀──────────│ Prometheus │
└──────────┘ └───────────────┘ └────────────┘常见 Exporter 与默认端口:
| Exporter | 端口 | 采集内容 |
|---|---|---|
node_exporter | 9100 | Linux/Unix 主机:CPU、内存、磁盘、网络、文件系统 |
mysqld_exporter | 9104 | MySQL:连接数、QPS、慢查询、主从延迟 |
redis_exporter | 9121 | Redis:内存、命中率、连接数、键数量 |
blackbox_exporter | 9115 | 黑盒探测:HTTP/TCP/ICMP/DNS 可达性与证书有效期 |
nginx-prometheus-exporter | 9113 | Nginx 连接数与请求数 |
kube-state-metrics | 8080 | K8s 对象状态:Deployment 副本数、Pod 阶段 |
cAdvisor | 8080 | 容器级 CPU / 内存 / 网络 |
| Pushgateway | 9091 | 短任务指标中转(见下文) |
如果是你自己写的服务,不需要额外 Exporter——直接引入官方客户端库(Go / Java / Python / Node.js 等),在代码里定义指标,库会自动帮你在 /metrics 上暴露。这叫直接埋点(direct instrumentation),比 Exporter 更准确、更贴近业务。
3.1 /metrics 文本格式
这是整个生态的「通用语」,规则极简:一行一个样本,以 # 开头的是元信息注释。
# HELP http_requests_total Total number of HTTP requests.
# TYPE http_requests_total counter
http_requests_total{method="GET",status="200"} 1027
http_requests_total{method="GET",status="500"} 3
http_requests_total{method="POST",status="200"} 218
# HELP process_resident_memory_bytes Resident memory size in bytes.
# TYPE process_resident_memory_bytes gauge
process_resident_memory_bytes 3.4275328e+07
# HELP http_request_duration_seconds Request latency histogram.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.1"} 900
http_request_duration_seconds_bucket{le="0.5"} 1200
http_request_duration_seconds_bucket{le="1"} 1240
http_request_duration_seconds_bucket{le="+Inf"} 1248
http_request_duration_seconds_sum 187.3
http_request_duration_seconds_count 1248要点:
# HELP <指标名> <描述>:人类可读的说明,会显示在 Web UI 里。# TYPE <指标名> <类型>:类型为counter/gauge/histogram/summary/untyped。- 样本行格式:
指标名{标签...} 值 [毫秒时间戳]。时间戳通常省略,让 Prometheus 用抓取时刻作为时间戳(这是推荐做法)。 - 值是 float64,支持科学计数法(
3.4275328e+07),也支持NaN、+Inf、-Inf。 - 无标签的指标可以直接写指标名和值。
# 亲手看一眼真实的输出
curl -s http://localhost:9100/metrics | grep -A2 '^# TYPE node_load1'4. Alertmanager:告警的路由与降噪
一个常见的误解是「Prometheus 会发告警邮件」。不会。 Prometheus 只负责判断告警是否触发,然后把告警事件通过 HTTP 推给 Alertmanager。剩下的事全是 Alertmanager 的:
| 能力 | 解决的问题 |
|---|---|
| 分组(Grouping) | 100 台机器同时磁盘告警,合并成 1 条通知,而不是 100 封邮件 |
| 抑制(Inhibition) | 整个机房断网时,抑制掉该机房内所有应用的「服务不可用」告警 |
| 静默(Silence) | 计划内维护窗口期间,临时屏蔽指定标签的告警 |
| 去重(Deduplication) | 两个 HA 的 Prometheus 发来同一条告警,只通知一次 |
| 路由(Routing) | 按标签把告警发给不同的人:severity=critical 打电话,warning 发 Slack |
| 重复通知控制 | 问题一直没解决时,每隔 4 小时提醒一次,而不是每 15 秒 |
# alertmanager.yml 骨架
route:
group_by: ['alertname', 'cluster']
group_wait: 30s # 首次触发后等 30s,攒一批一起发
group_interval: 5m # 同组内有新告警时,最快 5m 再发一次
repeat_interval: 4h # 同一告警未恢复时,4h 重复提醒
receiver: 'default-mail'
routes:
- matchers:
- severity = critical
receiver: 'oncall-webhook'
inhibit_rules:
# critical 存在时,压制同一实例上的 warning
- source_matchers: [severity = critical]
target_matchers: [severity = warning]
equal: ['alertname', 'instance']
receivers:
- name: 'default-mail'
email_configs:
- to: 'ops@example.com'
- name: 'oncall-webhook'
webhook_configs:
- url: 'http://dingtalk-bridge:8060/dingtalk/webhook/send'Alertmanager 推给 Webhook 的载荷长这样,自己写钉钉/飞书桥接时会用到:
{
"receiver": "oncall-webhook",
"status": "firing",
"alerts": [
{
"status": "firing",
"labels": {
"alertname": "InstanceDown",
"instance": "10.0.0.12:9100",
"job": "node",
"severity": "critical"
},
"annotations": {
"summary": "10.0.0.12:9100 已宕机超过 5 分钟"
},
"startsAt": "2024-05-01T10:00:00.000Z",
"endsAt": "0001-01-01T00:00:00Z",
"fingerprint": "a1b2c3d4e5f60718"
}
],
"groupLabels": { "alertname": "InstanceDown" },
"commonLabels": { "severity": "critical", "job": "node" },
"externalURL": "http://alertmanager:9093"
}两个原因。其一是高可用:多个 Prometheus 实例(含 HA 副本)可以共用同一组 Alertmanager,由它统一去重;Alertmanager 自身也能组成 gossip 集群互相同步状态。其二是职责分离:告警「什么时候触发」是数据问题(Prometheus 擅长),「发给谁、怎么发、要不要合并」是流程问题(跟数据无关)。混在一起会让两边都变复杂。
5. Pushgateway:短任务的中转站
上一章提到 Pull 模型对付不了「跑几秒就退出」的任务。Pushgateway 是官方给出的答案:一个长期运行的、可以被推送的缓存。
CronJob (30s 后退出) ──POST──▶ Pushgateway ──被 Prometheus 抓取──▶ TSDB
(常驻,一直持有最后一次推送的值)# 备份脚本结束时上报结果
cat <<EOF | curl --data-binary @- http://pushgateway:9091/metrics/job/db_backup/instance/db01
# TYPE backup_last_success_timestamp_seconds gauge
backup_last_success_timestamp_seconds $(date +%s)
# TYPE backup_duration_seconds gauge
backup_duration_seconds 128.4
# TYPE backup_size_bytes gauge
backup_size_bytes 4823449600
EOF
# 删除某个 job 的所有指标(任务下线时清理)
curl -X DELETE http://pushgateway:9091/metrics/job/db_backup/instance/db01然后基于它写告警——「备份超过 26 小时没成功」:
time() - backup_last_success_timestamp_seconds > 26 * 3600- 它不会过期数据。推上去的值会永远保留,直到你显式 DELETE 或重启它。所以一个已经下线的任务会一直上报「陈旧的成功状态」,制造假象。
- 它是单点。所有短任务的数据都经过它,它挂了这部分数据就断了。
- 它会掩盖任务失活。Prometheus 抓的是 Pushgateway(一直 UP),所以
up指标反映的是 Pushgateway 的健康,不是任务的。判断任务是否还活着,必须靠上面那种「最后成功时间戳」的写法。
结论:Pushgateway 只用于短生命周期的批处理任务,绝不要拿它当通用的 Push 入口。 常驻服务永远应该直接暴露 /metrics。
6. Service Discovery:让目标列表自己维护自己
如果目标是 3 台固定的机器,手写 IP 就够了。但在 K8s 里 Pod 随时被销毁重建、IP 每次都变,手写配置根本不可行。
服务发现(SD) 让 Prometheus 从外部系统动态获取目标列表:
| SD 类型 | 数据来源 | 典型场景 |
|---|---|---|
static_configs | 配置文件里写死 | 少量固定机器、自监控 |
file_sd_configs | 监听本地 JSON/YAML 文件变化 | 用 CMDB 脚本生成文件,通用性最强 |
kubernetes_sd_configs | 查询 K8s API Server | 容器环境,最常用 |
consul_sd_configs | Consul 服务目录 | 微服务注册中心 |
dns_sd_configs | DNS SRV / A 记录 | 有内部 DNS 的环境 |
ec2/gce/azure_sd_configs | 云厂商 API | 云主机自动纳管 |
scrape_configs:
# 1) 静态:写死地址
- job_name: 'node'
static_configs:
- targets: ['10.0.0.11:9100', '10.0.0.12:9100']
labels:
env: prod
# 2) 文件发现:外部脚本写这个文件,Prometheus 自动感知变化,无需重启
- job_name: 'app-from-cmdb'
file_sd_configs:
- files: ['/etc/prometheus/targets/*.json']
refresh_interval: 30s
# 3) K8s 发现:自动纳管所有带注解的 Pod
- job_name: 'k8s-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
# 只保留注解 prometheus.io/scrape=true 的 Pod
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
# 把 Pod 的 namespace 提升为正式标签
- source_labels: [__meta_kubernetes_namespace]
target_label: namespacefile_sd_configs 读取的 JSON 文件格式:
[
{
"targets": ["10.0.1.5:8080", "10.0.1.6:8080"],
"labels": { "job": "order-service", "env": "prod", "region": "bj" }
},
{
"targets": ["10.0.2.5:8080"],
"labels": { "job": "order-service", "env": "staging", "region": "sh" }
}
]服务发现返回的原始元数据都带 __meta_ 前缀(如 __meta_kubernetes_pod_name),并且默认会被丢弃。relabel_configs 就是在抓取前对这些元数据做加工的流水线:用 keep/drop 过滤掉不想要的目标,用 replace 把元数据提升成正式标签。可以说,没有 relabel 的服务发现是不完整的。
7. 完整数据流:从 target 到通知
把所有零件串起来,一条数据的完整旅程是这样的:
- 发现目标:Service Discovery(或静态配置)产出一批 target,每个 target 是一个
host:port+ 一堆__meta_元标签。 - relabel 加工:执行
relabel_configs,过滤掉不需要的 target,把元标签改写成正式标签,确定最终的抓取地址与路径。 - 抓取(scrape):Retrieval 模块按
scrape_interval发起GET /metrics,超时受scrape_timeout约束。 - 解析与打标:解析文本格式为样本,自动附加
job和instance标签,并写入up、scrape_duration_seconds等元指标。 - metric_relabel 过滤:执行
metric_relabel_configs,在入库前丢弃高基数或无用的序列(这是控制成本最有效的一道闸门)。 - 写入 TSDB:样本先写 WAL(保证崩溃不丢),再进 Head Block(内存);2 小时后压实成磁盘 block。
- 分叉一:查询。Grafana 或 Web UI 通过
/api/v1/query_range发起 PromQL 查询,TSDB 用倒排索引定位序列、按时间范围取样本、执行 PromQL 引擎,返回结果画图。 - 分叉二:规则求值。Rule Evaluator 每
evaluation_interval跑一遍所有规则:- 记录规则 → 结果写回 TSDB,变成一条新序列。
- 告警规则 → 表达式结果非空则告警进入
Pending;持续满足for时长后转为Firing。
- 推送告警:
Firing状态的告警被持续(默认每分钟)POST 给 Alertmanager 的/api/v2/alerts。 - Alertmanager 处理:去重 → 按
group_by分组 → 等待group_wait攒批 → 检查抑制规则和静默规则 → 匹配路由树找到 receiver。 - 发出通知:调用邮件 / Slack / Webhook 等 receiver 发送;未恢复的告警按
repeat_interval重复提醒。 - 恢复:表达式不再满足时,Prometheus 发送
endsAt让告警resolved,Alertmanager 视配置发送恢复通知。
target → scrape → relabel → TSDB → 分两路:一路 → PromQL 查询 → Grafana,另一路 → 告警规则求值 → Alertmanager → 分组/抑制/路由 → 通知。
下面 5 个需求,各自应该由哪个组件负责?简述理由。
- 采集 Linux 主机的磁盘剩余空间
- 让「同一机房 50 台机器同时告警」只发一条钉钉消息
- 让每天凌晨的数据清洗任务把执行结果上报到监控系统
- 判断「5xx 错误率连续 10 分钟超过 1%」
- K8s 集群扩容后,新 Pod 自动被纳入监控
给定下面这段 /metrics 输出:
# HELP app_queue_depth Current number of jobs in queue.
# TYPE app_queue_depth gauge
app_queue_depth{queue="email",priority="high"} 12
app_queue_depth{queue="email",priority="low"} 340
app_queue_depth{queue="sms",priority="high"} 0
# HELP app_jobs_processed_total Total jobs processed.
# TYPE app_jobs_processed_total counter
app_jobs_processed_total{queue="email",result="ok"} 98213
app_jobs_processed_total{queue="email",result="fail"} 47回答:
- 这段文本一共包含几条时间序列?
- Prometheus 抓取后,每条序列上实际会有哪些标签(比配置文件里多了什么)?
- 写一条 PromQL,查询所有队列的总积压量(按 queue 分组)。
你有一个每天 02:00 执行的数据库备份脚本,正常耗时约 10 分钟。要求:
- 备份失败或超过 26 小时未成功,要告警。
- 备份耗时超过 30 分钟,要告警(可能出问题了)。
请给出:上报方式(推什么指标、怎么推)+ 两条告警规则。
有人提议:「告警规则都在 Prometheus 里判断了,为什么不干脆让它直接发邮件,非要多部署一个 Alertmanager?」
请从高可用和职责分离两个角度说明这样做的问题。
小结
- Prometheus 生态是多个独立进程协作,Server 是唯一必需组件。
- Server 内部:Retrieval 抓取(附带
up等元指标)、TSDB 存储(内存 Head + WAL + 2h 磁盘块)、Rule Evaluator 求值规则、HTTP API / Web UI 对外服务。 - Exporter 是翻译官,把 MySQL/Redis/主机的数据转成
/metrics文本格式;自己的应用可以用客户端库直接埋点。 - Alertmanager 独立负责去重、分组、抑制、静默与路由——独立的根本原因是 Prometheus 多副本高可用需要一个统一的去重点。
- Pushgateway 只服务短生命周期批处理任务,注意「数据不过期」和「掩盖任务失活」两个坑。
- Service Discovery +
relabel_configs让目标列表在动态环境下自我维护。 - 数据流:
target → scrape → relabel → TSDB →一路查询、一路告警。 - 下一章我们真正把它装起来跑一遍 →