路线图

11 故障复盘(Blameless Postmortem)

星辉 2026-07-02 阅读 6 min 1,251 字 路线图
11 故障复盘(Blameless Postmortem) 封面

概述#

故障响应的终点不是"服务恢复了",而是"我们从这次事故里学到了什么"。Blameless Postmortem(无指责复盘)是 SRE 体系中最具组织价值的实践——它把每次事故从一个"需要甩锅的坏消息"变成"推动系统改进的杠杆"。

一个成熟团队的标志不是"不出事故",而是"每次事故后系统都变得更好"。这只能通过规范化的复盘实现——没有复盘的事故是"白白付出的代价",有复盘的事故是"昂贵的学费"。

本章从文化前置条件到模板到 Action Item 闭环,配合来自生产环境的真实案例。

一、Blameless 文化:复盘的前提#

如果组织文化里还有"谁的锅"这个问题,那后面的模板和流程都是花架子——工程师不敢说真话,复盘就是表面文章。

Blameless ≠ 不追责#

Blameless 是把"人为什么会犯错"当成系统问题来分析:

问题有指责的追问Blameless 追问
改错了生产配置"谁改的?""为什么这个配置是人手改的而不是 GitOps 管的?"
发版引入 bug"谁写的代码?""为什么没有 peer review?为什么 CI 没拦截?"
误删数据库记录"谁删的?""为什么没有 dry-run 机制?为什么 prod 和 dev 操作入口是同一个?"
值班响应慢"谁没看到告警?""为什么告警没有升级机制?为什么手机静音了?"

结论永远指向系统改进,而不是"以后小心点"。"以后小心点"是无效的——人在疲劳、压力下必然会犯错,系统设计必须假设人会犯错。

Blameless 的心理学基础#

人在被指责时会启动防御机制:

  • 隐藏细节("这个错误看起来不严重,就不说了")
  • 推卸责任("这是上游的问题")
  • 编造理由("我当时是想...")

这些防御行为让复盘失去真实信息,根因分析变成"猜"。Blameless 文化让工程师敢于说真话——"我改了这个配置,以为没影响,结果...",这种诚实是找到根因的前提。

实践做法#

  1. 事故响应群禁止追究"谁做的"。响应阶段聚焦止损,复盘阶段才分析原因。
  2. 复盘文档默认不写人名。如果必须写(如"张三执行了 rollback"),用角色而非人名:"IC 执行了 rollback"。
  3. 复盘会议邀请无关人旁观。旁观者的存在让"追责"行为变得不合适。
  4. 领导层明确表态支持 blameless。CTO 在全员会议上说"我们不追责,我们改系统"比任何制度都有效。
  5. 复盘文档对所有员工公开。公开本身就是"这是学习材料不是惩罚记录"的信号。

Blameless 的边界#

Blameless 不等于"什么都能原谅"。以下行为不属于 blameless 保护范围:

  • 恶意破坏:故意删数据、注入后门
  • 重复犯错:同样的错误第三次,是态度问题
  • 违反明确规则:如明令禁止 prod 直接操作却违反
  • 隐瞒事故:出了事不报,私下处理

这些需要单独处理,不在 postmortem 范围内。

二、Postmortem 模板#

以下是完整的复盘文档模板(来自故障响应实战文章,经过多次事故验证):

text
# Postmortem: <标题>

## 摘要
<三句话讲清楚发生了什么、影响多大、怎么恢复的>

## 事故元数据
- 发现时间: 2026-07-01 15:02
- 恢复时间: 2026-07-01 15:58
- 持续时间: 56 分钟
- Severity: SEV-2
- IC: <角色>
- 影响: 下单成功率降低至 67%,影响约 12 万笔订单
- 检测方式: Prometheus 告警 OrderAPI_p99_high

## 时间轴(UTC+8)
- 14:50 订单 API v2.4.1 发布到 prod(灰度 50%)
- 14:58 监控显示 order-api pod DB 查询耗时上升
- 15:02 Alertmanager 告警 order-api p99 >1s,值班 ack
- 15:05 值班判断 SEV-2 拉事故群,任命 IC
- 15:08 Ops Lead 查 DB slow query log,发现新 SQL 未走索引
- 15:15 IC 决策: rollback deployment
- 15:22 rollback 完成
- 15:28 p99 回落到 200ms
- 15:45 业务指标全面恢复
- 15:58 IC 宣布 resolve

