16.3 告警与 on-call
有了流水线、有了灰度门禁,还缺最后一环:当坏版本真的上线了,谁在多久之内知道? 如果答案是「用户投诉之后」,那么前面所有的自动化都只是在缩小损失,而不是在阻止损失。告警系统的目标不是「出事就响」,而是「在用户明显受影响之前响,且只在需要人介入时响」。
本节把 TaskHub 推进到「用 SLO 驱动告警、用值班手册承接告警、用不追责复盘消化告警」:先用本机真实计算的错误预算与燃烧率讲清多窗口告警,再给出告警规则与值班清单。Prometheus 告警规则本机无法真跑,会在正文里标注。
16.3.1 告警分两类:症状型与原因型
一个反复被踩的坑是把原因当告警。例如「CPU 使用率 > 80%」——CPU 高但服务正常,你半夜被叫醒却什么也做不了。正确的做法是只对症状告警,用原因做排查:
| 类型 | 例子 | 是否该叫人 | 用途 |
|---|---|---|---|
| 症状型 | 5xx 比例超标、P99 延迟超标 | 是 | 直接反映用户受影响 |
| 原因型 | CPU 高、内存涨、磁盘满 | 否(做看板/低优告警) | 排查线索 |
| 变更型 | 刚发了新版本、刚改了配置 | 否 | 关联上下文 |
判断标准很简单:这条告警响了,值班的人有没有明确的动作? 没有,它就不该是 page(呼叫),最多是 ticket(工单)。把 CPU 高设成 page,结果就是告警疲劳——大家学会忽略它,真正的故障也一起被忽略。
16.3.2 SLO 与错误预算:把「可用性」变成数字
SLO(Service Level Objective)是一句可度量、可判定的承诺,例如「TaskHub 的创建任务接口,月度可用性 ≥ 99.9%」。由它派生出错误预算(error budget):一个月里允许「不达标」的总时长。
设 SLO = 0.999、月度 30 天,本机真算:
SLO=0.999 月度错误预算=43.2 分钟
也就是说,一个月里 TaskHub 可以累计坏 43.2 分钟。这个数字是后面一切的基础:告警不是「感觉变慢了就响」,而是「按当前速率,43.2 分钟预算会被烧完吗」。
要算燃烧率,先得有「好请求 / 总请求」这个原始计数。它必须在应用里埋点暴露出来——这正是第 10 章指标部分要接的东西。一个最小中间件:
type metrics struct {
total *prometheus.CounterVec
}
func (m *metrics) instrument(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sw := &statusWriter{ResponseWriter: w, code: 200}
next.ServeHTTP(sw, r)
// 按「接口 + 状态码」两个标签计数,SLO 只统计服务端错误
m.total.WithLabelValues(r.URL.Path, strconv.Itoa(sw.code)).Inc()
})
}
注意标签的基数(cardinality)问题:把 user_id、task_id 这种高基数字段做成标签,会让时序数据库爆炸。SLO 指标只需要 path(且要归一到路由模板而非真实 URL,否则 /tasks/123 和 /tasks/456 会变成两条时间线)和 code。
SLO 的取值本身也要诚实:不要定一个自己都达不到的目标。99.9% 意味着每月只能坏 43.2 分钟,这对一个还在快速迭代、每周发好几次版的服务是相当高的门槛。把 SLO 定得过高,结果不是服务变好,而是大家学会无视它。合理的起点是「过去三个月的实际可用性」向上取一个够得着但需要努力的值。
16.3.3 多窗口燃烧率:既不漏报也不吵人
燃烧率(burn rate) 定义为「当前错误率 ÷ 允许错误率」。燃烧率 = 1 表示按这个速率刚好在一个月内烧完预算;= 14 表示按这个速率 30 天 ÷ 14 ≈ 2.1 天就烧光。用本机真跑的一段程序,对四个窗口各算一次:
func (w Window) BurnRate() float64 {
return w.BadRatio / (1 - w.SLO)
}
func (w Window) TimeToExhaust(days float64) float64 {
br := w.BurnRate()
if br == 0 {
return 1e9
}
return days * 24 * 60 / br // 窗口总分钟数 ÷ 燃烧率
}
本机真实输出:
SLO=0.999 月度错误预算=43.2 分钟
窗口=1h 错误率=0.0140 燃烧率= 14.00x 按此速率耗尽预算=51.4 小时(2.1 天)
窗口=6h 错误率=0.0040 燃烧率= 4.00x 按此速率耗尽预算=180.0 小时(7.5 天)
窗口=1d 错误率=0.0015 燃烧率= 1.50x 按此速率耗尽预算=480.0 小时(20.0 天)
窗口=3d 错误率=0.0008 燃烧率= 0.80x 按此速率耗尽预算=900.0 小时(37.5 天,超出月度窗口)
这张表读起来很有意思:1 小时窗口里 1.4% 的错误率,燃烧率高达 14x,意味着按这个速率约 2.1 天就会烧光整月预算——必须立刻 page。而 3 天窗口里 0.08% 的错误率,燃烧率 0.8x,按这个速率预算根本烧不完(超出 30 天窗口),完全不用叫人。
多窗口多燃烧率的精髓就在这里:
| 窗口对 | 燃烧率阈值 | 预算消耗 | 动作 |
|---|---|---|---|
| 1h + 5m | 14.4 | 2% | page(快速烧) |
| 6h + 30m | 6 | 5% | page(中速烧) |
| 3d + 6h | 1 | 10% | ticket(慢速烧) |
「长窗口 + 短窗口」同时超标才告警,是为了避免抖动误报:短窗口负责「反应快」,长窗口负责「确认不是瞬时毛刺」。只用短窗口会太吵,只用长窗口会太慢——两个一起用,才能既快又稳。
16.3.4 告警规则(未在真实 Prometheus 上验证)
以下 Prometheus 规则未在本机真跑(本机无 Prometheus / Alertmanager 实例),仅作结构与表达式参考。用 sli:errors:ratio 这类记录规则预先算好错误率,再对燃烧率设阈值:
groups:
- name: taskhub-slo
rules:
- record: sli:errors:ratio_5m
expr: |
sum(rate(http_requests_total{job="taskhub",code=~"5.."}[5m]))
/ sum(rate(http_requests_total{job="taskhub"}[5m]))
- alert: TaskHubErrorBudgetFastBurn
expr: |
sli:errors:ratio_5m > (14.4 * 0.001)
and
sli:errors:ratio_1h > (14.4 * 0.001)
for: 2m
labels:
severity: page
annotations:
summary: "TaskHub 错误预算快速燃烧"
runbook: "https://wiki.internal/runbooks/taskhub-error-budget"
三处值得注意:for: 2m 让规则必须持续成立 2 分钟才触发,滤掉瞬时尖峰;severity: page 是分级标签,路由到不同接收渠道;runbook 直接给处置链接——没有 runbook 的告警等于把难题丢给半夜的人。
16.3.5 值班手册:告警响了之后做什么
值班手册(runbook)要写清「看到这条告警,按顺序做这几件事」。以「错误预算快速燃烧」为例:
- 确认影响面:看 5xx 比例、受影响接口、受影响租户数
- 关联近期变更:过去 1 小时有没有发布/配置变更/DB 迁移
- 若有近期变更,优先执行回滚(见 16.2.6 的回滚清单)
- 若无关变更,看依赖健康:数据库、Redis、下游服务
- 5 分钟内无法定位,升级到二级 on-call
- 恢复后:确认指标回到基线,再关闭告警
手册里最有用的一条往往是「优先回滚」。人在压力下容易想「再查一查原因」,但恢复服务优先级永远高于定位根因——根因可以事后慢慢查。
16.3.6 升级路径与轮值
单人 on-call 是反模式:他会疲劳、会漏看、会在关键时候不在。TaskHub 用两级升级:
一级 on-call(15 分钟响应)
└─ 15 分钟未确认 → 二级 on-call(资深工程师)
└─ 30 分钟未确认 → 值班负责人 + 拉事故群
每一级的响应时限与职责:
| 级别 | 响应时限 | 职责 |
|---|---|---|
| 一级 on-call | 15 分钟 | 确认告警、执行 runbook、必要时回滚 |
| 二级 on-call | 15 分钟 | 一级搞不定时介入,可跨服务排查 |
| 值班负责人 | 30 分钟 | 对外沟通、决定是否拉全员、宣布事故等级 |
「确认」不是「看到」,而是在告警渠道里回复「已接手」——这样系统才知道有人在处理,不会继续升级。
轮值的基本原则:谁写坏的谁更容易被叫醒是有意为之——它让每个人在写代码时就想着可观测性。但轮值不能是惩罚,否则大家会倾向于隐瞒故障。配套的是 16.3.8 的复盘文化。
衡量轮值健不健康,有一个很朴素的指标:每人每周被 page 的次数。业界经验值是不超过 2 次/周;超过这个数,团队会开始「告警疲劳」,也就是看见告警先划掉、过一会儿再看——那时告警系统实际上已经失效了。一旦超过,就该回头看哪些告警是原因型误报,把它们降级。
16.3.7 静默与维护窗口
发布、迁移、演练期间指标会短暂变差,如果不管就会触发一堆告警。Alertmanager 提供了**静默(silence)**机制,按标签临时屏蔽:
# 一次计划发布的静默:匹配 job=taskhub 且 severity=page
matchers:
- name: job
value: taskhub
- name: severity
value: page
startsAt: "2026-10-07T03:00:00Z"
endsAt: "2026-10-07T03:30:00Z"
createdBy: "release-bot"
comment: "canary rollout v1.5.0"
本节这段静默配置未在本机验证(无 Alertmanager 实例)。但纪律是明确的:静默必须有时限、有理由、有创建者,且发布完成后自动过期。手动的、无限期的静默是事故的温床——它会把真实故障一起静音,而且没人记得去关掉它。
与静默相对的是维护窗口:对「计划内停机」这类可预期的事件,应让监控知道「现在是预期的」,而不是靠静默去掩盖。
16.3.8 不追责复盘(blameless postmortem)
复盘的目标是改进系统,不是找人。一份合格的复盘模板包含:
| 部分 | 内容 |
|---|---|
| 时间线 | 从第一次异常到恢复,逐条带时间戳 |
| 影响 | 受影响租户数、时长、消耗的预算比例 |
| 根因 | 技术根因 + 为什么没被更早发现 |
| 处置 | 实际做了什么,哪些有效哪些无效 |
| 行动项 | 可验证、有负责人、有截止日期的改进 |
关键在最后一行:行动项必须可验证。「加强监控」不是行动项,「为 /tasks 接口增加 5xx 燃烧率告警,负责人 X,截止 Y」才是。没有行动项的复盘,等于把同一个故障预约给下个月。
16.3.9 告警自检清单
- 只对症状告警,原因类做看板或工单
- 每条 page 告警都有明确动作和 runbook 链接
- SLO 与错误预算已量化(TaskHub:99.9% → 43.2 分钟/月)
- 用多窗口多燃烧率,长窗口确认、短窗口加速
- 告警有分级(page / ticket)与升级路径
- 复盘不追责,行动项可验证、有负责人、有期限
16.3.10 常见误区
- 把原因当告警:CPU/内存超标就叫醒人,却没定义「用户受了什么影响」。
- 没有 runbook 的 page:把人叫醒却不告诉他做什么。
- 只用短窗口:流量抖动导致误报,久而久之没人信告警。
- 只用长窗口:故障烧了半小时才响,预算已经烧掉大半。
- 无限期静默:掩盖了发布期间的真实故障,还常忘了关。
- 把复盘写成追责会:结果是下次没人敢如实汇报时间线。
- 高基数标签:
user_id进指标标签,时序库被撑爆。
小结
- 只对症状告警(5xx、延迟),原因类(CPU、内存)做排查线索而非呼叫依据;判据是「响了有没有明确动作」。
- SLO 派生出错误预算:TaskHub 99.9% 对应月度 43.2 分钟,本机实算。
- 燃烧率 = 当前错误率 ÷ 允许错误率;本机实测 1h 窗口 1.4% 错误率对应 14x 燃烧率,约 2.1 天烧光预算。
- 多窗口多燃烧率(1h+5m / 6h+30m / 3d+6h)同时超标才告警,兼顾快与稳。
- 本节 Prometheus 告警规则未在本机真跑(无 Prometheus 实例),仅作结构与表达式参考。
- 值班要有手册、有升级路径、有轮值;复盘不追责,行动项必须可验证。
- 静默必须有时限、有理由、有创建者,发布结束自动过期,避免掩盖真实故障。
到这里第 16 章结束:TaskHub 的发布链路已经完整——提交即验证、灰度放量、门禁回滚、告警值班。下一章把视角从「发布」转到「防御」:17.1 输入校验与注入防护 会用一段真跑的实验演示 SQL 注入如何越权读到别的租户,以及参数化查询如何挡下它。
阅读导航:上一节:16.2 灰度/蓝绿与回滚 · 下一节:17.1 输入校验与注入防护 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。