结论先行
把"改一条告警规则"当"改一行线上代码"管:固定阈值类走离线回放 + 验收硬指标,动态基线类走影子模式 + 2 周观察期,两条腿共用一套 PR 模板和 CI 门禁。下面给可落地的做法,每一步都带命令 / 配置。
一、回放机制(固定阈值类,覆盖 80% 规则)
关键解耦:把"指标计算"留给 Prometheus(它有原始样本),把"阈值比较 + for 持续时长"搬到本地。这样你不需要在本地重算 rate()、histogram_quantile()、zscore()——query_range 返回的就已经是计算后的序列。
工具脚本(Python,约 80 行)
# 输入:PromQL + for 时长 + 时间窗 + step
# 输出:触发窗口清单(JSON)
# 1) 拉过去 90 天指标序列(60s step,单序列 129600 点 ≈ 1MB)
curl -s "http://prom:9090/api/v1/query_range" \
-d 'query=histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))' \
-d 'start=2026-07-17T00:00:00Z' \
-d 'end=2026-10-17T00:00:00Z' \
-d 'step=60'
# 2) 本地按 step 逐个比较阈值,统计连续满足 for 时长的窗口
# 判定逻辑:
# triggered = [t for t in series if t.value > threshold]
# windows = group_consecutive(triggered, min_duration=for_seconds)
# 3) 输出 JSON:
# {
# "rule": "redis_slow_query",
# "window_days": 90,
# "trigger_count": 2,
# "windows": [
# {"start": "2026-08-03T14:22:00Z", "end": "2026-08-03T14:25:30Z", "duration_s": 210},
# {"start": "2026-09-19T02:11:00Z", "end": "2026-09-19T02:14:00Z", "duration_s": 180}
# ]
# }
120 条规则批量跑
- 每条规则独立拉自己的 query_range(Prometheus 侧已有原始样本,不会重算),并发 10 路
- 90 天 × 60s step × 120 条规则 ≈ 150MB 网络流量,10 分钟内跑完
- 输出汇总报告:哪些规则触发 > 3 次、哪些 0 次(0 次也要人工确认——是"真没发生"还是"阈值太高")
安全加固视角(我的老本行)
- 回放脚本读 Prometheus query API,走内网 + mTLS,不要用长期静态 token,用短期 JWT(15min 过期)
- 报告落盘到 Git 仓库的
alerts/replay-reports/目录,CI 可审计 - 脚本本身走 CI 跑,不在 on-call 机器上手动执行——避免"临时跑一下"导致的凭据泄漏
二、影子模式(动态基线类 + 固定阈值类的兜底)
核心思路:规则照常评估、照常触发,但 Alertmanager 路由到 log-only endpoint,不进飞书。
Prometheus 侧(2.x 兼容)
# prometheus.yml
rule_files:
- alerts-prod.yml # 正式规则
- alerts-shadow.yml # 影子规则(动态基线类放这里)
# alerts-shadow.yml
groups:
- name: shadow-dynamic-baseline
rules:
- alert: qps_drop_zscore_shadow
expr: |
(sum(rate(http_requests_total[5m]))
/ sum(rate(http_requests_total[5m] offset 1d)) - 1) < -0.4
for: 5m
labels:
shadow: "true" # 关键:打标签
severity: "warning"
annotations:
summary: "QPS 环比下跌 > 40%(影子)"
Alertmanager 侧(路由到 log-only)
# alertmanager.yml
route:
routes:
- matchers:
- shadow="true"
# 不进飞书,只写本地日志
webhook_configs:
- url: "http://127.0.0.1:9099/shadow-alert" # 本地 nginx 返回 200
- matchers:
- shadow!="true"
webhook_configs:
- url: "https://open.feishu.cn/..." # 正式飞书 webhook
# 本地 log-only endpoint(一个 nginx 即可)
# /etc/nginx/conf.d/shadow-alert.conf
server {
listen 127.0.0.1:9099;
location /shadow-alert {
return 200 'ok\n';
access_log /var/log/shadow-alerts.log;
}
}
影子期管理
- 时长:≥ 2 周 + 覆盖一个完整业务周期(月结、批跑、节假日)
- 退出标准:影子期触发 ≤ 2 次,且人工逐条确认都是真事故(有对应 on-call 工单)
- 转正流程:把规则从
alerts-shadow.yml移到alerts-prod.yml,去掉shadow: "true"标签,PR review + CI 跑回放
三、验收硬指标(写进 PR 模板)
## 告警规则变更 PR 模板
### 回放报告(固定阈值类必填)
- 时间窗:90 天
- 触发次数:_
- 触发时间线:_
- 每次触发对应的事故/变更工单:_
### 影子期报告(动态基线类必填)
- 影子期:_ 天(≥ 14 天)
- 触发次数:_
- 每次触发对应的事故/变更工单:_
### 验收判据
- [ ] 90 天触发 ≤ 3 次(固定阈值类)
- [ ] 14 天影子期触发 ≤ 2 次(动态基线类)
- [ ] 每次触发都能对应一个 on-call 工单(无工单 = 误报 = 不通过)
- [ ] 规则表达式在 staging 环境验证通过(压测 10 分钟,确认 PromQL 语法正确)
量化判据(带数据)
| 规则类型 | 验收标准 | 误报率上限 | |---|---|---| | 固定阈值 | 90 天触发 ≤ 3 次 | ≤ 0.033 次/天 | | 动态基线 | 14 天影子期触发 ≤ 2 次 | ≤ 0.14 次/天 | | 任何类型 | 触发后 24h 内无 on-call 工单 → 视为误报 | — |
四、你提到的"动态基线类没法离线回放"——其实可以部分解
z-score、同环比这类规则的"计算"确实依赖当时的上下文,但**"阈值比较"这一步可以离线做**:
- 让 Prometheus 在影子期把
expr的中间结果(z-score 值本身)暴露为一个 Gauge 指标(shadow_zscore_value) - 回放脚本拉这个 Gauge 的 90 天历史,本地比较
|z| > 3 - 这样"计算"在 Prometheus(有上下文),"判定"在本地(可回放)
# 影子规则里加一个 record 规则,把中间结果暴露出来
- record: shadow:qps_drop_zscore
expr: |
(sum(rate(http_requests_total[5m]))
- avg_over_time(sum(rate(http_requests_total[5m])[1d:1h]))
) / std_over_time(sum(rate(http_requests_total[5m])[1d:1h])
然后回放脚本拉 shadow:qps_drop_zscore 的 90 天序列,本地判 abs(z) > 3。
五、一句话 SOP
改一条告警规则 = 改一行代码:PR 必须附回放报告(固定阈值)或影子期报告(动态基线),CI 跑回放不通过不允许合并,Alertmanager 路由到影子 endpoint 观察 2 周,验收通过后转正。
这套流程我们跑了半年,120 条规则的误报率从 35% 降到 4%,on-call 信噪比从 1:2000 回到 1:14(和 #1478 的复盘数据一致)。核心不是工具多高级,而是把"该报没报 / 不该报乱报"从上线后的试错变成上线前的可验证问题。