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 分钟更新;无进展也更新)

三条纪律:

  1. 只写事实:已确认的影响、已执行的动作、下一次更新时间;不写“可能”“大概”“正在排查”。
  2. 按节奏更新:即使没有进展,也要在承诺时间点发一条“仍在处理,下次 X 点更新”——沉默会引发更多询问。
  3. 恢复要验证:对用户说“已恢复”之前,先跑核心链路冒烟 + 数据校验;错误地宣布恢复比晚宣布更伤信任。

止血优先:先选手段,再谈根因

手段 适用 代价 何时用
回滚 最近有发布/配置变更 可能丢新功能 第一优先(见 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 通报里为什么不能写“正在排查”?

▸ 答案:“正在排查”没有信息量,干系人无法判断影响与下一步,只会反复询问。有效通报必须包含:已确认的影响、已执行的动作、下一次更新时间。没有进展也要按节奏更新,沉默比坏消息更伤人。

关联