监控 ·
Prometheus + Grafana 监控体系:安装、PromQL 编写与日常排障
Prometheus 是云原生监控的事实标准。本文从指标分类、Exporter 采集、PromQL、告警规则、Grafana 面板到最常踩的指标不准、不抓数、告警没发、OOM 等坑系统整理。
Prometheus + Grafana 监控体系:安装、PromQL 编写与日常排障
Prometheus 是一个主动拉取 + 时序数据库 + PromQL 查询 + 告警规则一体化的监控系统。配上 Grafana 画图、Alertmanager 发告警、加上各种 Exporter(导出应用/系统/中间件指标),基本就是现代运维监控的标配。
一、监控体系总览
1.1 核心组件
┌──────────────┐ scrape (主动HTTP拉) ┌─────────────────┐
│ Prometheus │ <──────────────────────── │ Exporter / App │
│ (存储+查询+ │ │ (Node/MySQL/ │
│ 规则计算) │ │ Redis/业务...)│
└──────┬───────┘ └─────────────────┘
│
│ remote_write
▼
┌──────────────┐ ┌───────────────┐ ┌──────────────┐
│ Alertmanager │───────▶│ Email │ │ Webhook(钉钉│
│ (去重/分组/ │ │ 飞书/企业微信 │ │ /飞书/...) │
│ 静默/路由) │ └───────────────┘ └──────────────┘
└──────────────┘
▲
│ datasource
┌──────────────┐
│ Grafana │ <---- 人类:画图、看板、排查用
└──────────────┘
1.2 四大指标类型
| 类型 | 说明 | 示例 | PromQL 常用函数 |
|---|---|---|---|
| Counter(计数器,单调递增) | 只增不减,重启归零 | 请求总数 http_requests_total、错误数、网卡字节 | rate() / irate() / increase()(不能直接看原始值) |
| Gauge(仪表盘) | 可增可减的瞬时值 | CPU 使用率、内存、磁盘、连接数、温度、队列长度 | 直接用,或 avg_over_time() / delta() |
| Histogram(直方图) | 把观测值分到多个”桶”+ count + sum | 请求延迟、响应大小 | histogram_quantile() 算 P99/P95/P50 |
| Summary(摘要) | 客户端直接算分位数 + count + sum | 同上,客户端性能更好但不能聚合 | 看 {quantile="0.99"} |
Counter 使用铁律:永远不要直接 graph counter 本身(图就是一条单调升的斜线,看不出速率)。一定要包
rate(my_counter[5m])看”每秒增量”。
二、安装
2.1 方案 A:最快上手(Docker Compose)
compose.yml:
services:
prometheus:
image: prom/prometheus:v2.54.1
container_name: prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=30d"
- "--web.enable-lifecycle" # 热加载配置:curl -X POST localhost:9090/-/reload
- "--web.enable-admin-api"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./rules:/etc/prometheus/rules:ro
- prom_data:/prometheus
ports:
- "9090:9090"
restart: unless-stopped
node-exporter:
image: prom/node-exporter:v1.8.2
container_name: node_exporter
pid: host # 看到宿主机进程网络
network_mode: host # 真机 IP,方便 Prometheus 拉宿主机指标
command:
- "--path.rootfs=/host"
volumes:
- "/:/host:ro,rslave"
restart: unless-stopped
alertmanager:
image: prom/alertmanager:v0.27.0
container_name: alertmanager
volumes:
- ./alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
ports:
- "9093:9093"
restart: unless-stopped
grafana:
image: grafana/grafana-oss:11.2.0
container_name: grafana
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: adminadmin # 生产用 secret
volumes:
- grafana_data:/var/lib/grafana
ports:
- "3000:3000"
restart: unless-stopped
volumes:
prom_data:
grafana_data:
2.2 prometheus.yml
global:
scrape_interval: 15s # 默认每 15s 拉一次
evaluation_interval: 15s # 规则计算频率
external_labels:
cluster: prod
region: cn-hz
# 告警管理器地址
alerting:
alertmanagers:
- static_configs:
- targets: ["alertmanager:9093"]
# 告警与记录规则
rule_files:
- "rules/*.yml"
# 抓取目标
scrape_configs:
- job_name: "prometheus-self"
static_configs:
- targets: ["localhost:9090"]
- job_name: "node"
static_configs:
- targets:
- "192.168.1.10:9100"
- "192.168.1.11:9100"
labels: { env: prod }
# 用 consul / 云 / k8s_sd 动态发现
- 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
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
热加载配置(不用重启):
docker exec prometheus kill -HUP 1
# 或
curl -X POST http://localhost:9090/-/reload
2.3 Linux 直接装 Prometheus(生产)
sudo useradd --no-create-home --shell /bin/false prometheus
wget https://github.com/prometheus/prometheus/releases/download/v2.54.1/prometheus-2.54.1.linux-amd64.tar.gz
tar xzf prometheus-*.tar.gz && cd prometheus-2.54.1.linux-amd64
sudo cp prometheus promtool /usr/local/bin/
sudo mkdir -p /etc/prometheus /var/lib/prometheus
sudo cp prometheus.yml /etc/prometheus/
sudo chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus
写 systemd unit prometheus.service:
[Unit]
Description=Prometheus
After=network.target
[Service]
User=prometheus
Group=prometheus
Type=simple
ExecStart=/usr/local/bin/prometheus \
--config.file=/etc/prometheus/prometheus.yml \
--storage.tsdb.path=/var/lib/prometheus \
--storage.tsdb.retention.time=30d \
--web.enable-lifecycle \
--storage.tsdb.wal-compression
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload && sudo systemctl enable --now prometheus
systemctl status prometheus
三、PromQL 常用写法
先在浏览器打开 Prometheus UI http://localhost:9090 → Graph 标签页。
3.1 基础查询
# 1) 所有机器 CPU 空闲百分比(100 - 所有非空闲的总和)
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode!="idle"}[5m])) * 100)
# 2) 内存使用率
(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100
# 3) 磁盘使用率(排除 tmpfs / devtmpfs)
100 - (node_filesystem_avail_bytes{fstype!~"tmpfs|devtmpfs"} / node_filesystem_size_bytes) * 100
# 4) 网卡每秒出/入流量(bytes/s)
rate(node_network_receive_bytes_total{device!="lo"}[5m])
rate(node_network_transmit_bytes_total{device!="lo"}[5m])
# 5) 1 分钟负载 > CPU 核数(过载迹象)
node_load1 > on (instance) count by (instance) (node_cpu_seconds_total{mode="idle"})
# 6) HTTP 请求速率(业务 counter)
rate(http_requests_total{job="myapp"}[5m])
# 7) 错误率(5xx 占比)
sum(rate(http_requests_total{status=~"5.."}[5m])) by (instance)
/
sum(rate(http_requests_total[5m])) by (instance)
3.2 Histogram 算 P99/P95
# histogram_quantile(分位数, 某 bucket 的 rate)
# 注意一定要先对每个 le 桶取 rate,再喂进去
histogram_quantile(0.99, sum by (le, path) (rate(http_request_duration_seconds_bucket[5m])))
histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
3.3 irate() vs rate() vs increase()
| 函数 | 算法 | 适合场景 |
|---|---|---|
| rate[5m] | 区间首尾两点的线性斜率 / 秒 | 平滑长期趋势,看大盘;告警首选(抗尖刺) |
| irate[5m] | 用区间最后两个样本点瞬时差 / 秒 | 快速响应波动,看精细毛刺;但告警慎用(一个点异常就告警) |
| increase[1h] | 区间总增量(counter 增加量) | 看”1 小时总共请求数” |
3.4 聚合操作符 & 修饰符
# by / without:sum/min/max/avg/topk + by(标签)
avg by (instance, job) (rate(http_requests_total[5m]))
# topk:取 CPU 前 5 的容器
topk(5, rate(container_cpu_usage_seconds_total{name!=""}[5m]))
# on() / ignoring():多指标 join
# 例:只有 up==1 的机器上计算磁盘
(node_filesystem_avail_bytes) * on (instance) group_left() (up == 1)
四、告警规则
在 rules/alerts.yml:
groups:
- name: node_alerts
interval: 30s
rules:
- alert: NodeDown
expr: up{job="node"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "实例 {{ $labels.instance }} 下线"
description: "已 2 分钟抓不到 node-exporter。可能机器挂、exporter 挂或网络不通。"
- alert: HighCPUUsage
expr: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode!="idle"}[5m])) * 100) > 85
for: 10m
labels: { severity: warning }
annotations:
summary: "{{ $labels.instance }} CPU > 85%"
description: "当前值 = {{ $value | printf \"%.2f\" }}%"
- alert: DiskAlmostFull
expr: (1 - node_filesystem_avail_bytes{fstype!~"tmpfs|devtmpfs"} / node_filesystem_size_bytes) * 100 > 90
for: 5m
labels: { severity: warning }
annotations:
summary: "{{ $labels.instance }} 磁盘 {{ $labels.mountpoint }} > 90%"
description: "剩余 {{ 100 - $value | printf \"%.2f\" }}%,请尽快清理。"
- alert: MyAppErrorRateHigh
expr: >
sum by (instance) (rate(http_requests_total{status=~"5.."}[5m]))
/
sum by (instance) (rate(http_requests_total[5m]))
> 0.05
for: 5m
labels: { severity: critical }
annotations:
summary: "{{ $labels.instance }} 5xx 占比 > 5%"
校验规则语法:
promtool check rules rules/alerts.yml
# 无报错 = OK
五、Alertmanager 告警分发
alertmanager.yml(企业微信 webhook 为例,钉钉/飞书类似):
global:
resolve_timeout: 5m
smtp_smarthost: 'smtp.example.com:465'
smtp_from: 'alerts@example.com'
smtp_auth_username: 'alerts@example.com'
smtp_auth_password: '<password>'
route:
group_by: ['alertname', 'cluster']
group_wait: 10s # 同一组内前 10s 等更多告警一起发
group_interval: 5m # 同一组两次发送间隔
repeat_interval: 8h # 还没解决,多久再提醒
receiver: default-receiver
routes:
- matchers:
- severity = critical
receiver: oncall-webhook
continue: true
- matchers:
- severity = warning
receiver: mail-receiver
receivers:
- name: default-receiver
email_configs:
- to: 'ops@example.com'
send_resolved: true
- name: mail-receiver
email_configs:
- to: 'dev@example.com'
- name: oncall-webhook
webhook_configs:
- url: 'http://your-webhook-adapter:8080/wework'
send_resolved: true
max_alerts: 20
# 静默期(节假日可加)和抑制规则
inhibit_rules:
# 发生严重告警时,抑制同一机器的低级 warning
- source_matchers: [severity = critical]
target_matchers: [severity = warning]
equal: [alertname, instance]
六、Grafana 看板三步
- 打开
http://localhost:3000,登录 - Connections → Data sources → Add data source → Prometheus,URL 填
http://prometheus:9090→ Save & test - Dashboards → New → Import,输入社区 Dashboard ID:
- Node Exporter 全功能大盘:1860(Node Exporter Full)
- Prometheus 自身:3662
- MySQL:7362
- Redis:763
- Kubernetes 集群大盘:6417 / 15761(新)
社区:https://grafana.com/grafana/dashboards 搜关键字就行,一般是官方 exporter 的 Dashboards 最稳。
七、常用 Exporter 清单
| 目标 | Exporter | 默认端口 |
|---|---|---|
| 宿主机 / 虚拟机 | node_exporter | 9100 |
| MySQL / MariaDB | mysqld_exporter | 9104 |
| PostgreSQL | postgres_exporter | 9187 |
| Redis | redis_exporter | 9121 |
| Nginx | nginx-prometheus-exporter(或 stub_status 自己转) | 9113 |
| Kafka | kafka_exporter / JMX Exporter | 9308 |
| MongoDB | mongodb_exporter | 9216 |
| Windows | windows_exporter | 9182 |
| GPU (NV) | dcgm-exporter | 9400 |
| 日志(转成计数指标) | Promtail + Loki(日志);Metric 另用 promtail 的 metrics stage | — |
| 黑盒探测/ICMP/HTTP/TLS | blackbox_exporter | 9115 |
| 业务 / 自己写的代码 | 语言 SDK(Python prometheus-client / Go prometheus/client_golang…) | 自定义端口 |
业务代码里暴露 /metrics(Go 示例):
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var httpReqs = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total http requests.",
}, []string{"path", "status"})
func main() {
prometheus.MustRegister(httpReqs)
http.Handle("/metrics", promhttp.Handler())
_ = http.ListenAndServe(":8080", nil)
}
八、日常排障手册
8.1 调试工具链
| 想知道 | 怎么做 |
|---|---|
| Prometheus 有没有抓某个 target | http://<prom>:9090/targets 看状态 + Last scrape + scrape duration + scrape error 列(必看第一页) |
| 某个 target 返回了哪些原始指标 | curl http://<target-ip>:<port>/metrics |
| 配置语法对吗 | promtool check config prometheus.yml + promtool check rules rules/*.yml |
| 某条 PromQL 正确吗 | UI 上先跑;命令行 promtool query instant --local /prometheus 'my_metric' |
| TSDB 占多大 | promtool tsdb analyze /var/lib/prometheus |
| 告警发没发 | http://<prom>:9090/alerts 看 FIRING / PENDING;http://<am>:9093/#/alerts 看 AM 收到没 |
8.2 常见报错对照表
| 现象 | 排查路径 |
|---|---|
| Target 红色 state=DOWN | ① curl host:port/metrics 看能不能通(网络 / 防火墙 / 进程挂了)② scrape URL 写对没( metrics_path 默认 /metrics)③ TLS / basic_auth / bearer_token 配置错 |
| Target UP 但 Prometheus UI 查不到指标 | ① 指标名打错了 ② 被 metric_relabel_configs drop 了(用 {__name__=~".+"}[1m] 搜该 target 有无任何指标)③ honor_labels: true 配置冲突 |
rate() 总返回空 | Counter 在 5m 内完全没增长(空闲状态正常);或者 scrape_interval 配得比窗口还大(把窗口调大到 [15m]) |
| 告警迟迟不 FIRING | for: 2m 要等满;或 expr 表达式根本就一直没 true;或规则文件没写进 rule_files: 也没 reload |
| 告警 FIRING 了但 AM 没发 | ① alerting.alertmanagers 配错(AM 地址、端口)② AM 路由没匹配(把 route 调成继续匹配 continue: true 调试)③ receiver 本身失败(看 AM 日志 4xx/5xx) ④ 被 inhibit_rule 抑制 / 被 Silence 静默 |
| Prometheus 内存爆炸 / OOM | 典型 “cardinality explosion(基数爆炸)“——某个标签(比如 request_id / user_id / path 把完整 URL 带进去)无限增长。排查: topk(10, count by (__name__, job) ({__name__=~".+"})) 找高基数指标;Prometheus UI → “TSDB Status” → Top 10 label pair counts 查得最快 |
| TSDB 磁盘涨得太快 | ① retention 设短一点(--storage.tsdb.retention.time=15d)② 开启 WAL 压缩(默认有) ③ 打日志:哪个 job 指标最多。长期方案:remote_write 到 Thanos / VictoriaMetrics / Mimir |
| 图表中 series 会”断一下又续上” | rate() / irate() 窗口大小 < 两次 scrape 间隔;或者 target 重启了 counter 归零。把窗口调成 ≥ 4 × scrape_interval |
| P99 结果是 NaN / 不对 | histogram bucket 配置不合理(所有桶都装不下真实值)。检查 _bucket{le="+Inf"} 是否真的包含了全部请求;或 le 没在 sum by (le, …) 里带上 |
| Grafana 图表”No data”但 Prometheus UI 有值 | ① Grafana DS 的 time range 选短了 / 时区不对 ② step / $__interval 太大导致 sum_over_time 聚合漏数据③ Prometheus 版本 > 2.26 后默认开启了 promql_engine: prometheus,旧表达式要调 |
Prometheus 启动失败,日志 opening storage failed | TSDB 损坏(异常断电)。 ① 有备份就替换 /prometheus/chunks_head/、wal/,然后 promtool tsdb clean / rebuild② 最省事: --storage.tsdb.allow-overlapping-blocks 启动,能起来就赶紧 snapshot 把要的数据救出来 |
8.3 Cardinality(基数)爆炸排障一步到位
# 找每个 job 下最高基数的指标
topk(20, count by (__name__, job) ({__name__!=""}))
# 找某个指标里最高基数的标签值组合
topk(20, count by (path) (http_requests_total))
解决:把那个高基数字段(user_id/req_id/完整 URL path)从 label 里拿掉;真要保留就用 Loki / ClickHouse 存”日志/事件流”,别塞 Prometheus。
8.4 黑盒监控(HTTP/TCP/ICMP/TLS 证书过期)
配合 blackbox_exporter 最常见的几条 PromQL:
# 站点 HTTPS 证书过期少于 15 天
(probe_ssl_earliest_cert_expiry - time()) / 86400 < 15
# 探测失败 = 0
probe_success == 0
# HTTP 响应慢 > 1s
probe_duration_seconds > 1
九、学习路线与进阶
- 部署 + Node Exporter → 看懂 node_cpu / mem / disk 三张图
- PromQL 熟练:
rate/irate/increase、histogram_quantile、avg by、* on() group_left()join - 写告警规则 + Alertmanager 路由 + 企业微信 webhook 跑通
- 业务代码自己埋 Counter / Histogram / Gauge
- 服务发现:file_sd / consul_sd / kubernetes_sd 代替手写 static_configs
- 高可用 & 长期存储:Prometheus 联邦、Thanos、VictoriaMetrics、Grafana Mimir
- 可观测三件套合一:Prometheus(指标)+ Loki(日志)+ Jaeger / Tempo(链路)→ 统一到 Grafana Explore
- 日志指标化:Promtail pipeline
metrics:阶段,从日志里”反推” Counter,不用改业务代码
参考资料
- Prometheus 官方:https://prometheus.io/docs/
- PromQL 基础:https://prometheus.io/docs/prometheus/latest/querying/basics/
- PromQL Cheatsheet:https://promlabs.com/promql-cheat-sheet/
- Grafana Labs:https://grafana.com/docs/
- Awesome Prometheus alerts:https://awesome-prometheus-alerts.grep.to/
- Cardinality 白皮书:https://grafana.com/blog/2022/02/15/what-are-cardinality-spikes-and-why-do-they-matter/