第4章:指标系统搭建 — Prometheus 架构、Exporter 开发、PromQL 查询语言、告警规则配置
聊到可观测性,指标系统是绕不开的基石。而 Prometheus,说实话,已经是云原生领域的事实标准了。今天这一章,我会把 Prometheus 的架构、Exporter 开发、PromQL 查询以及告警规则配置,掰开揉碎了讲清楚。
4.1 Prometheus 架构:拉模式与时间序列
Prometheus 的核心设计理念,就是「拉」(Pull)模式。它不像传统监控那样,等着客户端把数据推过来。而是主动去抓取目标暴露的指标端点。这个设计,我个人觉得非常优雅——你想想看,谁在控制采集频率?谁来决定采集哪些目标?都是 Prometheus Server 自己说了算,不会被下游的突发流量冲垮。
它的架构大致分三块:
- Prometheus Server:负责拉取数据、存储时间序列、执行 PromQL 查询。
- Exporter:负责暴露指标端点,把业务或系统数据转成 Prometheus 能理解的格式。
- Alertmanager:处理告警,负责去重、分组、路由到不同的接收端(比如钉钉、邮件)。
我在项目中遇到过一个问题:刚开始搭建时,所有服务都往一个 Prometheus 实例上拉,结果单点压力巨大。后来我们做了分片,按业务域拆成多个 Prometheus,再用 Thanos 做全局聚合。嗯,这里要注意,架构设计一定要考虑规模扩展。
核心要点:Prometheus 存储的是带时间戳的键值对,每条数据叫一个「样本」。指标名 + 标签组成了唯一标识。比如 http_requests_total{method="GET", status="200"}。
4.2 Exporter 开发:从零写一个自定义 Exporter
官方提供了很多现成的 Exporter,比如 node_exporter、mysqld_exporter。但业务指标,比如「当前排队人数」、「订单处理延迟」,就得自己写了。
开发一个 Exporter 其实不复杂。说白了,就是暴露一个 HTTP 接口,返回 Prometheus 格式的文本。我习惯用 Go 语言写,因为官方 client_golang 库支持很完善。
下面是一个最简单的示例,暴露一个自定义的计数器指标:
package main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
myCounter = prometheus.NewCounter(prometheus.CounterOpts{
Name: "myapp_requests_total",
Help: "Total number of requests processed.",
})
)
func init() {
prometheus.MustRegister(myCounter)
}
func handler(w http.ResponseWriter, r *http.Request) {
myCounter.Inc()
w.Write([]byte("OK"))
}
func main() {
http.Handle("/metrics", promhttp.Handler())
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
这段代码跑起来后,访问 /metrics 就能看到类似这样的输出:
# HELP myapp_requests_total Total number of requests processed.
# TYPE myapp_requests_total counter
myapp_requests_total 42
避坑指南:我曾经犯过一个低级错误——忘记在 init() 里注册指标,结果 Prometheus 抓取时一直报错 metric not found。另外,指标命名建议遵循 namespace_subsystem_name 的规范,比如 myapp_http_requests_total,这样在 PromQL 里查询时一目了然。
4.3 PromQL 查询语言:从入门到实战
PromQL 是 Prometheus 的查询语言。说实话,刚开始接触时觉得有点别扭,但用熟了会发现它非常强大。它本质上是对时间序列进行过滤、聚合、运算。
我把它分成三个层次来讲:
4.3.1 基础查询:选择器与标签过滤
最简单的查询就是直接写指标名:
myapp_requests_total
这会返回所有标签组合下的最新值。如果想过滤,用花括号:
myapp_requests_total{method="GET", status="200"}
你还可以用正则匹配:
myapp_requests_total{method=~"GET|POST"}
4.3.2 范围查询与速率计算
Counter 类型的指标是累积的,直接看绝对值意义不大。我们通常用 rate() 函数看每秒增长率:
rate(myapp_requests_total[5m])
这个查询会计算过去5分钟内,每秒平均增长了多少请求。我在项目中排查过一个问题:某个服务突然流量暴增,就是靠 rate() 配合 topk() 快速定位到异常的接口。
4.3.3 聚合操作与子查询
当你有多个实例时,可以用 sum by 做聚合:
sum by (instance) (rate(myapp_requests_total[5m]))
这会按实例分组,算出每个实例的请求速率。
子查询稍微复杂一点,但很实用。比如你想看过去1小时内,每5分钟的平均 CPU 使用率:
avg_over_time(rate(cpu_usage_total[1m])[1h:5m])
个人经验:我建议新手先掌握 rate()、increase()、histogram_quantile() 这三个函数。尤其是 histogram_quantile(),用来计算 P99 延迟非常方便。比如:histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))。
4.4 告警规则配置:让系统主动找你
指标有了,查询会写了,接下来就是告警。Prometheus 的告警规则写在 YAML 文件里,由 Prometheus Server 定期评估。
一个典型的告警规则长这样:
groups:
- name: example
rules:
- alert: HighRequestRate
expr: rate(myapp_requests_total[5m]) > 100
for: 2m
labels:
severity: critical
annotations:
summary: "请求速率过高 (instance {{ $labels.instance }})"
description: "过去5分钟请求速率超过100,当前值:{{ $value }}"
这里有几个关键点:
- expr:触发条件,就是一条 PromQL 表达式。
- for:持续时间。比如持续2分钟都超过阈值才触发告警,避免毛刺误报。
- labels:可以附加标签,比如严重级别。
- annotations:告警信息模板,支持模板变量。
注意:我曾经踩过一个坑——告警规则里用了 rate(),但 for 时间设得太短,结果每次 Prometheus 重启后,因为数据不足,rate() 返回 NaN,导致告警误报。后来我加了一个 absent() 判断,或者把 for 时间设长一点,问题就解决了。
告警触发后,会发送到 Alertmanager。Alertmanager 负责去重、分组、静默,然后路由到不同的接收器。比如你可以配置一个路由,把 critical 级别的告警直接发到电话,warning 级别的发到钉钉群。
配置 Alertmanager 的路由规则,大致是这样的:
route:
receiver: 'default'
routes:
- match:
severity: critical
receiver: 'pagerduty'
receivers:
- name: 'default'
webhook_configs:
- url: 'http://dingtalk-hook:8080/alert'
- name: 'pagerduty'
pagerduty_configs:
- routing_key: 'your-key'
嗯,到这里,指标系统的核心环节就串起来了。从架构设计,到自定义 Exporter 开发,再到 PromQL 查询和告警配置,每一步都有不少细节。但只要你动手搭一遍,遇到问题再回头看我说的这些坑,应该能少走很多弯路。
最后一个小建议:告警规则不要写太多。我见过有人一口气配了200条规则,结果每天告警轰炸,最后大家都麻木了。记住,告警的目的是让人采取行动,不是制造噪音。优先关注那些真正影响用户体验的指标,比如错误率、延迟、饱和度。