【前情提要】 Grafana 装好了,Dashboard 也导入了,CPU、内存、磁盘、网络都能看了。老周说:“现在你能看到数据了,但你不能 24 小时盯着屏幕。你得让 Prometheus 自己判断,什么时候该告警。”小林打开 Prometheus 的告警规则配置,准备写第一条规则。但他不知道,expr、for、labels、annotations,每一个都有讲究。

一、Grafana 好看,但没人看

周一早上,小林打开 Grafana,看着自己配的 Dashboard,CPU、内存、磁盘曲线一路平稳。

“周哥,你看,监控配好了。”

老周走过来,看了一眼:“好看。”

“那当然。”

“但我问你一个问题。”老周说,“你周六晚上在干嘛?”

“打游戏啊。”

“那周六晚上,你博客 CPU 飙到 90%,你知道吗?”

小林愣住了:“我……不知道。”

“对。”老周说,“Grafana 再好看,也得有人看。没人看,它就是一张画。 你不能 24 小时盯着屏幕,但你得知道服务器什么时候出问题。这就需要告警。”

“那告警谁来发?”

“Prometheus。”老周说,“你给它定规则,它自己判断。条件满足了,它就‘喊人’。”

“喊谁?”

“先喊 Alertmanager,Alertmanager 再决定怎么通知你。”老周说,“但今天,你先学怎么让 Prometheus‘判断’。”

二、告警规则长什么样?

“告警规则,就是一个 YAML 文件。”老周说,“你在里面写:什么条件下,触发什么告警。”

“给我看看。”

老周拿过小林的笔记本,写了一个例子:

groups:
  - name: example
    rules:
      - alert: HighCpuUsage
        expr: 1 - avg(rate(node_cpu_seconds_total{mode="idle"}[1m])) > 0.8
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "CPU 使用率超过 80%"
          description: "实例 {{ $labels.instance }} 的 CPU 使用率已持续 2 分钟超过 80%,当前值 {{ $value }}"

“一行一行解释。”老周说。

  • groups:规则组。一个文件可以有多个组,每组一个 name。
  • rules:规则列表。一组可以有多个规则。
  • alert:告警名称。HighCpuUsage,你自己起。
  • expr:表达式。PromQL,条件满足就触发。
  • for:持续时间。条件满足后,持续多久才真正触发。2m 表示 2 分钟。
  • labels:标签。severity: warning,表示告警级别。
  • annotations:注释。summary 和 description,告警内容,支持模板变量。

“那 {{ $labels.instance }} 是啥?”

“模板变量。”老周说,“$labels 是当前告警的标签,$value 是当前值。你写 {{ $labels.instance }},告警发出来时,会替换成具体的实例名。”

小林在笔记本上画了个表:

字段 作用
alert 告警名称
expr PromQL 表达式,条件
for 持续时间,条件满足多久才触发
labels 告警标签,如 severity
annotations 告警内容,支持模板变量

三、写第一条规则

“现在写第一条。”老周说,“你先建一个规则文件。”

“放哪?”

“Prometheus 配置目录下,建个 rules 文件夹。”

sudo mkdir -p /etc/prometheus/rules
sudo nano /etc/prometheus/rules/node_rules.yml

“写什么?”

“写一条‘CPU 超过 80% 持续 2 分钟’的告警。”

小林写:

groups:
  - name: node_alerts
    rules:
      - alert: HighCpuUsage
        expr: 1 - avg(rate(node_cpu_seconds_total{mode="idle"}[1m])) > 0.8
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "CPU 使用率超过 80%"
          description: "CPU 使用率已持续 2 分钟超过 80%,当前值 {{ $value | humanizePercentage }}"

“humanizePercentage 是啥?”

“模板函数,把 0.85 变成 85%。”老周说,“Prometheus 的模板支持一些函数,你以后慢慢学。现在先写这个。”

“那 for: 2m,如果我写 2 呢?”