## 影响分析
- 用户影响: 约 5 万用户下单失败或重试
- 业务影响: 订单量 1h 内减少 30%
- 财务影响: 初估损失 X 万
- SLA 影响: Order API 月度 SLO 消耗 0.04 个错误预算

## 根因分析(RCA)
1. 直接原因: v2.4.1 引入一条新 SQL 未走索引
2. 触发条件: 灰度 50% 后写入压力让 SQL 每秒执行 1000+ 次
3. 传播原因: 数据库慢查询导致连接池耗尽,无慢查询的请求也被阻塞
4. 检测滞后: 告警规则基于 p99,从异常到告警滞后约 4 分钟

## 贡献因素(Contributing Factors)
1. 缺少 SQL 审查流程: PR 里新增 SQL 没有 DBA 审查
2. 缺少 staging 真实流量压测: staging 写入 QPS 只有 prod 的 1%
3. 告警滞后: p99 采样窗口 2m,告警 for 2m,理论下限延迟 4m
4. 回滚流程文档不够清晰: IC 花了 3 分钟确认 rollback 命令

## 做得好的地方(What went well)
- IC / Ops Lead 角色任命清晰
- 15 分钟内决策 rollback
- 没有盲目查代码,先止损

## 做得不好的地方(What went wrong)
- PR 未拦截索引问题
- 灰度策略未在小流量时观察足够长
- 业务侧通知滞后 10 分钟

## 运气成分(Where we got lucky)
- 事故发生在非高峰(周三下午),高峰可能严重 10 倍
- 上一次发布的 image 还在 registry,rollback 快

## Action Items
| # | 描述 | 类型 | 优先级 | Owner | Deadline |
|---|---|---|---|---|---|
| 1 | 为 order.order_status 建索引 | Fix | P0 | DB Team | 2026-07-02 |
| 2 | PR 流程加 SQL lint 检查 | Prevent | P1 | Platform | 2026-07-15 |
| 3 | staging 复制 prod 1/10 真实流量 | Detect | P2 | SRE | 2026-08-01 |
| 4 | p99 告警窗口调到 1m | Mitigate | P1 | SRE | 2026-07-10 |
| 5 | 更新 rollback runbook | Process | P1 | SRE | 2026-07-12 |
| 6 | 编写业务侧通知 playbook | Process | P2 | CL | 2026-07-25 |

## 学到的东西(Lessons Learned)
1. SQL 索引问题在小流量灰度时很难暴露,需要真实写压力
2. 事故响应最耗时的部分不是诊断,是"确认下一步动作是否安全"
3. 回滚路径上的任何摩擦点都会放大事故时长

模板要点说明#

  1. 不写人名,用角色(IC / Ops Lead / CL)
  2. Action Items 五分类
    • Fix:修根因(索引错误 → 加索引)
    • Prevent:防再发(加 SQL lint → 同类错误不再进入 prod)
    • Detect:早发现(staging 压测 → 上线前暴露)
    • Mitigate:减影响(告警窗口缩短 → 更快发现)
    • Process:流程改进(rollback runbook 更新)
  3. 必须有 Deadline 和 Owner——没有这两个字段的 Action Item 不算
  4. "运气成分"一栏非常重要——它提醒你"这次没死纯属运气",推动更严谨的改进
  5. "学到的东西"是给组织读的,比 action items 更长远
  6. 每类 Action Item 至少一条——只有 Fix 没有 Prevent,同类问题会再发生

三、真实案例分析#

案例一:CNI 自锁故障(摘自 A 旗舰故障复盘)#

表象:集群"扩不动"——新节点全 NotReady,日志报 MissingIAMPermissions

第一直觉(错的):权限被删了,去加权限。

经过两层证伪后的真因:addon 更新的中间态让 IRSA 临时失效 → fallback 到没配兜底 policy 的 node role → CNI 初始化失败 → 节点 NotReady → addon rollout 完不成 → 形成自锁死循环(新节点持续 fallback 失败 → addon 永远停留在 UPDATING 中间态)。

