56.5 事故响应
一句话
事故响应是一套在信息不全时仍能推进的流程:先分级、再定角色、按节奏通报、以止血为第一目标;恢复之后再谈根因。
先破除四个误区
| 误区 | 现实 |
|---|---|
| “事故 = 故障” | 故障是技术事实,事故是需要协调与通报的事件。一个自动恢复的故障可以不是事故;一个没影响用户但引发信任危机的事件可能是 S1 |
| “先查根因,再处理” | 根因可能要几小时;用户每分钟都在受损。响应阶段的第一目标是止血,根因放到恢复之后 |
| “响应靠自觉” | 没有角色、没有时间盒、没有通报节奏,响应会退化成“几个人在群里各查各的” |
| “通报 = 甩锅” | 通报是让干系人知道“影响、动作、下次更新时间”,避免重复询问与谣言;写事实,不写猜测 |
分级:先定“多大”,再定“怎么办”
| 级别 | 触发条件(满足任一) | 响应 | 通报间隔 | 状态页 | 复盘 |
|---|---|---|---|---|---|
| S1 | 核心功能不可用;资损;数据丢失/错乱;安全事件;≥30% 用户受影响;燃烧率 ≥ 14.4 | 立即,全员 | 15 分钟 | 是 | 必须 |
| S2 | 部分用户受影响(≥5%);资损 ≥ 20 万/小时;燃烧率 ≥ 6 | 15 分钟 | 30 分钟 | 是 | 必须 |
| S3 | 可感知但可降级;≥1% 用户受影响;燃烧率 ≥ 1 | 工作时间/30 分钟 | 60 分钟 | 否 | 轻量 |
| S4 | 无用户感知;内部指标异常 | 下一工作日 | 按需 | 否 | 否 |
⚠️ 表里的数字(30%/5%/1%、100 万/20 万、燃烧率 14.4/6)是示例阈值,必须按业务规模、资损承受力与 SLO 改写;照抄别人的阈值,等于用别人的风险偏好做自己的决策。
分级要就高不就低:拿不准时先按高一级响应,10 分钟后可以降级;反过来(先低后高)会浪费最宝贵的止损窗口。
角色:谁负责什么
| 角色 | 职责 | 不该做什么 |
|---|---|---|
| IC(事故指挥) | 定级、定节奏、拍板止血方案、对外代表事故 | 亲自排查(会失去全局视角) |
| OL(运维/技术 lead) | 组织排查与执行止血 | 绕过 IC 直接对外承诺 |
| CL(沟通 lead) | 写通报、维护状态页、对接业务/客服/高管 | 猜测原因、承诺未确认的恢复时间 |
| Scribe(记录) | 记时间线、决策、动作、指标 | 参与排查(会漏记) |
| SME(领域专家) | 提供根因线索与修复方案 | 各自为战、不汇报进展 |
小团队可以一人多角,但IC 与 OL 不能是同一个人:一个负责“决定做什么”,一个负责“怎么做”。
时间线:每个阶段都有目标
| 阶段 | 目标 | 关键动作 |
|---|---|---|
| 发现 → 认领 | ≤ 5 分钟 | 告警路由、ack(见 56.3 on-call (值班 / 升级路径 / runbook));告警质量决定发现速度(见 56.4 告警治理) |
| 认领 → 宣布事故 | ≤ 5 分钟 | 定级、拉群、指定 IC/CL/Scribe |
| 宣布 → 止血 | ≤ 30 分钟(S1) | 回滚/降级/限流/切流(见 55.3 回滚设计) |
| 止血 → 确认恢复 | ≤ 30 分钟 | 验证核心链路、清脏数据、确认无二次故障 |
| 恢复 → 复盘 | ≤ 5 个工作日 | 见 56.6 无指责复盘 |
下面的程序把“分级”和“S1 时间线”跑出来:
package main
import "fmt"
type impact struct {
usersPct float64 // 受影响用户比例
revenue float64 // 预计资损(万元/小时)
burn float64 // 错误预算燃烧倍率
dataLoss bool
security bool
}
type plan struct {
sev string
ic string
commsMin int
statusPage bool
execNotify bool
postmortem bool
}
func classify(i impact) (plan, string) {
switch {
case i.security:
return plan{"S1", "必须", 15, true, true, true}, "安全事件"
case i.dataLoss:
return plan{"S1", "必须", 15, true, true, true}, "数据丢失/错乱"
case i.usersPct >= 30 || i.revenue >= 100 || i.burn >= 14.4:
return plan{"S1", "必须", 15, true, true, true}, "影响面/资损/燃烧率触顶"
case i.usersPct >= 5 || i.revenue >= 20 || i.burn >= 6:
return plan{"S2", "必须", 30, true, true, true}, "部分用户受影响或快速燃烧"
case i.usersPct >= 1 || i.revenue > 0 || i.burn >= 1:
return plan{"S3", "建议", 60, false, false, true}, "可感知但可降级/慢烧"
default:
return plan{"S4", "不需要", 120, false, false, false}, "无用户感知"
}
}
func main() {
cases := []struct {
name string
i impact
}{
{"支付成功率 99.9% → 90%", impact{usersPct: 40, revenue: 200, burn: 20}},
{"某地域错误率 5%", impact{usersPct: 8, revenue: 30, burn: 7}},
{"消息积压,用户无感", impact{usersPct: 0.5, revenue: 0, burn: 1.2}},
{"订单表被误删", impact{usersPct: 2, revenue: 5, burn: 3, dataLoss: true}},
}
fmt.Println("== 事故分级 ==")
fmt.Printf("%-26s %-4s %-6s %-9s %-8s %-8s %-6s %s\n", "场景", "级别", "IC", "通报间隔", "状态页", "通知高管", "复盘", "依据")
for _, c := range cases {
p, why := classify(c.i)
fmt.Printf("%-26s %-4s %-6s %-9s %-8v %-8v %-6v %s\n",
c.name, p.sev, p.ic, fmt.Sprintf("%d 分钟", p.commsMin), p.statusPage, p.execNotify, p.postmortem, why)
}
// S1 的时间线:每一步都有目标,超出就是流程问题
type phase struct {
name string
target, at int
}
phases := []phase{
{"发现→认领", 5, 6},
{"认领→宣布事故", 5, 9},
{"宣布→止血", 30, 38},
{"止血→确认恢复", 30, 95},
}
fmt.Println("\n== S1 时间线(支付故障)==")
total := 0
breach := 0
for _, ph := range phases {
status := "ok"
if ph.at > ph.target {
status = fmt.Sprintf("超 %d 分钟", ph.at-ph.target)
breach++
}
fmt.Printf(" %-16s 目标 %3d 分钟 实际 %3d 分钟 %s\n", ph.name, ph.target, ph.at, status)
total += ph.at
}
fmt.Printf(" 从发现到确认恢复:%d 分钟(%d 个阶段超时)\n", total, breach)
fmt.Println(" 注意:'止血' 与 '恢复' 是两个里程碑;对用户报 '已恢复' 之前,必须先验证核心链路")
}
本机运行结果:
== 事故分级 ==
场景 级别 IC 通报间隔 状态页 通知高管 复盘 依据
支付成功率 99.9% → 90% S1 必须 15 分钟 true true true 影响面/资损/燃烧率触顶
某地域错误率 5% S2 必须 30 分钟 true true true 部分用户受影响或快速燃烧
消息积压,用户无感 S3 建议 60 分钟 false false true 可感知但可降级/慢烧
订单表被误删 S1 必须 15 分钟 true true true 数据丢失/错乱
== S1 时间线(支付故障)==
发现→认领 目标 5 分钟 实际 6 分钟 超 1 分钟
认领→宣布事故 目标 5 分钟 实际 9 分钟 超 4 分钟
宣布→止血 目标 30 分钟 实际 38 分钟 超 8 分钟
止血→确认恢复 目标 30 分钟 实际 95 分钟 超 65 分钟
从发现到确认恢复:148 分钟(4 个阶段超时)
注意:'止血' 与 '恢复' 是两个里程碑;对用户报 '已恢复' 之前,必须先验证核心链路
最刺眼的是“止血→确认恢复 95 分钟”:止血只用了 38 分钟,但确认恢复多花了 95 分钟。常见原因是:核心链路验证脚本缺失、数据校验没有基线、或者不敢对外说恢复。这一段往往不被当成“响应时间”,但它决定用户真正受苦多久。
通报模板:三段,别写小说
【S1 支付故障】影响:约 40% 用户支付失败(10:02 起)
当前动作:已回滚 10:00 的发布,错误率从 30% 降到 2%,正在验证核心链路
下次更新:10:45(每 15 分钟更新;无进展也更新)
三条纪律:
- 只写事实:已确认的影响、已执行的动作、下一次更新时间;不写“可能”“大概”“正在排查”。
- 按节奏更新:即使没有进展,也要在承诺时间点发一条“仍在处理,下次 X 点更新”——沉默会引发更多询问。
- 恢复要验证:对用户说“已恢复”之前,先跑核心链路冒烟 + 数据校验;错误地宣布恢复比晚宣布更伤信任。
止血优先:先选手段,再谈根因
| 手段 | 适用 | 代价 | 何时用 |
|---|---|---|---|
| 回滚 | 最近有发布/配置变更 | 可能丢新功能 | 第一优先(见 55.3 回滚设计) |
| 降级 | 非核心功能拖垮核心 | 功能缺失 | 回滚不确定时 |
| 限流 | 流量超过容量 | 部分用户被拒 | 容量问题、攻击 |
| 切流 | 单地域/单机房故障 | 切换风险 | 故障域隔离(见 36.4 两地三中心与多活) |
| 修数据 | 脏数据已产生 | 二次错误风险 | 先停写,再补偿 |
| 改代码 | 明确的小 bug | 发布风险 | 以上都不可行时 |
顺序原则:能回滚先回滚,能降级先降级,能限流先限流;改代码是最后手段,因为它引入新的不确定性。
长事故:交接与轮换
超过 2 小时的事故,IC 和主要排查人必须轮换:
| 项 | 要求 |
|---|---|
| 交接内容 | 当前影响、已排除的假设、当前动作、下一步、未处理的风险 |
| 交接方式 | 书面(事故群置顶/文档),不只口头 |
| 节奏 | 每 2 小时或按值班班次轮换 |
| 记录 | 交接时间与接手人写进时间线 |
不轮换的后果是判断力下降:凌晨 4 点的 IC 做的决定,往往不如 8 点接班的 IC。
生产落地:指名 → 可抄 → 可验 → 可退
指名:事故编排用 PagerDuty(事件时间线、升级、状态页)或 incident.io(Slack 原生、角色分配);小团队用“事故群 + 置顶模板 + 计时器”也能跑。关键不是工具,是角色与节奏写死。
可抄(本机无账号,以下命令未验证):
# PagerDuty:手动创建事件(S1)
curl -s -X POST https://events.pagerduty.com/v2/enqueue \
-H 'Content-Type: application/json' \
-d '{"routing_key":"'"$PD_KEY"'","event_action":"trigger",
"payload":{"summary":"S1 支付成功率下降","source":"manual","severity":"critical",
"custom_details":{"impact":"40% 用户支付失败","commander":"oncall-lead"}}}'
# 事故群置顶模板(三段)
# 影响:… 当前动作:… 下次更新:…
可验:本节分级器真跑过,输出如上;它验证的是“分级规则与时间线判定”,不依赖工具。线上至少验证:一次演练中能否在 5 分钟内定级并指定 IC、15 分钟内发出第一条对外通报、恢复前跑完核心链路冒烟。
可退:
- 定级过高:10 分钟内可降级,并在群里说明“降级原因”,避免团队长期紧绷。
- 止血失败:按 55.3 回滚设计 的“回滚失败预案”走,不要连续试错。
- 通报出错:立即更正并说明“哪条信息错了、现在的正确信息是什么”;不要删消息假装没发生。
反模式
| 反模式 | 后果 |
|---|---|
| 没有 IC,所有人各查各的 | 决策慢、重复劳动、对外口径不一致 |
| 先查根因再止血 | 用户受损时间被拉长 |
| 通报写“正在排查” | 干系人反复询问,CL 被拖垮 |
| 承诺“很快恢复” | 未兑现会透支信任 |
| 恢复未验证就宣布 | 二次故障,用户信任受损 |
| 长事故不轮换 | 判断力下降,错误决策 |
| 把复盘开成追责会 | 信息隐藏,下次更糟(见 56.6 无指责复盘) |
| 只处理不记录 | 复盘无时间线,改进无依据 |
发布类事故的分级与止损顺序,可对照 82.4 发布事故(待补)的案例复盘。
自检
Q1 为什么事故响应要先止血、后根因?
▸ 答案:用户按分钟受损,根因可能要几小时。先止血(回滚/降级/限流/切流)能把影响面与持续时间压到最小;根因分析放在恢复之后,且不影响止血决策。例外:如果止血手段本身有数据风险(如修数据),要先停写、评估后再动。
Q2 IC 为什么不能同时做排查?
▸ 答案:IC 的职责是全局:定级、定节奏、协调资源、对外代表事故。一旦亲自排查,就会陷入细节、失去全局视角,忘记通报与升级。IC 可以懂技术,但响应期间要“动手不动手,只做决策与协调”。小团队可以让 IC 兼记录,但不能兼主要排查人。
Q3 为什么“已恢复”必须验证后才能宣布?
▸ 答案:“止血”只表示指标暂时回落,不代表数据正确、依赖恢复、二次故障消失。未验证就宣布,常见后果是几分钟后再次故障,用户与干系人的信任被透支。验证至少包括:核心链路冒烟、关键数据校验、错误率/P99 稳定一段时间。
Q4 长事故为什么要强制轮换 IC?
▸ 答案:人在长时间高压下判断力下降,容易固执于一个假设、忽略新信息。轮换让新的 IC 带着新鲜视角接手,同时通过书面交接保留上下文。轮换不是“换人背锅”,是保持决策质量的组织手段。
Q5 通报里为什么不能写“正在排查”?
▸ 答案:“正在排查”没有信息量,干系人无法判断影响与下一步,只会反复询问。有效通报必须包含:已确认的影响、已执行的动作、下一次更新时间。没有进展也要按节奏更新,沉默比坏消息更伤人。
关联
- 前置:55.3 回滚设计、56.3 on-call (值班 / 升级路径 / runbook)
- 并列:56.4 告警治理
- 下游:56.6 无指责复盘
- 待补:82.4 发布事故