8.5 配套代码:告警规则与有效率复盘
对应小节:8.5 告警设计:燃烧率与领先指标 两部分:① 完整的告警规则(含 runbook);② 告警有效率复盘脚本。
一、完整的告警规则
# observability/alerts.yml
groups:
# ═══════════════════════════════════════════════════════════
# P0:page(立即叫人)
# ═══════════════════════════════════════════════════════════
- name: p0-burn-rate
rules:
- alert: ErrorBudgetBurnFast
# 双窗口:长窗口抑制误报 + 短窗口保证灵敏
expr: |
slo:orders_availability:burn_rate_1h > 14.4
and
slo:orders_availability:burn_rate_5m > 14.4
for: 2m
labels:
severity: page
slo: orders-availability
annotations:
summary: "订单查询错误预算燃烧过快(1h + 5m 双窗口同时超标)"
description: |
当前燃烧率 {{ $value | humanize }}×,按此速度约
{{ printf "%.1f" (div 30 (div 1 $value)) }} 天将耗尽 30 天预算。
runbook: |
【5 分钟内的动作】
1. 打开 Grafana 的「SLO 与错误预算」看板,看「按接口分解」面板
→ 确定是哪个接口在消耗预算
2. 查看最近 30 分钟的发布记录
→ 如果有发布,优先考虑回滚(见 runbook-rollback)
3. 查看相邻的「饱和度」面板
→ 连接池 pending / 队列深度 / CPU 节流,哪一项在涨?
4. 如果 5 分钟内无法定位 → 升级给值班负责人
- name: p0-critical-unavailable
rules:
- alert: ServiceUnavailable
expr: |
sum(rate(http_requests_total{status!~"5.."}[2m]))
/ sum(rate(http_requests_total[2m])) < 0.9
for: 2m
labels:
severity: page
annotations:
summary: "整体成功率低于 90%(持续 2 分钟)"
runbook: |
1. 确认是全局还是部分实例(看 per-instance 分解)
2. 检查依赖:数据库可达?Redis 可达?下游可达?
3. 检查最近的变更(发布/配置/DB 迁移)
4. 考虑限流降级(保护核心链路)
# ═══════════════════════════════════════════════════════════
# P1:ticket(当天处理)
# ═══════════════════════════════════════════════════════════
- name: p1-leading-indicators
rules:
# ① 连接池排队 —— 最早的信号
- alert: ConnectionPoolSaturated
expr: max(db_pool_pending) > 5
for: 10m
labels:
severity: ticket
annotations:
summary: "连接池排队(pending > 5 持续 10 分钟)"
runbook: |
⚠️ 【先查慢查询,不要先加池】
1. 查最贵的语句:
psql -c "SELECT calls, mean_exec_time, total_exec_time, left(query,50)
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;"
2. 查长事务与锁等待:
SELECT pid, now()-xact_start AS age, state, wait_event_type
FROM pg_stat_activity WHERE xact_start IS NOT NULL
ORDER BY age DESC LIMIT 10;
3. 只有在【查询正常】且【并发确实高】时,才用 Little's Law 调整池大小
详见第 6.5 节
# ② 容器 CPU 节流 —— "本地好、上线慢"的头号原因
- alert: ContainerCpuThrottled
expr: |
rate(container_cpu_cfs_throttled_periods_total{container="app"}[5m])
/ rate(container_cpu_cfs_periods_total{container="app"}[5m]) > 0.25
for: 10m
labels:
severity: ticket
annotations:
summary: "容器被 CPU 配额节流 > 25%"
runbook: |
1. 确认 CPU limit:cat /sys/fs/cgroup/cpu.max
2. 判断 CPU 需求是否合理(用火焰图找浪费,比如日志/序列化)
3. 需求合理 → 提高 limit;需求不合理 → 优化代码
详见第 4.8 节
# ③ 线程池队列积压
- alert: ExecutorQueueBacklog
expr: executor_queue_depth > 100
for: 10m
labels:
severity: ticket
annotations:
summary: "线程池队列积压 > 100"
runbook: |
1. 查线程在忙什么(是阻塞 IO 还是真的在算):
jcmd <pid> Thread.print | grep -A3 "BLOCKED\|socketRead\|HikariPool"
2. 如果是阻塞 IO → 考虑异步化或增加隔离
3. 如果是真的在算 → 考虑扩容或优化算法
详见第 6.5 节
# ④ 错误预算消耗过半
- alert: ErrorBudgetHalfConsumed
expr: slo:orders_availability:budget_remaining < 0.5
for: 1h
labels:
severity: ticket
annotations:
summary: "错误预算已消耗 50%(30 天窗口)"
runbook: |
1. 看「按接口分解」确定主要消耗来源
2. 如果是缓慢消耗 → 安排优化
3. 如果是突发消耗 → 按 ErrorBudgetBurnFast 的 runbook 处理
4. 考虑暂停非必要的功能发布(第 8.4 节的预算策略)
# ⑤ 容量余量不足
- alert: CapacityHeadroomLow
expr: |
max_over_time(sum(rate(http_requests_total[5m]))[1h:5m])
/ on() group_left capacity_safe_limit > 0.8
for: 30m
labels:
severity: ticket
annotations:
summary: "峰值 QPS 已达到安全容量的 80%"
runbook: |
1. 确认流量趋势(是临时高峰还是持续增长)
2. 持续增长 → 启动扩容流程(第 8.7 节的容量表)
3. 检查是否有异常流量(爬虫、攻击、重试风暴)
# ═══════════════════════════════════════════════════════════
# P2:log(定期 review,不打扰人)
# ═══════════════════════════════════════════════════════════
- name: p2-observe-only
rules:
- alert: GcPauseHigh
expr: |
histogram_quantile(0.99,
sum by (le) (rate(jvm_gc_pause_seconds_bucket[5m]))) > 0.2
for: 10m
labels:
severity: log
annotations:
summary: "GC 停顿 P99 > 200ms(仅记录)"
runbook: |
⚠️ 先观察,不要立刻行动:
1. 把 GC 停顿时间戳与 P99 尖刺做时间对齐(第 6.6 节)
2. 对齐 → 升级为 ticket 并处理
3. 不对齐 → 考虑删除此告警(它经常不是真问题)
- alert: SlowRequestCountHigh
expr: increase(http_request_duration_seconds_count{le="1.0"}[10m]) == 0
for: 10m
labels:
severity: log
annotations:
summary: "10 分钟内没有请求慢于 1 秒(分布过于干净,可能是采样问题)"
- alert: FileDescriptorUsageHigh
expr: process_open_fds / process_max_fds > 0.7
for: 30m
labels:
severity: log
annotations:
summary: "文件描述符使用率 > 70%(可能是泄漏的前兆)"
runbook: "观察趋势:如果持续上升,按第 3.5 节的浸泡分析方法排查"
二、告警有效率复盘脚本
# tools/alert-review.py <ALERTMANAGER_API> [DAYS]
"""
统计每条告警的触发次数与有效率,用于校准阈值。
有效率 = 需要人行动的触发次数 / 总触发次数
用法:
python3 tools/alert-review.py http://alertmanager:9093 30
"""
import json
import sys
import urllib.parse
import urllib.request
from collections import defaultdict
from datetime import datetime, timedelta
def fetch_alerts(alertmanager_url, days):
"""从 Alertmanager 拉取历史告警"""
since = (datetime.now() - timedelta(days=days)).isoformat()
url = f"{alertmanager_url}/api/v2/alerts?" + urllib.parse.urlencode({
"active": "false",
"silenced": "false",
"inhibited": "false",
})
try:
with urllib.request.urlopen(url, timeout=10) as r:
return json.load(r)
except Exception as e:
print(f"⚠️ 无法从 Alertmanager 拉取数据:{e}", file=sys.stderr)
return []
def main(url, days):
alerts = fetch_alerts(url, days)
print("═" * 88)
print(f"告警复盘(最近 {days} 天)")
print("═" * 88)
print()
if not alerts:
print("(没有拉到数据 —— 请确认 Alertmanager 地址,或改用日志/工单系统统计)")
print()
print("替代方案:手工统计")
print(" 1. 从告警渠道(Slack/PagerDuty)导出历史")
print(" 2. 统计每条告警的触发次数")
print(" 3. 人工标注哪些需要行动(有效)、哪些是误报")
print(" 4. 填入下面的表格")
print()
print_alerts_template()
return
by_name = defaultdict(lambda: {"total": 0, "severity": ""})
for a in alerts:
name = a.get("labels", {}).get("alertname", "?")
by_name[name]["total"] += 1
by_name[name]["severity"] = a.get("labels", {}).get("severity", "?")
print(f"{'告警名':<40}{'级别':>10}{'触发次数':>10} 建议")
print("-" * 88)
for name, info in sorted(by_name.items(), key=lambda x: -x[1]["total"]):
n = info["total"]
if n > 30:
advice = "⚠️ 触发太频繁 —— 检查阈值是否太紧"
elif n > 10:
advice = "观察:确认有效率"
else:
advice = "✅ 频率合理"
print(f"{name[:38]:<40}{info['severity']:>10}{n:>10} {advice}")
print()
print("═" * 88)
print("下一步:人工标注有效率")
print()
print_alerts_template()
def print_alerts_template():
print("""统计模板:
| 告警名 | 触发次数 | 有效次数 | 有效率 | 处理 |
| --- | --- | --- | --- | --- |
| ErrorBudgetBurnFast | | | | 保持 |
| ConnectionPoolSaturated | | | | |
| GcPauseHigh | | | | |
| ... | | | | |
判断标准:
有效率 > 80% → 保持(甚至可以升级级别)
有效率 30%~80% → 检查阈值与 for 持续时间
有效率 < 30% → 放宽阈值、降级、或删除
调整手段(按优先级):
① 提高阈值(用第 5 章的噪声底线确定)
② 增加 for 持续时间(避免瞬时波动触发)
③ 改用双窗口(长窗口 + 短窗口)
④ 降级(page → ticket → log)
⑤ 删除(如果指标与真实问题不相关)
⚠️ 核心原则:宁可不告警,也不要每天误报。
因为告警一旦被忽略,真实故障也会被一起忽略。""")
print("═" * 88)
if __name__ == "__main__":
url = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:9093"
days = int(sys.argv[2]) if len(sys.argv) > 2 else 30
main(url, days)
三、runbook 模板
每条告警都应该有一个 runbook——它决定了「值班同学能不能独立处理」。
<!-- runbooks/ConnectionPoolSaturated.md -->
# ConnectionPoolSaturated
## 这是什么
数据库连接池出现排队(`db_pool_pending > 5` 持续 10 分钟)。
## 为什么重要
`pending > 0` 说明**请求在等连接**——系统余量正在被耗尽。
即使当前延迟还达标,一旦流量再涨一点就会崩(排队的非线性,第 0 章 0.5 节)。
## 5 分钟内的动作
### 第一步:确认不是误报
```bash
curl -s localhost:8080/metrics | grep db_pool
# 期望看到 pending > 0;如果 pending = 0,可能是告警延迟,稍等
第二步:查慢查询(⭐ 不要先加池)
psql -c "SELECT calls, mean_exec_time, total_exec_time, left(query,50)
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;"
- 有慢查询(mean 高或 total 大)→ 根因是查询慢,优化 SQL/加索引
- 查询都快 → 继续第三步
第三步:查长事务与锁
psql -c "SELECT pid, now()-xact_start AS age, state, wait_event_type, left(query,50)
FROM pg_stat_activity WHERE xact_start IS NOT NULL
ORDER BY age DESC LIMIT 10;"
- 有长事务 → 根因是事务范围过大,检查是否有忘记提交的事务
wait_event_type = Lock→ 查阻塞链
第四步:只有在前面都正常时,才考虑调池
用 Little’s Law 计算:
所需连接 = 峰值 QPS × 单次查询 P99
不要做的事
- ❌ 不要立刻加连接池 —— 那会把压力转移到数据库(第 7.6 节)
- ❌ 不要重启服务(会丢失现场,且不能解决问题)
相关文档
- 第 6.5 节:池化类瓶颈的诊断
- 第 7.6 节:连接池大小的推导
- 第 4.7 节:数据库与中间件观测
## 四、动手改造
| 改动 | 观察什么 |
| --- | --- |
| 用 `alert-review.py` 分析你的告警 | 找出触发最频繁的那些 |
| 给 `GcPauseHigh` 加 runbook 并按 runbook 处理一次 | 体会"有 runbook"与"没有 runbook"的差别 |
| 把 `ConnectionPoolSaturated` 的阈值从 5 改成 0 | 触发次数会上升——**验证"阈值要合理"** |
| 把某条告警从 page 降级为 ticket | 观察值班负担的变化 |
| 给每条告警写 runbook | 这是让告警可被独立处理的关键 |
## 五、这段代码的局限
- **`alert-review.py` 依赖 Alertmanager API**:如果告警走的是其他渠道(Slack、PagerDuty),需要改用它们的 API 或手工统计。
- **「有效率」需要人工标注**:脚本无法判断"这次触发是否需要行动"——**这是复盘的核心,必须人工做**。
- **runbook 需要随系统演进更新**:指标名、命令、阈值变化后,runbook 也要更新(否则会误导值班同学)。
- **`capacity_safe_limit` 需要作为一种指标上报**:它来自容量规划(第 8.7 节),需要手工配置。
- **告警规则里的 `slo:*` 指标依赖 recording rules**(第 8.4 节),必须先配置好。