排查过程

text
第一次证伪:子网 IP 耗尽?
  → 实测还剩 3914 个 IP → 不是

第二次证伪:有人删了 CNI policy?
  → CloudTrail 查 0 条 DetachRolePolicy 事件 → 不是

顺藤摸瓜:老节点凭什么正常?
  → 查 IRSA env,发现 CNI 平时走 IRSA role(权限正常)
  → 那 node role 缺 CNI policy 平时毫无影响
  → 为什么新节点 fallback 到 node role?
  → CloudTrail 查到 13:37 有人 UpdateAddon(vpc-cni)
  → addon 进入 UPDATING 中间态,IRSA 短暂失效
  → SDK 凭据链 fallback 到没权限的 node role
  → 自锁循环形成

双层归因的价值

  • 触发因素(什么坏了):addon 更新中间态 + IRSA 短暂失效(偶发、防不胜防)
  • 结构性缺陷(为什么没兜住):node role 没配 CNI policy 兜底——所有 eksctl 纯 IRSA 模式建的 nodegroup 都有这个隐患

真正的 Action Item:给所有 EKS node role 普配 CNI policy 兜底,一次性拆弹。这不是一个集群的问题——所有 eksctl 纯 IRSA 模式建的 nodegroup 都是同款定时炸弹。

案例二:Passport 全挂(潜伏炸弹)#

表象:passport 服务突然全挂(TLS 无响应),但其他服务正常。

对照组破案:passport 和 goalfymax 的区别——passport 直连 NLB,goalfymax 走 CloudFront。问题在"passport → NLB"链路。

真因:system-pool 节点带 taint system-pool=true:NoSchedule,istio gateway 绑 nodeSelector=system-pool 但没配对应 toleration。平时 NoSchedule 不驱逐已运行的 Pod,所以一直正常。节点轮换后新 Pod 重新调度时被 taint 拦住,永久 Pending。

炸弹链

text
system-pool 节点带 taint
istio gateway 绑 nodeSelector 但没配 toleration
平时 NoSchedule 不驱逐已运行 Pod → 一直正常(炸弹被掩盖)
某天节点轮换 → Pod 要重新调度
被 taint 拦住 → 永久 Pending
Service endpoint 空 → NLB 无健康 target → TLS 无响应 → 全挂

元教训:之前修 cert-manager 时已经发现 system-pool 缺 toleration,但只修了 cert-manager 没管 istio gateway。同一个缺陷、同一个集群、同一类组件,修了一个漏了另一个。

教训写进流程:发现一个组件缺 toleration / 配置错误,必须排查所有同 nodeSelector / 同配置的组件。一个缺陷往往是一类

案例三:发版超时(双重根因)#

表象:file 服务每次发版流水线 wait 900s 超时。

第一直觉(错的):镜像或部署脚本坏了。

对照组破案:US-prod 同镜像发版完全正常。同镜像同脚本,US 正常 CN 超时 → 问题在 CN 集群的部署环境。

两个独立根因叠加

text
根因① 内存型 HPA 把副本虚扩到 13
  - file 每 pod CPU ~0%,但常驻内存 1.4-2.5Gi
  - HPA 同时挂 cpu(70%)+memory(80%)
  - 内存高被 HPA 读成"要扩容",副本 min4 一路顶到 13
  - 加副本根本压不下单 pod 内存(与业务量脱钩)

根因② maxSurge 撞满专用节点池
  - 13 副本塞满 3 个 c7/c6.2xlarge 节点(84-89% 使用率)
  - surge 的第 14 个 pod 没空位 → Pending
  - 该节点池无自动扩容 → 永远 Pending → wait 900s 超时

为什么必须看到"两个"根因:如果只修 HPA,maxSurge 撞节点池的隐患还在;如果只改 maxSurge,内存 HPA 还会在别的时候把副本顶上去。双根因故障的特征就是"单修一个不彻底"

可迁移结论:常驻内存型服务(Node 堆、文件代理)禁用 memory-Utilization HPA——加副本不降单 pod 内存,只会越扩越多。

四、RCA 方法论:不被表象骗#

来自三个真实案例共同提炼的排查框架:

