凌晨 3 点,手机震动。
makefile
告警:server-01 CPU 使用率超过 80%
触发时间:03:12:45
持续时长:2 分钟
你睁开惺忪的眼睛,登录服务器查看,发现只是 Jenkins 在跑定时编译任务。
这已经是本周第 12 次误报。
问题不在 Prometheus,也不在 node_exporter,而在告警规则的设计。
在《从 node-exporter 学如何写出可复用的监控指标》中,我们学习了 node_exporter 的设计哲学:暴露事实,不是观点。
- CPU 暴露的是累计时间,不是使用率
- 内存暴露的是多个 Gauge,不是一个"使用率"
- 磁盘容量和 IO 分开设计
这些"事实"很完整,但问题来了:有了指标,怎么变成告警?
这不是简单设个阈值就行。你真的需要在 CPU 达到 80% 时立即告警吗?还是应该等它持续 10 分钟再打扰你?
本文聚焦一个核心问题:**基于 node_exporter 的数据,如何设计一套生产级的虚拟机告警规则?
1. 为什么需要告警规则的设计智慧
1.1 Alertmanager 的局限
很多人觉得 Prometheus 有 Alertmanager,为什么还要自研告警引擎?
Alertmanager 的三个痛点:
yaml
# Alertmanager 的配置
route:
group_by: ['alertname']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: 'team-pager'
| 问题 | 表现 | 影响 |
|---|---|---|
| 配置复杂 | YAML 难理解 | 运维成本高,改规则要重启 |
| 无状态可见 | 不知道当前有哪些告警 | 无法追溯告警历史 |
| 难以扩展 | 加功能要改源码 | 无法融入业务逻辑 |
一个真实场景:
arduino
运维:"CPU 使用率高的告警触发了多少次?"
Alertmanager:"不知道,我不存历史。"
运维:"这个告警持续多久了?"
Alertmanager:"不知道,我只管通知。"
运维:"能不能在发布窗口屏蔽告警?"
Alertmanager:"可以,但要写复杂的 YAML。"
自研的价值:
- Web UI 管理规则,不用改 YAML
- 数据库存储历史,可以分析趋势
- 灵活扩展,融入业务场景(如发布窗口自动静默)
1.2 Google 的两个方法论
Google SRE 给了我们两个黄金法则:
USE 方法(资源监控):
利用率] --> B[提前预警] C[Saturation
饱和度] --> D[确认拥塞] E[Errors
错误] --> F[确认故障] style A fill:#fff4e1 style C fill:#ffe1f5 style E fill:#ffcccc
- Utilization:资源用了多少?(CPU 80% → 需要关注)
- Saturation:是否有排队?(IO 队列长 → 性能下降)
- Errors:是否有错误?(磁盘错误 → 硬件故障)
四个黄金信号(服务监控):
虽然虚拟机不是"服务",但可以映射:
- Latency:IO 延迟高 → 影响上层应用
- Traffic:网络流量异常 → 可能被攻击
- Errors:硬件错误 → 需要介入
- Saturation:资源饱和 → 需要扩容
启示:告警不是简单设阈值,而是要理解系统在不同阶段的表现。
1.3 从指标到告警的核心挑战
node_exporter 给了我们"事实":
ini
node_cpu_seconds_total{mode="user"} 123456 # CPU 累计时间
node_memory_MemAvailable_bytes 8388608000 # 可用内存
node_filesystem_avail_bytes 53687091200 # 磁盘可用空间
但这些是 Counter 和 Gauge,怎么变成告警?
三个核心问题:
- 阈值怎么定? ------ 80% 合适还是 90% 合适?
- 怎么过滤毛刺? ------ 短暂峰值要不要告警?
- 如何提前预警? ------ 等到真出问题才告是不是太晚了?
接下来,我们用 CPU、内存、磁盘、网络四个维度,看看如何设计告警规则。
2. CPU 告警:不只是设个 80%
2.1 错误示例
很多人第一反应是这样:
promql
# 错误示例:CPU 使用率 > 80% 就告警
(1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m]))) * 100 > 80
会发生什么?
makefile
10:00 - CPU 85%(代码编译,正常) → 告警
10:01 - CPU 60%(编译结束) → 恢复
10:05 - CPU 85%(定时任务,正常) → 告警
10:06 - CPU 50% → 恢复
一天收到几十条告警,都是正常的业务波动。
问题:
- 没有时间维度,瞬时峰值也告警
- 80% 的阈值太低,正常繁忙就会触发
- 不区分"临时忙"和"持续忙"
2.2 正确示例 1:加上时间维度
promql
# 正确示例:CPU 持续 10 分钟 > 90%
(1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) by (instance)) * 100 > 90
for: 10m
改进点:
| 参数 | 值 | 作用 |
|---|---|---|
[5m] |
评估窗口 | 平滑 15 秒抓取间隔的抖动 |
> 90 |
阈值提高 | 留 10% 缓冲,避免误报 |
for: 10m |
持续时长 | 过滤临时波动(编译、备份) |
效果:
- 短暂的编译、备份不会告警
- 只有真正持续繁忙才告警
- 误报率从 80% 降到 < 10%
2.3 正确示例 2:区分 CPU 模式
整机 CPU 告诉你"忙不忙",但不知道"为什么忙"。
promql
# 用户态 CPU 过高 → 应用代码效率问题
avg(rate(node_cpu_seconds_total{mode="user"}[5m])) by (instance) > 0.8
for: 10m
# 系统态 CPU 过高 → 系统调用过多(网络/磁盘 IO)
avg(rate(node_cpu_seconds_total{mode="system"}[5m])) by (instance) > 0.3
for: 10m
# iowait 过高 → 磁盘 IO 瓶颈
avg(rate(node_cpu_seconds_total{mode="iowait"}[5m])) by (instance) > 0.2
for: 5m
价值:不只知道"CPU 高",还知道"为什么高"。
erlang
场景 1:整机 CPU 90%,同时 user 高
→ 运维:应用代码有问题,去排查业务
场景 2:整机 CPU 90%,同时 iowait 高
→ 运维:磁盘 IO 慢,去排查磁盘
场景 3:整机 CPU 90%,同时 system 高
→ 运维:系统调用多,可能是网络问题
这就是区分模式的价值:同样是 CPU 90%,但问题的根因完全不同,处理方式也不同。
3. 内存告警:Linux 的内存不是你想的那样
3.1 错误示例
promql
# 错误示例:用 MemFree 计算使用率
(1 - node_memory_MemFree_bytes / node_memory_MemTotal_bytes) * 100 > 90
会发生什么?
makefile
16G 内存的服务器:
MemTotal: 16G
MemFree: 1G (真正空闲)
Cached: 8G (文件缓存,可释放)
错误计算:使用率 = (16 - 1) / 16 = 93.75%
→ 告警:"内存不足!"
→ 运维登录查看
→ 发现 8G 是 cache,随时可以释放
→ 误报!
问题:不理解 Linux 内存管理机制。
Linux 会把空闲内存用来做 cache,提升文件读取性能。这些 cache 在应用需要内存时会自动释放,不应该算作"已使用"。
3.2 正确示例 1:用 MemAvailable
promql
# 正确示例:用 MemAvailable 计算
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 90
for: 5m
MemAvailable 是什么?
ini
MemAvailable = MemFree + 可回收的 Cache + 可回收的 Buffer
这才是真正可用的内存。
注意:
node_memory_MemAvailable_bytes在 node_exporter v0.16+ 版本才可用。如果你的版本较旧,需要先升级。
对比:
ini
场景:16G 内存服务器
MemFree: 1G
Cached: 8G (可回收)
MemAvailable: 9G
用 MemFree:
使用率 = (16 - 1) / 16 = 93.75% → 误报
用 MemAvailable:
使用率 = (16 - 9) / 16 = 43.75% → 正确
3.3 正确示例 2:预测未来耗尽
当前可用内存充足,但如果有内存泄漏,迟早会耗尽。
promql
# 预测 4 小时后可用内存是否耗尽
predict_linear(node_memory_MemAvailable_bytes[1h], 4*3600) < 0
for: 10m
predict_linear 的原理:
案例:
ini
10:00 - MemAvailable = 10G
10:30 - MemAvailable = 8G
11:00 - MemAvailable = 6G
每小时下降 4G
→ 预测 4 小时后:6G - 16G = -10G < 0
→ 触发告警:"预计 4 小时后内存耗尽"
价值:提前 4 小时发现风险,而不是等到真的耗尽。
这就是 predict_linear() 的威力:从"被动应对"变成"主动预防"。
4. 磁盘告警:当前值和未来趋势
4.1 错误示例
promql
# 错误示例:磁盘使用率 > 85% 就告警
(1 - node_filesystem_avail_bytes / node_filesystem_size_bytes) * 100 > 85
会发生什么?
erlang
场景 1:磁盘使用率 86%,但日志增长很快
→ 触发告警
→ 运维去清理日志
→ 但 2 小时后磁盘满了
→ 服务挂了
为什么?因为 85% 的阈值太晚了!
问题:
- 不知道磁盘增长速度
- 等到 85% 再告警,可能来不及处理
- 没有考虑"未来会怎样"
4.2 正确示例 1:预测式告警
promql
# 基于过去 6 小时数据,预测 24 小时后磁盘是否满
predict_linear(node_filesystem_avail_bytes[6h], 24*3600) < 0
for: 1h
价值:
erlang
场景 1:磁盘使用率 82%,但增长很快
→ 规则 1 告警:"预测 24h 后满"
→ 运维:优先处理,明天就会满
场景 2:磁盘使用率 82%,但增长很慢
→ 规则 1 不告警:预测 7 天后才满
→ 运维:不紧急,排期清理即可
同样是 82%,但处理优先级完全不同。
4.3 正确示例 2:容量和 IO 分开
磁盘有两类问题:
- 容量问题:快满了
- 性能问题:IO 慢了
promql
# 容量告警:使用率 > 90%
# 注意:过滤掉临时目录和虚拟文件系统
(1 - node_filesystem_avail_bytes{
fstype=~"ext4|xfs",
mountpoint!~"/tmp|/var/lib/docker|/boot"
} / node_filesystem_size_bytes) * 100 > 90
for: 10m
# 性能告警:IO 使用率 > 80%
rate(node_disk_io_time_seconds_total{device=~"sd.*|vd.*"}[5m]) > 0.8
for: 10m
# 性能告警:IO 队列长度 > 10
rate(node_disk_io_time_weighted_seconds_total{device=~"sd.*|vd.*"}[5m]) > 10
for: 5m
为什么要过滤路径?
diff
不需要告警的挂载点:
- /tmp:临时目录,满了也无所谓
- /var/lib/docker:容器存储,由 Docker 管理
- /boot:引导分区,通常很小且不会变化
只关注业务数据盘:
- /:根分区
- /data:数据分区
- /home:用户目录
为什么要分开?
yaml
场景 1:磁盘使用率 50%,但 IO 队列很长
→ 容量充足,但性能差
→ 需要优化 IO(换 SSD、减少小文件操作)
场景 2:磁盘使用率 95%,但 IO 正常
→ 容量不足,但性能正常
→ 需要清理磁盘或扩容
两类问题,两种处理方式。
**运维口诀**:容量看趋势,性能看队列。
---
## 5. 网络告警:流量和错误
### 5.1 错误示例
```promql
# 错误示例:网络流量 > 100MB/s 就告警
rate(node_network_receive_bytes_total[5m]) / 1024 / 1024 > 100
问题:
- 100MB/s 对千兆网卡是正常的(125MB/s 满载)
- 没有区分"流量大"和"异常流量"
- 没有考虑错误率
5.2 正确示例:流量 + 错误率
promql
# 网络流量超过带宽的 80%
# 注意:排除回环接口和虚拟接口
rate(node_network_receive_bytes_total{
device!~"lo|docker.*|veth.*|br-.*"
}[5m]) / (node_network_speed_bytes * 0.8) > 1
for: 5m
# 网络错误率 > 0.1%
rate(node_network_receive_errs_total{
device!~"lo|docker.*|veth.*|br-.*"
}[5m])
/
rate(node_network_receive_packets_total{
device!~"lo|docker.*|veth.*|br-.*"
}[5m]) > 0.001
for: 5m
为什么要过滤接口?
markdown
不需要监控的网络接口:
- lo:回环接口,本地通信
- docker0, br-*:Docker 网桥
- veth*:容器虚拟网卡
只监控物理网卡:
- eth0, eth1:传统命名
- ens*, enp*:systemd 命名
- bond*:网卡绑定
改进点:
- 相对阈值(带宽的 80%)而不是绝对值
- 关注错误率,不只是流量
- 错误率 0.1% 很小,但可能是硬件问题的信号
场景:
场景 1:流量高,错误率正常
→ 业务正常繁忙,无需告警
场景 2:流量正常,错误率高
→ 网卡/交换机有问题,需要排查硬件
场景 3:流量高,错误率也高
→ 可能是 DDoS 攻击或网络故障
6. 告警规则设计方法论
基于上面的案例,可以总结出告警规则设计的三个维度:
平滑抖动] B --> F[持续时长: for 10m
过滤毛刺] C --> G[P0: 服务不可用
立即处理] C --> H[P1: 性能下降
1小时内] C --> I[P2: 资源紧张
当天处理] D --> J[区分场景
生产/测试] D --> K[关联指标
多维度判断] style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#ffcccc style D fill:#ccffcc
6.1 时间维度
评估窗口 [5m]:
- 作用:平滑 15 秒抓取间隔的抖动
- 选择:CPU/内存用 5m,磁盘用 6h(变化慢)
持续时长 for: 10m:
- 作用:过滤短暂峰值
- 选择:CPU 用 10m,磁盘用 1h(不需要那么敏感)
6.2 严重程度
三个级别:
| 级别 | 含义 | 响应时间 | 典型场景 |
|---|---|---|---|
| P0 | 服务不可用 | 立即 | 磁盘满、OOM |
| P1 | 性能下降 | 1 小时内 | CPU 95%、内存 95% |
| P2 | 资源紧张 | 当天 | CPU 85%、磁盘 80% |
设计原则:
- P0 的
for更短(5m),不能等太久 - P2 的
for更长(30m),避免误报
6.3 业务上下文
区分场景:
promql
# 生产环境 90% 告警
cpu_usage{env="prod"} > 0.9
# 测试环境 95% 告警(更宽松)
cpu_usage{env="test"} > 0.95
关联指标:
promql
# CPU 高 + iowait 高 → 可能是磁盘慢
cpu_usage > 0.9 and cpu_iowait > 0.2
# CPU 高 + 网络流量高 → 可能是网络处理
cpu_usage > 0.9 and network_traffic > 100MB/s
这就是"因地制宜"的价值:不同场景用不同规则,不能一刀切。
7. 好的告警系统应该解决什么问题
有了规则,还需要一个引擎来执行。好的告警系统应该解决这些问题:
7.1 Fingerprint:告警实例的唯一标识
同一个规则可能命中多个实例:
ini
规则: CPU 使用率 > 90%
命中:
- {instance="server-01", env="prod"} → Fingerprint = "abc123"
- {instance="server-02", env="prod"} → Fingerprint = "def456"
Fingerprint = hash(rule_id + labels)
每个 Fingerprint 是一个独立的告警,需要单独追踪:
- 什么时候开始的?
- 触发了几次?
- 现在状态如何?
7.2 防抖动:避免告警风暴
两层防抖:
观察期] B --> C{持续 >= for?} C -->|是| D[firing
确认告警] C -->|否| A D --> E{5分钟内重复?} E -->|是| F[跳过处理] E -->|否| G[写库/通知] style B fill:#fff4e1 style D fill:#ffcccc style F fill:#cccccc style G fill:#ccffcc
第一层:for_duration(规则层)
- 持续 10 分钟才从 pending 转为 firing
- 过滤短暂毛刺
第二层:时间窗口去重(消费层)
- 5 分钟内同一告警只处理一次
- 避免频繁写库和通知
7.3 可扩展:插件化数据源
指标] A --> C[日志扫描
关键字] A --> D[SQL 查询
数据库] A --> E[黑盒探测
HTTP/TCP] style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#ffe1f5 style D fill:#ffcccc style E fill:#ccffcc
不只是 Prometheus 指标,还可以扩展:
- 日志关键字(如"OutOfMemoryError")
- SQL 规则(如"超时订单 > 100")
- 黑盒探测(如"API 5xx 错误")
7.4 静默管理:计划性维护
场景:
erlang
晚上 22:00-23:00 发布新版本
→ 服务会重启
→ CPU 会短暂 100%
→ 不应该告警
解决方案:时间窗口静默
yaml
静默规则:
name: "发布窗口"
start: "22:00"
end: "23:00"
match:
env: "prod"
service: "api"
在发布窗口内,匹配的告警自动静默,不发通知。
7.5 告警历史:可追溯和分析
sql
-- 历史告警表
CREATE TABLE history_alerts (
fingerprint VARCHAR(64),
rule_name VARCHAR(255),
start_at DATETIME,
end_at DATETIME,
duration INT,
trigger_count INT
);
价值:
- 查看某台服务器过去一周的告警次数
- 分析哪些规则误报率高
- 计算平均恢复时间(MTTR)
告警历史不只是记录,更是持续改进的基础。没有数据就没有优化,好的系统要能自我进化。
8. 总结
8.1 从指标到告警的核心思想
不是简单设阈值,而是:
-
理解指标的本质
- CPU 是累计时间,要用 rate() 转成使用率
- 内存要用 MemAvailable,不是 MemFree
- 磁盘要看趋势,不只看当前值
-
加上时间维度
- 评估窗口
[5m]平滑抖动 - 持续时长
for: 10m过滤毛刺
- 评估窗口
-
考虑业务上下文
- 区分生产和测试环境
- 关联多个指标(CPU + iowait)
- 预测未来(predict_linear)
8.2 好的告警规则长什么样
| 维度 | 坏规则 | 好规则 |
|---|---|---|
| 阈值 | CPU > 80% | CPU > 90%,for: 10m |
| 指标 | 只看一个指标 | 关联多个指标 |
| 时间 | 只看当前值 | 看趋势(predict_linear) |
| 上下文 | 一刀切 | 区分场景(生产/测试) |
8.3 好的告警系统解决什么问题
五个核心能力:
- Fingerprint ------ 每个告警实例有唯一标识
- 防抖动 ------ 两层过滤(for + 时间窗口去重)
- 可扩展 ------ 不只是 Prometheus,还能接其他数据源
- 静默管理 ------ 计划性维护不告警
- 历史追溯 ------ 可分析、可改进
8.4 三个核心洞察
看完这篇文章,记住这三点就够了:
1. 好的告警要能定位问题
不只说"CPU 高",还要说"是 user 高还是 iowait 高"。不只告诉你有问题,还要告诉你什么问题。
2. 好的告警要能预测未来
用 predict_linear() 提前 4 小时、24 小时发现风险,而不是等到真的出问题才知道。从"被动应对"变成"主动预防"。
3. 好的系统要能自我进化
记录告警历史,分析误报率,持续优化规则。没有数据就没有改进,没有历史就没有进化。
附录:告警规则自查清单
在上线告警规则前,用这个清单检查一遍:
CPU 告警
- 是否使用了
rate()而不是直接用 Counter 值? - 阈值是否 >= 90%(而不是 80%)?
- 是否设置了
for: 10m持续时长? - 是否区分了 user/system/iowait 模式?
内存告警
- 是否使用了
MemAvailable而不是MemFree? - 版本是否 >= node_exporter v0.16?
- 是否使用了
predict_linear()预测未来? - P0 告警阈值是否 >= 95%?
磁盘告警
- 容量告警是否使用了
predict_linear()? - 是否过滤了
/tmp、/boot等临时目录? - 是否只监控
ext4和xfs文件系统? - 是否分别设置了容量告警和 IO 告警?
- IO 告警是否过滤了设备类型(如
device=~"sd.*|vd.*")?
网络告警
- 是否过滤了
lo、docker0、veth*等虚拟接口? - 流量告警是否使用了相对阈值(带宽的 80%)?
- 是否同时监控了流量和错误率?
- 错误率阈值是否足够敏感(如 0.1%)?
通用检查
- 所有规则是否都设置了
for持续时长? - P0/P1/P2 的严重级别是否合理?
- 是否区分了生产和测试环境?
- 规则名称是否清晰表达了告警内容?
- 是否添加了
annotations描述?
快速参考:核心规则速查
| 监控项 | 推荐规则 | 阈值 | for | 说明 |
|---|---|---|---|---|
| CPU 整机 | (1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m]))) > 0.9 |
90% | 10m | 基础告警 |
| CPU iowait | avg(rate(node_cpu_seconds_total{mode="iowait"}[5m])) > 0.2 |
20% | 5m | 磁盘瓶颈 |
| 内存可用 | (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) > 0.9 |
90% | 5m | 基础告警 |
| 内存预测 | predict_linear(node_memory_MemAvailable_bytes[1h], 4*3600) < 0 |
- | 10m | 提前预警 |
| 磁盘容量 | (1 - node_filesystem_avail_bytes / node_filesystem_size_bytes) > 0.9 |
90% | 10m | 基础告警 |
| 磁盘预测 | predict_linear(node_filesystem_avail_bytes[6h], 24*3600) < 0 |
- | 1h | 提前预警 |
| 磁盘 IO | rate(node_disk_io_time_seconds_total[5m]) > 0.8 |
80% | 10m | 性能问题 |
| 网络流量 | rate(node_network_receive_bytes_total[5m]) / node_network_speed_bytes > 0.8 |
80% | 5m | 带宽瓶颈 |
| 网络错误 | rate(node_network_receive_errs_total[5m]) / rate(node_network_receive_packets_total[5m]) > 0.001 |
0.1% | 5m | 硬件问题 |
从 node_exporter 的指标,到生产级的告警规则,核心是一套设计思想:
- 理解本质(指标的含义)
- 加上时间(评估窗口 + 持续时长)
- 结合上下文(业务场景 + 关联指标)
- 持续迭代(追踪历史 + 质量改进)
这不是一篇操作手册,而是一套方法论。掌握了这套思想,你就能设计出适合自己业务的告警规则。