核心问题
系统犯过的错,如何变成以后不再重复的知识?
软件工程里的认识论,不只是发现一次错误。
更重要的是:
把一次错误转化成系统和团队的长期学习能力。
如果一个事故解决后,只留下几句聊天记录:
已经修了。
下次注意。
那团队几乎没有真正变强。
真正有价值的复盘要回答:
- 我们原来相信什么?
- 事实证明哪里错了?
- 为什么现有测试、监控、流程没有提前发现?
- 系统要增加什么证据或约束?
- 以后类似问题如何更早暴露?
复盘不是追责
坏复盘会变成:
谁写的?
谁 review 的?
为什么没注意?
这种复盘会让人隐藏信息、避免承担、减少冒险。
好的复盘问:
什么系统条件让这个错误能够发生?
为什么它没有被更早发现?
我们如何让下一次更难发生、更早发现、更容易恢复?
这不是说个人责任不存在,而是说:
工程复盘的目标是改进系统,不是寻找替罪羊。
一次事故暴露的是认知缺口
事故常常说明团队某个判断错了。
例如:
我们以为退款后访问权会立即撤销。
实际上只有 purchase-based grant 被撤销,subscription-based grant 没处理。
这不是单纯代码漏了一行。
它暴露的是模型缺口:
我们没有明确区分不同访问权来源的撤销规则。
所以复盘要追到认知层:
- 哪个概念没定义清楚?
- 哪个契约是隐式的?
- 哪个 source of truth 被误用?
- 哪个缓存过期承诺没说清楚?
- 哪个状态流转没有被约束?
Postmortem 的基本结构
一份轻量但有效的复盘可以包含:
1. Summary
一句话说明发生了什么。
退款流程没有撤销部分课程访问权,导致 37 个已退款账号仍可访问课程。
2. Impact
说明影响范围。
影响时间:2026-06-10 14:20 - 16:05
影响用户:37 个账号
影响资源:12 门课程
用户可见影响:退款后仍可访问课程内容
3. Timeline
按时间记录事实,不要写猜测。
14:20 新退款逻辑上线
14:43 course_access_denied_count 无异常
15:12 客服收到第一例反馈
15:30 工程开始排查
15:48 定位到 subscription grant 未处理
16:05 关闭新逻辑 feature flag
16:40 完成数据修复
4. Root Cause
区分触发条件和根因。
触发条件:新退款逻辑只撤销 purchase grant。
根因:系统没有显式建模不同 access grant source 的撤销契约,也没有测试覆盖多来源访问权。
5. Detection Gap
为什么没有更早发现?
缺少 refund -> access revocation ratio 指标。
缺少多 grant source 的集成测试。
客服后台没有显示访问权来源和撤销状态。
6. Action Items
行动项必须具体、可验证、有 owner。
1. 增加 refund revocation integration test。
2. 为 CourseAccessGrant 增加 source-specific revocation policy。
3. 增加 refund_completed_count / access_grant_revoked_count 指标。
4. 客服后台展示 grant source、validUntil、revokedAt。
Action Item 要改变系统
坏行动项:
以后更仔细。
加强 review。
注意测试。
这些不可验证,也不会改变系统。
好行动项:
新增测试:退款后撤销 purchase、subscription、organization_assignment 三类 grant。
新增告警:refund_completed_count 正常但 access_grant_revoked_count 低于预期。
新增约束:所有 grant revocation 必须通过 revokeCourseAccessGrant。
好的行动项会改变反馈回路、契约、类型、测试、监控或工具。
原则:
如果行动项不能被验证,它就不是行动项。
知识沉淀到哪里
复盘后的知识不能只留在会议纪要里。
不同知识应该沉淀到不同地方:
| 知识类型 | 应该沉淀到 |
|---|---|
| 业务规则 | 领域文档、规格、测试 |
| 架构取舍 | ADR |
| 操作流程 | Runbook |
| 常见排查路径 | Debugging guide |
| 系统约束 | 类型、lint、schema、状态机 |
| 监控信号 | Dashboard、alert |
| 历史事故 | Incident log |
复盘的目标不是写一篇漂亮文档,而是:
把学到的东西放到未来最可能被用到的地方。
ADR:记录为什么这样设计
ADR 是 Architecture Decision Record。
它适合记录重要架构决策:
Decision: 使用 Postgres outbox 处理支付后的异步副作用。
Context:
当前订单量较低,但需要确保 purchase 和 outbox event 在同一事务中写入。
Alternatives:
Kafka, Redis Stream, direct async call.
Decision:
选择 Postgres outbox。
Consequences:
简化运维,保证事务一致性;高吞吐和多消费者场景下未来可能迁移。
ADR 的价值是让未来的人知道:
当时为什么这样选,而不是只看到现在的代码。
Runbook:记录如何处理
Runbook 适合记录操作流程。
例如:
Course access grant missing after payment
1. 用 paymentId 查询 payment record。
2. 检查 purchase 是否存在。
3. 检查 course_access_grant 是否存在。
4. 如果 payment succeeded 但 purchase missing,运行 reconciliation。
5. 如果 grant missing,检查 outbox event 和 worker logs。
6. 不要手动直接写 grant,除非 incident commander 批准。
Runbook 的价值是:
事故中减少临场发明。
Blameless 不等于 Toothless
无责复盘不是没有要求。
它不是:
没人有责任,所以什么都不用改。
它是:
不把人当根因,但必须改变系统。
如果同类事故反复发生,而 action items 没有完成,那就是管理和工程系统的问题。
复盘要温和对人,严格对系统。
复盘检查清单
每次事故后问:
- 事实时间线是什么?
- 用户或业务受到了什么影响?
- 触发条件是什么?
- 根因是什么?
- 哪个测试本应发现它?
- 哪个指标本应发现它?
- 哪个契约没有写清楚?
- 哪个 source of truth 被误解?
- 是否有数据需要修复?
- 哪些行动项会让同类问题更难发生?
- 这些行动项是否有 owner 和验证方式?
- 学到的规则应该沉淀到文档、测试、ADR、runbook 还是工具?
小结
- 复盘的目标是把错误转化成长期知识。
- 好复盘改进系统,不寻找替罪羊。
- 事故暴露的是认知缺口,不只是代码缺陷。
- Action item 必须具体、可验证、有 owner。
- 知识要沉淀到未来最可能被使用的地方。
- ADR 记录为什么这样设计,Runbook 记录出事时怎么处理。
- 无责复盘不是没有要求,而是温和对人、严格对系统。