text
症状(表象)
  │ ❌ 别在这层动手(加权限/查脚本/查证书)
  ▼ 用对照组排除错误方向(老节点 vs 新节点 / US vs CN)
中间层(调度/容量/fallback 路径)
  │ 用证据钉死每一层(CloudTrail / kubectl describe / 实测)
根因(结构性缺陷)
  │ 双层归因:不只"什么坏了",更问"为什么没兜住/为什么是一类"
根治 + 排查所有同款 + 防回退

三个规律#

规律一 · 表象都在"骗你往最近的那层看"

"缺权限"让你想加权限,"超时"让你想查脚本,"TLS 挂"让你想查证书——全错。破解靠对照组:案例一靠"老节点正常 vs 新节点挂"、案例二靠"US 正常 vs CN 超时"、案例三靠"goalfymax 正常 vs passport 挂"。对照组能瞬间排除一大片错误方向,是 RCA 最高效的工具。

规律二 · 真因常在兜底/fallback 路径上

案例一是 IRSA 失效 fallback 到没权限的 node role;案例三是节点轮换让 pod 走"重新调度"路径撞上缺失的 toleration。这类炸弹的共性:主路径正常时完全无感,能潜伏很久,边缘态才引爆——所以"平时好好的"不等于"没问题"。

规律三 · "修好它"远不够,要问"为什么没自愈 / 为什么是一类"

案例一真正该修的是"没有 CNI policy 兜底",案例二是"常驻内存服务不该用内存 HPA",案例三是"发现一个缺陷要排查所有同款"。触发因素千变万化,结构性缺陷可以根治——双层归因 + 一个缺陷当一类排查,才是从"灭火"升级到"拆弹"。

双层归因的具体做法#

text
第一层:触发因素(什么直接导致了故障)
  → 修复这一层只能防止"这次故障"重演

第二层:结构性缺陷(为什么系统没兜住这个触发因素)
  → 修复这一层能防止"一类故障"重演

举例:

  • 第一层:addon 更新让 IRSA 失效 → 修复:等 addon 更新完再扩节点
  • 第二层:node role 没有 CNI policy 兜底 → 修复:给所有 node role 配 CNI policy

第一层的修复是"针对这次",第二层的修复是"针对一类"。第二层才是 postmortem 的核心价值。

五、Action Items 跟踪机制#

90% 的 postmortem 死在 action items:写得漂亮但没人跟进。

跟踪五步法#

  1. 所有 action items 进 Jira/工单系统的 incident backlog
  2. IMOC 每周 review 未关闭的 items
  3. 超期 items 报给 team lead
  4. 月度事故回顾会回看所有 open items
  5. action items 未关闭的团队不允许做新的演进项目(软约束)

最有效的一点:把 P0/P1 action items 的 deadline 进 OKR。上线这条后,action item 完成率从 ~40% 涨到 ~85%。

Action Item 的分类要求#

每个 postmortem 的 Action Items 必须五类齐全(至少各一条):

类型作用缺失的后果
Fix修根因同样故障立即重演
Prevent防再发同类故障在其他地方出现
Detect早发现下次故障检测滞后
Mitigate减影响下次故障影响更大
Process流程改进流程缺陷继续存在

只有 Fix 没有 Prevent,同类问题会在其他服务重演;只有 Fix 没有 Detect,下次故障还是滞后发现。五类齐全才是完整的改进计划。

Action Item 的 SMART 原则#

每个 Action Item 必须:

  • Specific:具体("优化 SQL"不具体,"为 order_status 加索引"具体)
  • Measurable:可衡量("改进监控"不可衡量,"p99 告警窗口从 2m 调到 1m"可衡量)
  • Assignable:有 owner("团队负责"等于没人负责,必须具体到人)
  • Realistic:可达成("保证永不宕机"不现实)
  • Time-bound:有 deadline("尽快完成"等于没完成时间)

六、组织知识沉淀#

单个 postmortem 的价值一次性,多个 postmortem 才能变成组织资产。

事故数据库#

所有 postmortem 放在同一个仓库,打标签分类:

text
- scope: database / network / deployment / application / external
- root_cause: config-error / code-bug / capacity / dependency / human-error
- severity: SEV-1 / SEV-2 / SEV-3
- service: order-api / payment / passport / ...
- action_items_status: open / in-progress / closed

