我写过什么都没改变的复盘。我花了一下午写一份认真的文档,我们开了复盘会,所有人点头,然后六周后同一类故障又把我们打挂了一次——因为那份文档里没有一行,变成了系统里的一次改动。那次复盘是我们为了显得负责而做的仪式,而不是让我们变得更安全的机制。这篇文章写的是我现在改怎么做:一套复盘(事故复盘)流程,只瞄准一个结果——会议之后的系统,和会议之前不一样。
为什么大多数复盘都是走过场
当一份复盘满足了社交需要——有人「负责」它,我们「吸取了教训」——却没有碰到那个真正出故障的东西时,它就是走过场。有三种明显的特征。
一段没人再看的复盘时间线。 精确到分钟地还原,十二个时间戳加上一个上了色的故障频道聊天记录,然后再也没被打开过。时间线有价值,是在它回答某个问题的时候:我们过了多久才发现?过了多久才理解?过了多久才修好?一段从聊天记录里抄出来的时间线一个都答不上,它只能证明我们当时很忙。
一句「人为失误」当结论。 「工程师敲错了命令」不是根因,它是分析停下来的地方。它让调查还没开始就结束了,并且悄悄把一个系统问题转换成了一个人的问题。更糟的是,它教会所有人去写那种防御式的、把有趣的细节全部略掉的复盘。
永远不会发生的行动项。 八个行动项,没有一个指定到具体的人,没有一个有日期,其中两个是「补个 runbook」,而下个迭代开始前它们就已经过期了。一个没有名字和日期的行动项,只是一个愿望。
这三者有同一个破绽:复盘的产出是一份文档。如果产出是一份文档,那么文档本身就是交付物,而任何文档都可以被写出来——不管系统到底有没有变好。
我现在用的事故复盘结构
我每一份复盘都按同一个形状写,因为这个形状会强制你诚实。这个顺序,差不多就是我如果是在审问这次事故、而不是在表演给观众看时,会问问题的顺序。
1. 影响——用用户的语言说清谁受了什么影响。 不是「部署失败了」,而是「结账页 14 分钟返回 500;大约 2300 个请求失败,但没有丢单,因为队列把它们兜住了。」把影响写在最前面是一种纪律:它把整份文档锚定在现实里,也逼你给出一个量级估计——而这往往是整份文件里最有用的数字。
2. 发现——我们是怎么知道的,以及花了多久。 最便宜的改进通常就藏在这里。是用户发帖?是告警响了?还是我们三天后从一张图上才看出来?如果发现太慢,那么只修触发因素、不修发现机制,结果只是让下一次事故更安静。把这个缺口画出来,正是小规模服务器监控在做的事——让那个能在一开始十分钟内、而不是四十分钟后才抓住问题的告警存在。
3. 时间线——但要扣住上面那三个问题。 我会写得很短:最后一次正常、第一个坏症状、第一次发现、第一次正确假设、完全恢复。五六行,不要一整个聊天记录。
4. 贡献因素——永远是复数。 不是单一的某个原因。这一点为什么重要,看下一节。
5. 哪些地方做得好。 没人会记得写这一段,而恰恰是它让整个流程在社交上还能活下去。「回滚 90 秒内生效。」「API 是在暗发布后面上线的,所以没人注意到。」把它写下来,否则下一次事故复盘就会变成一场比谁更会推卸责任的游戏。
6. 我们要改什么——带负责人和日期。 少、真实、可追踪。具体怎么做在下面。
blameless 复盘不是没有问责的地方
blameless 在两个方向上都会被误解。它不意味着「没人有责任」;它意味着我们拒绝把分析停在某个人身上。「为什么一个理性、称职的工程师,在当时掌握的信息下,会做出那件后来证明是错的事?」这个问题比「谁搞砸了?」更让人不舒服——因为它的诚实答案通常是「因为我们使用的工具、文档或默认值引导了那个动作。」
但我也见过 blameless 被当成武器,变成「是系统让我干的」,于是永远没有人负责,什么都不会变。那不是 blameless,那是推卸。我牢牢抓住的区分是:指责问的是该惩罚谁,问责问的是该由谁修好什么。 blameless 去掉了前者,并且要求后者。你可以在事故怎么发生这件事上完全无责,同时仍然为那个能阻止它再次发生的行动项个人背书。没有借口,也没有替罪羊。
一个很实用的检验:如果把复盘里所有的名字都抹掉,这份文档还能解释清楚「为什么当时这是一个看起来合理的动作」吗?能,那你描述的是一个系统;不能,你描述的是一个人——那就再往下挖。
触发因素,而不是根因:诚实地做根因分析
我采纳的最有用的一个重新框定,是把触发因素和根因分开。
- 触发因素是造成影响之前刚刚发生的那件事。「数据库连接池被用光了。」「一次烂部署上线了。」「磁盘满了。」
- 根因,从系统的意义上说,是一种缺失——缺少的那道护栏、默认值、检查或知识,正是它让触发因素得以造成影响。
触发因素就是你最想写下来的那个东西。它让人满足、很具体,而且几乎总是症状本身。「连接池被耗尽,是因为我们没给新的 worker 限制并发」——触发因素是连接池,根因是没有任何机制强制这个新 worker 在能上线之前先声明一个并发预算。只有后者能泛化。
这就是根因分析诚实的版本:不是把单一元凶一路追到它最懒的那个原因,而是一组贡献因素,每一个都是一处小小的、可修复的改动。你不需要唯一的一个;你需要那两三个——如果它们当时不存在,就能把这条链断掉的。
把行动项变真:少、有归属、可验证
这里是复盘从一种情绪变成一种机制的地方。每个行动项过四道筛子:
- 少。 三个已经很多了;八个注定一个都不会落地。如果只能做三个,就选那三个——它们缺席时,能把上面那条链断掉的。
- 有归属。 一个指定的人,不是一个团队。「平台组」就是事情不会发生的方式。
- 可追踪。 一张和其他所有事放在同一个 tracker 里的工单,让它诚实地竞争注意力——而不是埋在某个 wiki 页面里的一个圆点。
- 可验证。 你能说出那个本该抓住这次事故的测试——它在修复前失败、在修复后通过。如果这个修复没法这样表达,那它就太模糊,不配叫做真的。
这是我用来教学的那个例子。
差的行动项:
- 改进连接池相关的监控。
好的行动项:
- 负责人:Priya。截止:2026-12-10。完成条件: 当新 worker 声明的并发上限超过连接池大小时,它的部署流水线直接失败;并且当连接池利用率在 2 分钟内超过 85% 时,Prometheus 告警触发。
差的那个没有负责人、没有日期、没有测试、没有可观测的终态。好的那个指名了一个人、一个截止日期、以及一个非真即假的条件——它不可能「基本完成」。注意好的那项其实是两个改动——一个预防、一个发现,而这通常就是正确的切分:让部署失败,以及早点告警。
同样的纪律也适用于部署,我见过相当一部分事故就起源于那里。零停机部署不只是吞吐上的收益,它还是损害控制——因为当一次烂发布回滚起来很便宜时,复盘的时间线(「90 秒恢复」)天生就短。
一份可以直接抄走的复盘模板
抄它。不需要的段落删掉,但别加太多。
# 复盘 — <事故标题> — <YYYY-MM-DD>
- **严重级别:** <SEV-1/2/3> **持续时长:** <开始–结束,自然时间>
- **作者:** <名字> **复盘会日期:** <日期> **参会人:** <名字>
## 影响
谁受了影响、怎么受的、持续多久。用用户的语言。量级估计
(用户数、请求数、收入、订单数)。什么**没有**受影响。
## 发现
我们是怎么知道的(告警 / 用户上报 / 图表)。从第一个坏症状
到发现的时间。从发现到第一个正确假设的时间。
## 时间线(5–8 行,不要聊天记录)
- 最后一次正常:
- 第一个坏症状:
- 发现:
- 正确假设:
- 完全恢复:
## 贡献因素
每一条写:缺的是什么(护栏、默认值、文档、测试),而不是谁。
## 哪些地方做得好
回滚、暗发布、某个人凌晨两点的正确判断。对,写下来。
## 行动项
| 行动 | 负责人 | 截止 | 完成条件(可验证) |
| --- | --- | --- | --- |
| | | | |
## 后续
这些都追踪在哪里。一个确认它们落地的复核日期。
我会对过去的自己说什么
两件事。第一,趁事故还热乎的时候写,允许它很糙——周二的一份粗复盘,胜过周五的一份精修复盘。第二,用 diff 而不是文档来评判这套流程。六周之后,打开那些行动项检查一下:它们落地了吗?后面的一次事故是不是被更早地抓住了?如果答案是「没有」,那这次复盘就是走过场,写得再好也是。
文档从来都不是重点。会议之后的那个系统才是。