三层归因方法论(表象 / 第二根因 / 第一根因 / 超前 2 年)

一句话速记

三层归因让你不只解决表面的 bug,还要追问背后的系统缺陷:表象(用户看到了什么错)→ 第二根因(代码/配置哪里出了 bug)→ 第一根因(为什么这个 bug 能合入主干、为什么监控没发现、为什么测试没覆盖)→ 超前 2 年(整个系统设计上有什么结构性缺陷,2 年后还会不会出同类问题)。

通俗解释(5 分钟版)

一次线上 OOM 的三层归因:

表象(T0):
  用户:页面 500 了
  监控:pod XXX OOMKilled,重启了

第二根因(合入/修复后的一周内):
  代码里 ThreadLocal 没 remove → 线程池复用后 ThreadLocal 值累积 → OOM
  → 修了,加了 remove

第一根因(你该继续追问的):
  Q: 为什么这个 ThreadLocal 没 remove 的代码能通过 code review?
  Q: 为什么 Old 区到了 90% 监控没报警?
  Q: 为什么单元测试没覆盖这个泄漏场景?
  Q: 代码规范里有没有写"ThreadLocal 必须在 finally 里 remove"?
  → 补了监控告警、加了代码规范检查、加了集成测试

超前 2 年的系统级根因:
  团队里有 5 个服务都在用 ThreadLocal,没有一个统一的 ThreadLocal 工具类封装
  → 每个服务自己管理,泄漏风险分散在各处
  → 写了一个 AutoCleanupThreadLocal 包装类,自动在 finally 里清理
  → 以后所有 ThreadLocal 都用这个包装类,这个问题永不再犯

关键细节

三层归因的判断标准

层级          问的问题                     判断标准                  谁来做
────────────────────────────────────────────────────────────────────────
表象         发生了什么?                 用户/监控能看到的现象         值班/oncall
             影响多大?

第二根因      代码哪里写错了?             能定位到代码行/配置项        开发
             怎么修?

第一根因      为什么这个错误能上线?        能回答"哪个流程缺失了"      技术 Leader
             哪个流程/规范/监控缺失了?    并且补上了流程/规范/监控

超前 2 年     这个系统的结构性缺陷是什么?  能抽象出一个通用方案/       架构师/技术
             2 年后还会不会出同类问题?    框架/规范,跨服务复用        负责人
                                         让同类问题永不再犯

每层的具体追问清单

第二根因追问(修 bug 不是终点)

不只是修代码,还要问:
- 这个 bug 是什么时候引入的?(git blame 看提交历史)
- 引入了多久才发现?(1 天 vs 1 个月 → 区别很大)
- 引入的 PR 有 review 吗?review 为什么没发现?
- 这个 bug 有没有在其他服务/模块里存在?(同类模式排查)

第一根因追问(补流程)

流程缺失排查表:
□ 代码规范:这个 bug 是否违反现有规范?如果规范里没有这条,要不要加上?
□ Code Review:Review Checklist 要不要加一条检查项?
□ 单元测试:这类场景是否应该在单元测试里覆盖?能不能加一个?
□ 集成测试:是否应该有一个集成测试来验证端到端行为?
□ 监控告警:为什么监控没发现?需要补什么指标/告警?
□ 灰度发布:如果灰度发布时观察久一点,是否能发现?灰度策略要不要调整?
□ 上线检查清单:是否有上线前的检查清单?要不要加一条?

超前 2 年追问(架构层面)

结构性缺陷识别:
- 这个 bug 是孤立事件还是模式问题?(同类 bug 是否在其他服务出现过?)
- 当前系统设计是否让这类问题容易发生?怎么设计让它不可能发生?
  → 例:ThreadLocal 手动管理容易泄漏 → 统一封装自动清理
  → 例:数据库连接手动管理容易泄漏 → 连接池 + try-with-resources 规范
  → 例:Redis 过期时间手动设容易忘 → 框架层强制默认过期时间
- 能否通过框架/中间件/平台能力一劳永逸?
  → 例:排查了 10 次 OOM 后发现 6 次都是 ThreadLocal → 直接写进框架层

实战示例

案例:订单服务 P99 从 200ms 飙到 5s

表象:
  用户投诉下单慢,P99 从 200ms → 5s
  监控显示订单服务 P99 突然飙高

第二根因(排查到代码行):
  arthas trace 发现 OrderService.validateCoupon() 耗时 4.5s
  → validateCoupon() 里调了优惠券服务的一个新接口
  → 优惠券服务的营销同事上线了一个新的优惠规则匹配逻辑
  → 那个逻辑里有个正则回溯(catastrophic backtracking)
  → 修了正则表达式

第一根因(补流程):
  Q: 为什么优惠券服务的新逻辑上线后没有压测?
  → 补了规定:所有新接口上线前必须压测,P99 < 100ms 才能发
  Q: 为什么订单服务调优惠券没有超时熔断?
  → 加了 Sentinel 限流 + 熔断,优惠券服务超时 > 500ms 直接降级
  Q: 为什么 P99 飙到 5s 了才有人发现?
  → 补了 P99 告警:P99 > 500ms 就报警

超前 2 年:
  整个公司所有对外的 HTTP 调用都没有统一的超时/熔断/降级机制
  → 写了一个统一 RPC 框架封装(基于 Sentinel + 统一配置)
  → 所有服务的 RPC 调用都强制有超时 + 熔断,防患于未然

延伸追问

  • Q:三层归因和 5 Whys 有什么区别? → 5 Whys 是”连续问 5 个为什么”,方向是线性的。三层归因是结构化的:代码错了(第二根因)→ 流程漏了(第一根因)→ 架构有缺陷(超前 2 年)。三层归因让你不会只停在修代码,而是系统性地提升。
  • Q:小 bug 也需要三层归因吗?会不会太形式主义? → 不需要每个 bug 都做。判断标准:① 影响用户数 > N(你们自己定);② 同样类型的 bug 出现过至少 2 次;③ 修复花了超过 1 小时。满足任一条件就值得做三层归因。
  • Q:超前 2 年的根因实际怎么落地? → 关键是不要在故障复盘会里提一嘴就忘。做法:把”超前 2 年”的改进点放进技术 Roadmap(排期、定 Owner、定验收标准),作为下个迭代的技术改进项,而不是永远在”有空的时候做”。

我的记法

  • 三层 + 超前:表象(what)→ 第二根因(code bug)→ 第一根因(流程缺失)→ 超前 2 年(架构缺陷)
  • 第二根因 ≠ 排障终点,第一根因才是”为什么能上线”的答案
  • 超前 2 年的关键问题是:「2 年后,还会不会有其他团队踩同样的坑?」
  • 一句话:「修 bug 只要 10 分钟,补流程要 1 周,改架构要 1 个月——但后两件事才是你真正防止下一次故障的保险」

状态

  • 已背速记
  • 能讲通俗版
  • 能答追问