概述
告警是监控系统的"最后一公里",也是最难做好的一环。一个常见的困境是:服务器上跑着几十个告警规则,每天产生上百条告警通知,值班工程师在微信/钉钉/邮件的轮番轰炸下逐渐麻木——真正紧急的告警被淹没在噪声中,直到客户投诉才发现系统早已出问题。
SRE 的黄金法则是:每一条告警都应该有明确的处理动作。如果一个告警收到后既不需要立即处理,也不需要记录跟踪,那它就不应该存在。从告警疲劳问题出发,详细梳理告警分级、SLO-based 告警设计、抑制与聚合策略、告警度量指标和治理方法,帮助你从"告警噪声"中提取出真正的"信号"。
参考来源:Google SRE Book《Monitoring Distributed Systems》、Prometheus 告警好的实践
一、告警疲劳:问题的根源
1.1 告警泛滥的典型表现
某团队告警统计(一周):
┌──────────────────────────┬────────┬──────────┐
│ 告警类型 │ 数量 │ 实际处理 │
├──────────────────────────┼────────┼──────────┤
│ CPU 使用率 > 80% │ 156 │ 3 │
│ 磁盘使用率 > 70% │ 89 │ 2 │
│ Pod 重启 │ 34 │ 5 │
│ HTTP 5xx 错误率 > 1% │ 12 │ 4 │
│ 数据库连接数 > 80% │ 8 │ 1 │
│ 证书即将过期 │ 3 │ 1 │
│ 服务不可达 │ 2 │ 2 │
├──────────────────────────┼────────┼──────────┤
│ 总计 │ 304 │ 18 │
└──────────────────────────┴────────┴──────────┘
有效告警率:18/304 = 5.9%
只有不到 6% 的告警需要实际处理,其余 94% 都是噪声。这种状态下,值班工程师的行为模式会变成:
- 看到告警通知 → 粗略扫一眼 → 判断"又是老问题" → 忽略
- 真正紧急告警到来时 → 也被忽略 → 事故扩大
- 事后复盘:“告警太多了,没注意到那条关键的”
1.2 告警泛滥的常见原因
| 原因 | 表现 | 根因 |
|---|---|---|
| 阈值不合理 | CPU > 80% 频繁告警 | 阈值太低,正常负载就触发 |
| 告警无分级 | 所有告警都发到同一个群 | 缺乏 severity 区分 |
| 缺少抑制 | 上游故障引发下游连锁告警 | 未配置 inhibit_rules |
| 重复告警 | 同一问题每小时通知一次 | repeat_interval 配置不当 |
| 告警无文档 | 收到告警不知道怎么处理 | 缺少 runbook 链接 |
| 自动恢复告警 | Pod 重启后立即恢复 | 恢复通知也发到群里 |
1.3 信号 vs 噪声
告警质量 = 信号量 / 总告警量
信号(Signal):需要人工干预或关注的问题
噪声(Noise):自动恢复、重复、误报、无意义的告警
目标:信号占比 > 80%,噪声占比 < 20%
SRE 原则:如果一条告警不能指向一个具体的处理动作(修复、扩容、记录、排查),它就不应该存在。宁可少告警,也不要告警疲劳。
二、告警分级策略
2.1 四级告警体系
| 级别 | 标签 | 通知方式 | 响应时间 | 示例 |
|---|---|---|---|---|
| P0 - 紧急 | severity=critical | 电话 + 短信 + IM | < 5 分钟 | 核心服务宕机、数据丢失 |
| P1 - 严重 | severity=warning | IM + 邮件 | < 30 分钟 | 部分功能降级、错误率升高 |
| P2 - 提醒 | severity=info | 邮件 / 日报 | 工作时间内 | 磁盘使用率 70%、证书 30 天过期 |
| P3 - 记录 | severity=debug | 仅记录不通知 | 无 | 测试环境告警、非核心组件 |
2.2 分级路由配置
# Alertmanager 路由配置
route:
receiver: default
group_by: ['alertname', 'cluster', 'service']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
# P0 - 紧急告警:立即电话通知
- matchers:
- severity = critical
receiver: critical-phone
group_wait: 0s
repeat_interval: 30m # 每 30 分钟重复提醒
# P1 - 严重告警:IM 群通知
- matchers:
- severity = warning
receiver: warning-im
group_wait: 30s
repeat_interval: 2h
# P2 - 提醒类:邮件通知
- matchers:
- severity = info
receiver: info-email
group_wait: 5m
repeat_interval: 12h
# P3 - 记录类:不通知
- matchers:
- severity = debug
receiver: null
receivers:
- name: critical-phone
webhook_configs:
- url: 'http://phone-gateway/alert' # 电话通知网关
send_resolved: true
- name: warning-im
webhook_configs:
- url: 'http://dingtalk-webhook/alert' # 钉钉/企业微信
- name: info-email
email_configs:
- to: 'ops-team@example.com'
- name: null
# 不配置任何通知方式
2.3 告警分级原则
告警分级决策树:
服务是否对外提供核心功能?
├── 否 → severity=info/debug(不打扰值班)
└── 是 → 是否影响大量用户(> 1% 用户感知)?
├── 否 → severity=warning
└── 是 → 是否导致功能完全不可用?
├── 否 → severity=warning
└── 是 → severity=critical
关键原则:
- 以用户影响为标准:不是"CPU 高了"就 critical,而是"用户无法下单"才 critical
- P0 告警必须少:每周不超过 2-3 条 P0,否则说明分级有问题
- 通知渠道匹配级别:电话用于 P0,IM 用于 P1,邮件用于 P2
- 夜间降级:非工作时间降低 P1/P2 通知频率,P0 不降级
三、阈值告警的问题
3.1 传统阈值告警的陷阱
# 典型的阈值告警规则
- alert: HighCPU
expr: 100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 5m
labels:
severity: warning
这个规则看起来没问题,但实际运行中会遇到:
| 问题 | 场景 | 后果 |
|---|---|---|
| 阈值难定 | CPU 80% 对某些服务正常,对另一些已危险 | 误报或漏报 |
| 瞬时尖峰 | 定时任务导致 CPU 短暂飙升 | 大量无效告警 |
| 容量差异 | 大实例 CPU 50% 的负载远超小实例 80% | 阈值不适用所有实例 |
| 无用户视角 | CPU 80% 但用户无感知 | 告警无实际意义 |
| 上下文缺失 | 不知道这是否影响 SLO | 无法判断严重性 |
3.2 多级阈值方案
改善方法之一是配置多级阈值,减少误报:
groups:
- name: cpu-alerts
rules:
# P2 - 提醒:CPU > 85%,持续 30 分钟
- alert: HighCPUWarning
expr: |
100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
for: 30m
labels:
severity: info
annotations:
summary: "CPU 使用率偏高: {{ $labels.instance }}"
description: "CPU 使用率 {{ $value }}% 超过 85%,持续 30 分钟"
# P1 - 严重:CPU > 95%,持续 5 分钟
- alert: HighCPUCritical
expr: |
100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 95
for: 5m
labels:
severity: warning
annotations:
summary: "CPU 使用率极高: {{ $labels.instance }}"
description: "CPU 使用率 {{ $value }}% 超过 95%,可能影响服务"
runbook: "https://wiki.internal/runbooks/high-cpu"
这比单一阈值更好,但仍然是"猜测阈值"——你不知道 85% 或 95% 是否真的对应用户感知到的问题。
四、SLO-based 告警
4.1 什么是 SLO-based 告警
SLO(Service Level Objective)是基于服务质量目标的告警方式。核心理念是:不要告警"某指标超过阈值",而是告警"即将消耗完错误预算"。
错误预算(Error Budget)= 1 - SLO 目标
示例:SLO = 99.9% 可用性
错误预算 = 1 - 0.999 = 0.1%
30 天窗口内允许的不可用时间 = 30 × 24 × 60 × 0.1% = 43.2 分钟
SLO-based 告警不是问"CPU 是否高",而是问"我们是否在过快地消耗错误预算"。
4.2 多窗口多燃烧率告警
Google SRE 推荐使用多窗口多燃烧率(Multi-Window Multi-Burn-Rate)策略:
groups:
- name: slo-alerts
rules:
# 快速燃烧:1h 窗口消耗 2% 错误预算(14.4x 燃烧率)
# → 在 1 小时内就能发现严重问题
- alert: SLOBurnRateFast
expr: |
(
sum(rate(http_requests_total{status=~"5.."}[1h])) /
sum(rate(http_requests_total[1h]))
) > (1 - 0.999) * 14.4
and
(
sum(rate(http_requests_total{status=~"5.."}[5m])) /
sum(rate(http_requests_total[5m]))
) > (1 - 0.999) * 14.4
for: 2m
labels:
severity: critical
slo: availability-999
annotations:
summary: "SLO 错误率快速燃烧"
description: "近 1 小时错误率超 SLO 14.4 倍,预计 2 小时内耗尽错误预算"
runbook: "https://wiki.internal/runbooks/slo-burn"
# 慢速燃烧:6h 窗口消耗 5% 错误预算(6x 燃烧率)
# → 发现持续性、非突发的服务质量下降
- alert: SLOBurnRateSlow
expr: |
(
sum(rate(http_requests_total{status=~"5.."}[6h])) /
sum(rate(http_requests_total[6h]))
) > (1 - 0.999) * 6
and
(
sum(rate(http_requests_total{status=~"5.."}[30m])) /
sum(rate(http_requests_total[30m]))
) > (1 - 0.999) * 6
for: 15m
labels:
severity: warning
slo: availability-999
annotations:
summary: "SLO 错误率慢速燃烧"
description: "近 6 小时错误率超 SLO 6 倍,预计 5 天内耗尽错误预算"
4.3 多窗口多燃烧率参数
| 窗口组合 | 燃烧率 | 消耗预算 | 告警级别 | 目的 |
|---|---|---|---|---|
| 1h + 5m | 14.4x | 2% (1h) | critical | 快速发现严重问题 |
| 6h + 30m | 6x | 5% (6h) | warning | 发现持续性问题 |
| 1d + 2h | 3x | 10% (1d) | warning | 长期趋势监控 |
| 3d + 6h | 1x | 10% (3d) | info | 预算消耗跟踪 |
多窗口多燃烧率逻辑:
短窗口 + 长窗口 都超阈值 → 告警
(避免短窗口的瞬时尖峰误报,同时保证响应速度)
4.4 SLO 告警 vs 阈值告警对比
| 维度 | 阈值告警 | SLO 告警 |
|---|---|---|
| 关注点 | 某指标是否超阈值 | 用户体验是否受影响 |
| 误报率 | 高(阈值难精确设定) | 低(基于实际错误率) |
| 上下文 | 缺失 | 有(错误预算消耗进度) |
| 用户相关性 | 间接 | 直接 |
| 运维负担 | 高(需频繁调阈值) | 低(SLO 目标稳定) |
| 告警数量 | 多 | 少(只有影响 SLO 的才告警) |
核心理念:SLO 告警回答的是"是否需要立即行动以保护用户体验",而不是"某个数字是否好看"。这是从"基础设施监控"到"用户体验监控"的思维转变。
五、告警抑制与聚合
5.1 抑制规则(Inhibition)
当上游故障发生时,下游会产生大量连锁告警。抑制规则可以在高级别告警触发时,自动静默低级别相关告警。
# Alertmanager inhibit_rules
inhibit_rules:
# 当服务不可达时,抑制该服务的 CPU/内存告警
- source_matchers:
- alertname = ServiceDown
target_matchers:
- alertname =~ 'HighCPU|HighMemory|DiskSpaceWarning'
equal: ['service', 'instance']
# 当集群级别告警触发时,抑制该集群下所有实例告警
- source_matchers:
- alertname = ClusterDown
- severity = critical
target_matchers:
- severity =~ 'warning|info'
equal: ['cluster']
# 当数据库主节点宕机时,抑制从节点同步延迟告警
- source_matchers:
- alertname = MySQLMasterDown
target_matchers:
- alertname = MySQLReplicationLag
equal: ['cluster']
5.2 告警聚合(Grouping)
通过 group_by 将相关告警合并为一条通知,减少通知数量:
route:
group_by: ['alertname', 'cluster', 'service'] # 按告警名+集群+服务分组
group_wait: 30s # 首次告警等待 30s,收集同组告警
group_interval: 5m # 同组后续告警间隔 5 分钟
repeat_interval: 4h # 重复通知间隔 4 小时
聚合效果示例:
聚合前(无 group_by):
[10:00:01] CPU 高 - web-01
[10:00:02] CPU 高 - web-02
[10:00:03] CPU 高 - web-03
[10:00:05] 内存高 - web-01
[10:00:06] 磁盘高 - web-01
→ 5 条独立通知
聚合后(group_by: ['alertname', 'cluster']):
[10:00:30] CPU 高 (web-01, web-02, web-03)
[10:05:00] 内存高 (web-01) + 磁盘高 (web-01)
→ 2 条聚合通知
5.3 告警去重
在双副本 Prometheus 场景下,两个实例都会产生相同的告警。Alertmanager 通过 Gossip 协议去重:
# Alertmanager 启动参数(集群模式)
alertmanager \
--cluster.listen-address=0.0.0.0:9094 \
--cluster.peer=alertmanager-2:9094 \
--cluster.peer=alertmanager-3:9094 \
--cluster.gossip-interval=200ms \
--cluster.pushpull-interval=1m
六、告警度量与治理
6.1 关键告警度量指标
| 指标 | 定义 | 目标值 | 计算方式 |
|---|---|---|---|
| 告警准确率 | 有效告警 / 总告警 | > 80% | 人工标注 + 定期审计 |
| MTTR | 平均故障恢复时间 | < 30min | 事故记录 |
| 误报率 | 误报告警 / 总告警 | < 10% | 告警 + 工单匹配 |
| 告警数量 | 每日告警总数 | 趋势下降 | Prometheus 指标 |
| 告警静默率 | 被静默的告警 / 总告警 | < 20% | Alertmanager API |
| P0 告警数 | 每周 P0 告警数 | < 5 | 告警记录 |
| 告警恢复率 | 自动恢复告警 / 总告警 | < 30% | Alertmanager metrics |
6.2 用 Prometheus 度量告警
# 告警度量规则
groups:
- name: alert-metrics
rules:
# 每日告警总数
- record: alerts:fired:total_per_day
expr: increase(ALERTS{alertstate="firing"}[24h])
# 各级别告警数量
- record: alerts:fired:by_severity
expr: count by(severity) (ALERTS{alertstate="firing"})
# 自动恢复率
- record: alerts:auto_resolved:rate
expr: |
sum(rate(ALERTS_FOR_STATE{alertstate="resolved"}[1h])) /
sum(rate(ALERTS_FOR_STATE[1h]))
# P0 告警数量(周)
- record: alerts:critical:per_week
expr: increase(ALERTS{alertstate="firing", severity="critical"}[7d])
6.3 告警治理流程
┌─────────────────────────────────────────────────────┐
│ 告警治理闭环 │
│ │
│ 1. 告警审计(每月) │
│ │ │
│ ▼ │
│ 2. 分类标注:有效 / 误报 / 噪声 / 缺文档 │
│ │ │
│ ▼ │
│ 3. 分析根因:阈值不当 / 缺少抑制 / 规则过时 │
│ │ │
│ ▼ │
│ 4. 优化措施:调阈值 / 加抑制 / 删规则 / 补文档 │
│ │ │
│ ▼ │
│ 5. 效果验证:下月告警数量和准确率对比 │
│ │ │
│ ▼ │
│ 6. 持续迭代 ←─────────────────────────────────────── │
└─────────────────────────────────────────────────────┘
6.4 告警审计脚本
#!/bin/bash
# alert-audit.sh — 月度告警审计报告
AM_URL="http://alertmanager:9093"
PROM_URL="http://prometheus:9090"
echo "========== 月度告警审计报告 =========="
echo "时间范围: $(date -d '1 month ago' '+%Y-%m-%d') ~ $(date '+%Y-%m-%d')"
echo ""
# 各级别告警数量
echo "## 告警数量统计"
curl -s "$PROM_URL/api/v1/query" \
--data-urlencode 'query=sum by(severity)(increase(ALERTS{alertstate="firing"}[30d]))' | \
jq -r '.data.result[] | " \(.metric.severity): \(.value[1])"'
echo ""
# Top 10 高频告警
echo "## Top 10 高频告警"
curl -s "$PROM_URL/api/v1/query" \
--data-urlencode 'query=topk(10, sum by(alertname)(increase(ALERTS{alertstate="firing"}[30d])))' | \
jq -r '.data.result[] | " \(.metric.alertname): \(.value[1])"'
echo ""
# 自动恢复率
echo "## 自动恢复率"
curl -s "$PROM_URL/api/v1/query" \
--data-urlencode 'query=sum(rate(ALERTS{alertstate="resolved"}[30d])) / sum(rate(ALERTS[30d]))' | \
jq -r '.data.result[0].value[1] | " 自动恢复率: \(.* 100 | floor)%"' 2>/dev/null
echo ""
echo "=========================================="
七、告警规则编写规范
7.1 告警规则模板
- alert: <AlertName> # 驼峰命名,简洁明确
expr: <PromQL> # 查询表达式
for: <duration> # 持续时间,避免瞬时告警
labels:
severity: <level> # critical/warning/info
service: <service> # 受影响的服务
team: <team> # 负责团队
annotations:
summary: "<一句话描述>" # 简短摘要
description: "<详细描述>" # 包含当前值和影响
runbook: "<URL>" # 处理文档链接
dashboard: "<URL>" # Grafana 仪表盘链接
7.2 好告警 vs 坏告警
# ✗ 坏告警:信息不足,无法行动
- alert: HighCPU
expr: cpu_usage > 80
for: 5m
labels:
severity: warning
# ✓ 好告警:信息完整,可行动
- alert: APIServerHighCPU
expr: |
100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle", job="api-server"}[5m])) * 100) > 90
for: 10m
labels:
severity: warning
service: api-server
team: platform
annotations:
summary: "API Server CPU 使用率超过 90%"
description: "实例 {{ $labels.instance }} CPU 使用率 {{ $value }}%,持续 10 分钟,可能影响 API 响应延迟"
runbook: "https://wiki.internal/runbooks/api-server-high-cpu"
dashboard: "https://grafana.internal/d/api-server?var-instance={{ $labels.instance }}"
7.3 告警规则 Review Checklist
| 检查项 | 要求 |
|---|---|
| 告警名称 | 简洁明确,能从名称判断问题 |
| 持续时间 | 配置合理的 for 时间,避免瞬时尖峰 |
| 严重性标签 | 正确的 severity 分级 |
| 摘要 | 一句话说清楚发生了什么 |
| 描述 | 包含当前值、影响范围、相关实例 |
| Runbook | 有可操作的处理步骤文档 |
| 仪表盘 | 有 Grafana 仪表盘链接 |
| 抑制 | 已配置相关的 inhibit_rules |
| 去重 | 已加入合适的 group_by |
八、告警降噪实战技巧
8.1 利用 for 消除瞬时尖峰
# 磁盘使用率告警:for 15m 避免日志写入尖峰
- alert: DiskSpaceWarning
expr: |
(node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} /
node_filesystem_size_bytes{fstype!~"tmpfs|overlay"}) * 100 < 20
for: 15m # 持续 15 分钟才告警
labels:
severity: warning
8.2 利用预测函数提前告警
# 预测磁盘将在 24 小时内写满
- alert: DiskWillFillIn24h
expr: |
predict_linear(node_filesystem_avail_bytes[2h], 24 * 3600) < 0
for: 1h
labels:
severity: warning
annotations:
summary: "磁盘预计 24 小时内写满: {{ $labels.instance }}"
description: "基于过去 2 小时趋势预测,{{ $labels.mountpoint }} 将在 24 小时内写满"
8.3 利用 absent 检测数据缺失
# Exporter 不可达导致数据缺失
- alert: ExporterDown
expr: absent(up{job="node-exporter"})
for: 5m
labels:
severity: critical
annotations:
summary: "所有 node-exporter 数据缺失"
description: "可能整个监控网络故障,或所有 node-exporter 都已下线"
8.4 按时间范围智能告警
# 工作时间才告警非关键问题
- alert: LowReplicaCountBusinessHours
expr: |
kube_deployment_status_replicas < kube_deployment_spec_replicas
and on() (hour() >= 8 and hour() < 22 and day_of_week() > 0 and day_of_week() < 6)
for: 10m
labels:
severity: warning
九、从告警到行动:Runbook
9.1 Runbook 的价值
告警的价值不在于通知本身,而在于通知后能被快速处理。Runbook(运维手册)是连接告警与处理动作的桥梁。
告警通知 → Runbook 链接 → 诊断步骤 → 修复操作 → 验证 → 关闭告警
9.2 Runbook 模板
# Runbook: API Server CPU 使用率过高
## 告警信息
- 告警名称: APIServerHighCPU
- 严重性: warning
- 影响范围: API 响应延迟可能升高
## 诊断步骤
### 1. 确认告警真实性
检查 Grafana 仪表盘确认 CPU 是否确实持续高位
- 仪表盘: https://grafana.internal/d/api-server
### 2. 检查异常进程
```bash
ssh {{ $labels.instance }}
top -c -b -n 1 | head -20
3. 检查请求量
确认是否有异常流量激增
curl -s http://localhost:9090/api/v1/query?query=rate(http_requests_total[5m])
修复操作
情况 A: 流量激增
- 检查是否有异常请求源
- 考虑临时扩容 Pod 副本数
情况 B: 资源泄漏
- 检查 Goroutine 数量
- 考虑滚动重启 Pod
升级路径
- 如果 30 分钟内无法解决 → 升级为 P0 → 通知架构师团队
## 总结
构建高质量告警体系是一个持续优化的过程,核心要点:
- **告警即行动**:每条告警必须有明确的处理动作,不能行动的告警应该删除或降级为记录
- **分级是基础**:四级别体系让值班工程师知道该关注什么、忽略什么
- **SLO 是方向**:从阈值告警迁移到 SLO-based 告警,以用户体验为中心
- **抑制降噪**:利用 Alertmanager 的 inhibit 和 group_by 消除连锁告警和重复通知
- **度量驱动治理**:定期审计告警准确率、MTTR、误报率,用数据驱动持续优化
- **Runbook 闭环**:每条告警都应该有对应的处理文档,告警→诊断→修复→验证形成闭环
告警治理不是一次性工程,而是持续迭代的过程。每月审计、定期优化、持续迭代,才能让告警体系从"噪声制造机"变成"可靠的故障信号灯"。
## 参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
1. [Prometheus 告警好的实践](https://prometheus.io/docs/practices/alerting/) — Prometheus 官方,参考了Prometheus 告警好的实践相关内容