季度回顾:事故次数、平均 MTTR、根因分布、Top 3 教训、关键 action items 完成情况。持续跟踪后 MTTR 从 52 分钟降到 28 分钟。

事故模式识别#

连续三次 postmortem 涉及 DNS 解析失败?说明有 DNS 架构问题,不是偶发。把这类模式抽出来做专题整改,比单次修复有价值 10 倍。

模式识别的方法:

  • 按根因分类聚合(同根因的故障统计)
  • 按服务聚合(哪个服务最频繁出事)
  • 按时间聚合(某段时间事故密集说明有外部因素)
  • 按故障类型聚合(配置错误类、容量类、依赖类)

入职必读#

把经典事故 postmortem 列成新人必读。新同事读完 10 个真实故事,对系统脆弱性、blameless 文化、"保持冷静"的重要性都有直观认识。比任何 onboarding 文档都有效。

季度事故回顾会#

全团队级别的 review,不是单次复盘,是"这 3 个月我们在事故里学到了什么"。一般 1 小时,内容:

  • 数据总结:事故次数、平均 MTTR、按类别分布
  • Top 3 教训
  • 关键 action items 完成情况
  • 下季度优先事项
  • 表彰"最佳 postmortem"(鼓励深度分析)

七、复盘会议怎么开#

时机#

  • 事故发生后 48-72 小时内
  • 不要太早(响应人员还在疲惫中)
  • 不要太晚(细节遗忘)

参与人#

  • IC 主持
  • Ops Lead、CL、IMOC 参加
  • 受影响服务 owner 参加
  • 无关人可旁观(传播 blameless 文化)
  • 领导层不参加(避免变成汇报会)

议程#

text
1. IC 念时间线(5 分钟)—— 对齐事实
2. RCA 讨论(15 分钟)—— 找根因
3. Action Items 制定(15 分钟)—— 怎么改
4. What went well / wrong(10 分钟)—— 流程反思
5. Lessons Learned(5 分钟)—— 沉淀知识

会议规则#

  • 不追责——任何"谁"的问题被制止
  • 聚焦系统——所有"为什么"指向系统设计
  • 行动导向——每个问题都要有对应的 Action Item
  • 不纠结细节——细节会后再讨论,会议聚焦决策

实战要点#

  1. 48 小时内写 Postmortem。超过 72 小时细节开始遗忘。
  2. Blameless 是文化不是技巧。如果领导在复盘会上追责,再好的模板也没用。
  3. Action Items 必须分类(Fix/Prevent/Detect/Mitigate/Process),每类至少一条。
  4. "运气"一栏不能省——它能推动真正严谨的改进。"这次没死是运气"是推动改进的最强动力。
  5. 一个缺陷当一类排查——发现一个问题,search 所有同款。
  6. Action Items 进 OKR——这是保证完成率的最有效手段。
  7. 复盘文档对全员公开——这是"学习材料不是惩罚记录"的信号。
  8. 季度回顾会必须开——单次复盘价值有限,跨事故的模式识别才有组织价值。
  9. 经典 postmortem 进新人必读——比任何 onboarding 文档都有效。
  10. 复盘本身要复盘——每次复盘会后 review"这次复盘哪里做得好、哪里可以改进"。

小结#

无指责复盘是 SRE 的"学习引擎"。核心公式:事故 = 固定模板(时间线+根因+Action Items)+ 双层归因(触发因素+结构性缺陷)+ 闭环跟踪(Deadline+Owner+OKR)

最好的团队不是不出事故的团队,而是每次事故后系统都变得更好的团队。判断一个团队的复盘文化是否成熟,看三件事:每起 SEV-2+ 事故有 postmortem、Action Items 完成率 > 80%、新人必读里有 10 份历史 postmortem。三件事都做到是成熟团队;只做到一两件是"在建立中";一件都没做,事故就是"白白付出的代价"。

RCA 的最高境界不是"找到根因",而是"找到一类根因"。一次复盘能拆掉一类定时炸弹,比修十个单点 bug 价值大得多。这是从"灭火"升级到"拆弹"的标志。