“那就会报错。”老周说,“for 必须带单位。2m 是 2 分钟,30s 是 30 秒,1h 是 1 小时。不写单位,Prometheus 不认。”

小林记下:for 必须带单位,2m、30s、1h。

四、加载规则:让 Prometheus 知道

“规则文件写好了,但 Prometheus 不知道。”老周说,“你得告诉它。”

“在哪告诉?”

“prometheus.yml 里的 rule_files。”

小林打开:

sudo nano /etc/prometheus/prometheus.yml

找到 rule_files:

rule_files:
  # - "first_rules.yml"
  # - "second_rules.yml"

改成:

rule_files:
  - "rules/*.yml"

“rules/*.yml 表示加载 rules/ 目录下所有 .yml 文件。”老周说,“你以后加新规则,不用改 prometheus.yml,直接放 rules/ 里就行。”

“那现在怎么让 Prometheus 生效?”

“先检查规则。”

promtool check rules /etc/prometheus/rules/node_rules.yml

输出:

Checking /etc/prometheus/rules/node_rules.yml
  SUCCESS: 1 rule found

“SUCCESS,规则没问题。”老周说,“现在检查整个配置。”

promtool check config /etc/prometheus/prometheus.yml

输出:

Checking /etc/prometheus/prometheus.yml
  SUCCESS: 1 rule files found

“现在重启 Prometheus。”

sudo systemctl restart prometheus
sudo systemctl status prometheus

输出:

Active: active (running)

“成了。”小林说。

五、看告警状态:Pending 和 Firing

“规则加载了,但你怎么知道它在工作?”老周说,“打开 Prometheus Web UI,点 Alerts。”

小林在浏览器里打开 http://localhost:9090,点顶部菜单的 Alerts。

页面出来了,列出了所有规则:

HighCpuUsage
  expr: 1 - avg(rate(node_cpu_seconds_total{mode="idle"}[1m])) > 0.8
  for: 2m
  labels: severity="warning"
  annotations: summary="CPU 使用率超过 80%"
  state: inactive

“state: inactive,说明条件没满足,没触发。”老周说,“现在你制造一下 CPU 负载。”

“怎么制造?”

“用 stress。”老周说,“先装。”

sudo apt install -y stress

“然后跑起来。”

stress --cpu 4 --timeout 300

“--cpu 4 表示用 4 个进程压 CPU,--timeout 300 表示持续 300 秒。”老周说,“现在你刷新 Prometheus 的 Alerts 页面。”

小林刷新,看到:

HighCpuUsage
  state: pending

“pending 了。”小林说。

“对。”老周说,“pending 表示条件满足了,但还没到 for 的时间。你等 2 分钟。”

两分钟后,小林刷新:

HighCpuUsage
  state: firing

“firing 了。”小林兴奋地说,“触发了!”

“对。”老周说,“inactive → pending → firing,这是告警的三个状态。 inactive 是正常,pending 是条件满足但还在等 for 时间,firing 是真正触发。”

“那 firing 之后,Prometheus 会干嘛?”

“它会把这个告警推给 Alertmanager。”老周说,“Alertmanager 再决定怎么通知你。但今天我们先把 Prometheus 这边搞定。”

小林记下:

  • inactive:条件不满足,正常
  • pending:条件满足,等待 for 时间
  • firing:真正触发,推给 Alertmanager

六、ALERTS 指标:用 PromQL 查告警

“除了在 Alerts 页面看,你还能用 PromQL 查告警状态。”老周说。

“怎么查?”

“ALERTS 指标。”

小林在查询框里敲:

ALERTS

结果:

ALERTS{alertname="HighCpuUsage", alertstate="firing", severity="warning"} 1

“看到了吗?”老周说,“alertname 是告警名,alertstate 是状态,severity 是级别,值 1 表示正在触发。”

“那 pending 状态呢?”

“ALERTS{alertstate="pending"}。”

ALERTS{alertstate="pending"}

“你现在可以查所有告警:”

ALERTS

“或者只查触发的:”

ALERTS{alertstate="firing"}

“这有啥用?”

“你以后可以在 Grafana 里加一个 Panel,专门显示当前触发的告警。”老周说,“或者用 PromQL 做告警的告警——比如‘如果超过 5 个告警同时触发,就发一条汇总告警’。”

小林记下:

  • ALERTS:查询所有告警状态
  • ALERTS{alertstate="firing"}:只看触发的
  • ALERTS{alertstate="pending"}:只看等待中的
  • ALERTS 的值:1 表示该状态激活

七、常用告警规则模板

“你学会写一条了,我再给你几条常用的。”老周说,“你直接抄,改改阈值就行。”

内存使用率超过 85%:

- alert: HighMemoryUsage
  expr: 1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes > 0.85
  for: 2m
  labels:
    severity: warning
  annotations:
    summary: "内存使用率超过 85%"
    description: "内存使用率已持续 2 分钟超过 85%,当前值 {{ $value | humanizePercentage }}"

磁盘使用率超过 85%:

- alert: HighDiskUsage
  expr: 1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} > 0.85
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "根分区使用率超过 85%"
    description: "根分区使用率已持续 5 分钟超过 85%,当前值 {{ $value | humanizePercentage }}"

目标离线:

- alert: InstanceDown
  expr: up == 0
  for: 1m
  labels:
    severity: critical
  annotations:
    summary: "实例 {{ $labels.instance }} 离线"
    description: "实例 {{ $labels.instance }} 已离线超过 1 分钟"

“注意最后一条。”老周说,“up == 0,这是最基础的告警。你以后不管监控什么,第一件事就是配 up == 0。 目标挂了,其他指标都查不到,你得先知道它挂了。”

“那 severity: critical 和 warning 有啥区别?”

“级别。”老周说,“critical 是严重,warning 是警告。你以后在 Alertmanager 里配路由,critical 发邮件+短信,warning 只发邮件。分级是为了让你半夜只被真正重要的事情吵醒。”

小林把三条规则抄进 node_rules.yml。

八、排错挑战:规则文件加载失败

下午,小林想加一条“Nginx 服务挂了”的告警。他写:

- alert: NginxDown
  expr: up{job="nginx"} == 0
  for: 1m
  labels:
    severity: critical
  annotations:
    summary: "Nginx 服务离线"

保存,检查:

promtool check rules /etc/prometheus/rules/node_rules.yml

输出:

Checking /etc/prometheus/rules/node_rules.yml
  FAILED: yaml: line 25: did not find expected key

“第 25 行。”小林打开文件:

      - alert: HighDiskUsage
        expr: 1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} > 0.85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "根分区使用率超过 85%"
          description: "根分区使用率已持续 5 分钟超过 85%,当前值 {{ $value | humanizePercentage }}"

      - alert: InstanceDown
        expr: up == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "实例 {{ $labels.instance }} 离线"
          description: "实例 {{ $labels.instance }} 已离线超过 1 分钟"

      - alert: NginxDown
        expr: up{job="nginx"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Nginx 服务离线"

“缩进看起来没问题啊。”小林说。

老周看了一眼:“你 annotations 下面 summary 和 description 的缩进,和 labels 下面的 severity 对齐了吗?”

小林仔细看:

        labels:
          severity: critical
        annotations:
          summary: "Nginx 服务离线"

“对齐了啊,都是 10 个空格。”

“那你再看 expr 那行。”

小林看:

        expr: up{job="nginx"} == 0

“这行也没问题啊。”

“你往上翻,看 InstanceDown 的 description 那行。”

小林翻上去:

          description: "实例 {{ $labels.instance }} 已离线超过 1 分钟"

“也没问题啊。”

老周凑近看了一眼:“你 description 里的 {{ $labels.instance }},双引号里面又有双引号?不对,没有。等等,你 summary 里的引号是中文引号还是英文引号?”

小林仔细看:

          summary: "实例 {{ $labels.instance }} 离线"

“英文引号啊。”

“那你把文件发我看看。”老周说。

小林把文件内容复制出来,老周在编辑器里打开,用“显示不可见字符”一看:

summary: "实例 {{ $labels.instance }} 离线"<0xA0>

“看到了吗?”老周说,“你行尾有个不换行空格,0xA0。这是从网页复制代码时带进来的。YAML 解析器把它当成了非法字符。”

“那我怎么删?”

“你 nano 里看不到,用 sed 删。”

sed -i 's/\xc2\xa0/ /g' /etc/prometheus/rules/node_rules.yml

“或者,你重新手敲一遍,别从网页复制。”老周说,“YAML 文件从网页复制,最容易带进不可见字符。 你以后写规则,尽量手敲,或者复制后检查一下。”

小林重新手敲了一遍,再次检查:

promtool check rules /etc/prometheus/rules/node_rules.yml

输出:

SUCCESS: 4 rules found

“成了。”小林松了口气。

排错思路总结:

  1. 规则文件加载失败,先 promtool check rules 定位行号。
  2. 检查缩进:同一层级必须对齐,用空格不用 Tab。
  3. 检查引号:中文引号和英文引号不一样,YAML 只认英文。
  4. 检查不可见字符:从网页复制可能带 0xA0 不换行空格,用 sed -i 's/\xc2\xa0/ /g' 替换。
  5. 最稳的方法:手敲,别复制。
  6. 改完先 promtool check rules,再 restart。

“记住,”老周说,“YAML 的坑,一半是缩进,一半是复制。 你以后写 YAML,眼睛盯着,手敲,别偷懒。”

九、练习

  1. 练习一:创建规则文件 /etc/prometheus/rules/node_rules.yml,写一条 HighMemoryUsage 告警,条件为内存使用率超过 85% 持续 2 分钟。用 promtool check rules 检查通过后,在 prometheus.yml 中配置 rule_files,重启 Prometheus。
  2. 练习二:在 Prometheus Web UI 的 Alerts 页面,确认 HighMemoryUsage 状态为 inactive。然后用 stress 制造内存负载(stress --vm 2 --vm-bytes 1G --timeout 300),观察状态从 inactive → pending → firing 的变化。
  3. 练习三:用 PromQL 查询 ALERTS 和 ALERTS{alertstate="firing"},确认正在触发的告警。然后用 up == 0 的规则,停掉 node_exporter,观察告警触发。

参考答案

练习一:

sudo nano /etc/prometheus/rules/node_rules.yml

内容:

groups:
  - name: node_alerts
    rules:
      - alert: HighMemoryUsage
        expr: 1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes > 0.85
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "内存使用率超过 85%"
          description: "内存使用率已持续 2 分钟超过 85%,当前值 {{ $value | humanizePercentage }}"
promtool check rules /etc/prometheus/rules/node_rules.yml
sudo nano /etc/prometheus/prometheus.yml

rule_files 改为:

rule_files:
  - "rules/*.yml"
promtool check config /etc/prometheus/prometheus.yml
sudo systemctl restart prometheus

练习二:

sudo apt install -y stress
stress --vm 2 --vm-bytes 1G --timeout 300

刷新 Prometheus Alerts 页面,观察 HighMemoryUsage 状态变化。

练习三:

ALERTS
ALERTS{alertstate="firing"}
sudo systemctl stop prometheus-node-exporter

等待 1 分钟,刷新 Alerts 页面,确认 InstanceDown 触发。

sudo systemctl start prometheus-node-exporter

十、下集预告

告警规则写好了,Prometheus 能判断“什么时候该喊人”了。但 Prometheus 只会“喊”,不会“通知”。谁负责把告警发到你邮箱?老周说:“Alertmanager。”小林打开 Alertmanager 文档,准备配置。但他不知道,route、receiver、group_by、repeat_interval,每一个都有讲究。

下一集:《Alertmanager:告警的“调度中心”》