监控 ·

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 看板三步

  1. 打开 http://localhost:3000,登录
  2. Connections → Data sources → Add data source → Prometheus,URL 填 http://prometheus:9090 → Save & test
  3. 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_exporter9100
MySQL / MariaDBmysqld_exporter9104
PostgreSQLpostgres_exporter9187
Redisredis_exporter9121
Nginxnginx-prometheus-exporter(或 stub_status 自己转)9113
Kafkakafka_exporter / JMX Exporter9308
MongoDBmongodb_exporter9216
Windowswindows_exporter9182
GPU (NV)dcgm-exporter9400
日志(转成计数指标)Promtail + Loki(日志);Metric 另用 promtail 的 metrics stage
黑盒探测/ICMP/HTTP/TLSblackbox_exporter9115
业务 / 自己写的代码语言 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 有没有抓某个 targethttp://<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]
告警迟迟不 FIRINGfor: 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 failedTSDB 损坏(异常断电)。
① 有备份就替换 /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

九、学习路线与进阶

  1. 部署 + Node Exporter → 看懂 node_cpu / mem / disk 三张图
  2. PromQL 熟练:rate/irate/increasehistogram_quantileavg by* on() group_left() join
  3. 写告警规则 + Alertmanager 路由 + 企业微信 webhook 跑通
  4. 业务代码自己埋 Counter / Histogram / Gauge
  5. 服务发现:file_sd / consul_sd / kubernetes_sd 代替手写 static_configs
  6. 高可用 & 长期存储:Prometheus 联邦ThanosVictoriaMetricsGrafana Mimir
  7. 可观测三件套合一:Prometheus(指标)+ Loki(日志)+ Jaeger / Tempo(链路)→ 统一到 Grafana Explore
  8. 日志指标化:Promtail pipeline metrics: 阶段,从日志里”反推” Counter,不用改业务代码

参考资料

问问 AI