第十三章 线上故障

一

那天晚上的事情,王志远后来回忆了很多次。

每次回忆,他都觉得那是他入职以来最漫长的一个夜晚,也是他真正"长大"的一个夜晚。

二

事情发生在周三晚上九点。

王志远刚回到出租屋,正准备洗澡,手机突然疯狂震动。钉钉群里一片红色告警:

🚨 P0 告警:核心交易系统异常,订单创建成功率降至 30%

🚨 P0 告警:支付回调处理延迟飙升至 60 秒(正常 < 1 秒)

🚨 P0 告警:Redis 缓存命中率降至 5%(正常 > 95%)

P0 是最高级别的告警——意味着核心业务受到严重影响,需要全员响应。

三分钟后,赵北辰在群里发了一条消息:

全组到公司,现在。

王志远穿上衣服就往外跑。

三

十点钟,会议室里坐满了人。

赵北辰、苏薇、陈锐、马德华、王志远,加上运维的老张和 DBA 小刘,七个人围着一张桌子,每个人面前一台笔记本电脑。

白板上,赵北辰写了三行字:

现象:
1. 订单创建成功率 30%(正常 99.9%)
2. 支付回调延迟 60s(正常 < 1s)
3. Redis 缓存命中率 5%(正常 > 95%)

"先确认一下,最近有什么变更?"赵北辰问。

运维老张翻了翻部署记录:"今天下午四点有一次发版,涉及订单模块和缓存模块。"

"谁的代码?"

老张看了一眼记录:"订单模块是苏薇的,缓存模块是马德华的。"

苏薇立刻打开了自己的 PR:"我改的是订单列表的查询优化,加了一个索引,跟订单创建没关系。"

所有人看向马德华。

马德华的表情有点不自然:"我改的是缓存预热的逻辑。之前缓存预热是手动触发的,我改成了自动预热——系统启动时自动加载热点数据到 Redis。"

"代码谁 Review 的?"赵北辰问。

马德华顿了一下:"没有 Review。我觉得改动不大,就直接合入了。"

会议室里安静了两秒。

赵北辰没有发火,但语气变得很严肃:"先排查问题,追责的事后面再说。德华,把你的代码打开,大家一起看。"

四

马德华打开了他的代码。

王志远看了一眼,心里咯噔了一下——这段代码的风格他太熟悉了。不是马德华平时的风格,而是一种……过于"完美"的风格。变量命名规范,注释详尽,逻辑结构清晰,甚至还用了几个设计模式。

这不像是马德华写的。马德华的代码风格一向是"能跑就行",变量名经常是 temp、data、result。

苏薇显然也注意到了。她皱了一下眉头,但没说什么。

赵北辰开始逐行分析代码。缓存预热的逻辑看起来没问题——系统启动时从数据库查询热点数据,批量写入 Redis。

"这段代码逻辑上没有明显问题。"赵北辰说。"但问题可能不在预热本身,而在预热触发的时机和方式。"

他看了一眼监控:"Redis 缓存命中率是从下午四点开始下降的,跟发版时间吻合。但不是突然降到 5% 的,而是逐渐下降的——四点是 90%,五点是 70%,六点是 50%,到九点降到了 5%。"

"逐渐下降?"苏薇说。"如果是预热逻辑有问题,应该是发版后立刻出问题,不会逐渐下降。"

"除非……"赵北辰想了想,"预热逻辑本身没问题,但它引入了一个副作用,导致正常的缓存在逐渐失效。"

他转向马德华:"你的代码里有没有动过缓存的过期策略?"

马德华翻了翻代码:"没有……等等。"

他指着一段代码:"这里。预热的时候,我给每个缓存 key 设了一个过期时间——3600 秒,一个小时。"

"之前呢?"

"之前这些 key 是没有过期时间的,永不过期。"

会议室里又安静了。

赵北辰闭上眼睛想了几秒钟,然后说:"找到了。"

五

他在白板上画了一条时间线:

16:00 发版,缓存预热执行
      → 所有热点数据写入 Redis,过期时间 = 3600 秒(1 小时)
      → 覆盖了原来永不过期的缓存

17:00 第一批缓存开始过期
      → 缓存命中率开始下降
      → 请求打到数据库

18:00-21:00 缓存持续过期
      → 命中率持续下降
      → 数据库压力持续上升
      → 订单创建变慢,支付回调排队

21:00 大部分缓存已过期
      → 命中率降至 5%
      → 数据库扛不住,订单创建成功率降至 30%

"问题的根本原因是:预热逻辑给缓存 key 加了一个小时的过期时间,覆盖了原来永不过期的设置。一个小时后缓存开始批量过期,所有请求都打到了数据库,数据库扛不住了。"

"这就是缓存雪崩。"苏薇说。"大量缓存在同一时间过期,请求全部穿透到数据库。"

"对。"赵北辰说。"而且更糟糕的是,所有 key 的过期时间都是 3600 秒,意味着它们几乎在同一时刻过期。如果过期时间加一个随机偏移量,至少不会同时过期。"

"先修复。"他做了决定。"两步:第一,把那些过期的缓存重新加载,去掉过期时间。第二,回滚马德华的代码。"

运维老张和 DBA 小刘开始执行。苏薇写了一个紧急脚本,批量刷新缓存。

二十分钟后,Redis 缓存命中率开始回升。

六

十一点,缓存命中率恢复到 90% 以上,订单创建成功率恢复到 99%。

但赵北辰没有让大家走。

"问题修复了,但根本原因还没有完全搞清楚。"他看着马德华。"德华,这段代码是你自己写的吗?"

马德华的脸色变了。

会议室里安静了几秒。

"我……用了 AI 工具生成的。"马德华终于说了。"我把需求描述给了 AI,它生成了整段代码。我看了一下,逻辑没问题,就直接用了。"

"你审查过每一行代码吗?"

"……大致看了一下。"

"'大致看了一下'是什么意思?"赵北辰的语气平静但锐利。"你有没有注意到 AI 给每个缓存 key 加了过期时间?"

马德华低下了头:"没注意。AI 生成的代码里有注释,说'设置合理的过期时间以防止缓存数据过期不一致'。我觉得有道理,就没改。"

"AI 说的'有道理',不代表在你的场景下有道理。"赵北辰说。"AI 不知道你们系统的缓存策略是永不过期的,它按照通用的最佳实践给你加了过期时间。这个'最佳实践'在你的场景下就是一个 bug。"

马德华抬起头,说了一句让所有人都愣住的话:

"AI 写的代码怎么能怪我?"

七

会议室里的气氛瞬间降到了冰点。

赵北辰看着马德华,沉默了大概五秒钟。然后他说了一句话,声音不大,但每个字都像钉子一样钉在空气里:

"代码署你的名,责任就是你的。AI 是工具,不是替罪羊。"

他站起来,走到白板前:

"你用 AI 生成代码,就像你用计算器算数。如果计算器给了你一个错误的结果,你拿着这个结果去做了一个错误的决策,你能说'计算器算错了,不怪我'吗?"

"你是工程师。工程师的职责是对交付的成果负责。不管这个成果是你手写的、AI 生成的、还是从 Stack Overflow 上抄的——只要是你提交的,就是你的责任。"

马德华不说话了。

陈锐在旁边忙着撇清关系:"我的模块跟这个没关系,我的代码都是自己写的。"

赵北辰看了他一眼,没有接话。

八

修复完成后,已经是凌晨一点了。

大家都很疲惫,但赵北辰说:"趁大家都在,把排查过程复盘一下。"

他在白板上写了一个标题:

无责复盘(Blameless Postmortem)

"无责复盘的规矩就一条:对事不对人。咱不是来追究谁的责任的,是来找出系统上的漏洞,别让同样的事再来一次。"

他列了一个框架:

1. 事件时间线(Timeline)
2. 影响范围(Impact)
3. 根本原因分析(Root Cause)
4. 修复措施(Resolution)
5. 改进行动(Action Items)

大家一起梳理了事件时间线和影响范围。然后到了根本原因分析。

"根本原因不是'马德华用了 AI 生成的代码'。"赵北辰说。"根本原因是我们的流程有漏洞。"

他在白板上写了三条:

根本原因:
1. 代码没有经过 Code Review 就合入了
2. 缓存模块没有自动化测试覆盖过期策略
3. 没有灰度发布机制——代码直接全量上线,没有逐步放量

"如果有 Code Review,Reviewer 可能会发现过期时间的问题。如果有自动化测试,测试会验证缓存的过期行为。如果有灰度发布,问题会在小流量阶段就被发现,不会影响全部用户。"

"所以问题不在于'谁犯了错',而在于'为什么我们的系统允许这个错误发生'。"

九

然后是改进行动。赵北辰让每个人提一条。

苏薇:"所有代码必须经过至少一人 Review 才能合入,没有例外。"

陈锐:"缓存模块要补充自动化测试,覆盖过期策略、缓存穿透、缓存雪崩等场景。"

马德华沉默了一会儿,说:"以后 AI 生成的代码,我会逐行审查,跟手写代码同等标准。"

运维老张:"上线流程加灰度发布——先发 10% 的流量,观察半小时没问题再全量。"

DBA 小刘:"数据库加慢查询告警,缓存命中率低于 80% 时自动告警。"

轮到王志远了。

他一直没说话。不是因为没有想法,而是因为他在想另一件事。

从九点到现在,四个小时,他一直在观察整个排查过程。他注意到一个问题——排查的前两个小时,大家一直在看代码、看日志、看监控,但始终没有找到根本原因。直到赵北辰画了那条时间线,才把"缓存逐渐过期"和"预热加了过期时间"联系起来。

是什么让赵北辰找到了根本原因?

是那条时间线。是他把零散的信息可视化了——画成了图,才看出了规律。

王志远想起了自己在第九章学到的东西:先画图,再分析。

他站起来,走到白板前。

十

"我有一个想法。"他说。

所有人看向他。

他在白板上画了一张图——不是时间线,而是一张故障排查流程图:

线上告警
  │
  ▼
1. 确认现象(看监控)
  │ → 什么指标异常?异常程度?影响范围?
  │
  ▼
2. 画时间线(关键!)
  │ → 异常从什么时候开始?
  │ → 同一时间有什么变更?(发版、配置变更、流量变化)
  │ → 异常是突然发生还是逐渐恶化?
  │
  ▼
3. 缩小范围
  │ → 根据时间线定位到可疑变更
  │ → 查看变更的代码 diff
  │
  ▼
4. 验证假设
  │ → 提出假设:"是不是因为 XX 导致了 YY?"
  │ → 用数据验证:日志、监控、复现
  │
  ▼
5. 修复
  │ → 先止血(回滚/临时修复)
  │ → 再根治(修复根本原因)
  │
  ▼
6. 复盘
  │ → 无责复盘
  │ → 改进行动

"今晚的排查,前两个小时我们一直在第 1 步和第 3 步之间来回——看现象、看代码、看现象、看代码。直到赵哥画了时间线,才发现'缓存逐渐过期'这个关键线索。"

他指着第 2 步:"画时间线是最关键的一步。它把零散的信息串成了一条线,让你看到事件之间的因果关系。"

赵北辰盯着这张图看了几秒钟,然后猛拍了一下桌子:"就是这个!"

他转向其他人:"志远画的这个流程图,以后作为我们组的故障排查 SOP(标准操作流程)。每次线上故障,按这个流程来。"

苏薇看了王志远一眼,忍不住说了一句:

"王小胖,行啊你。"

十一

凌晨三点,复盘结束。

赵北辰在白板上写下了最后一条 Action Item:

AI 生成代码的审查规范:所有 AI 生成的代码必须经过人工 Review 和测试覆盖,与手写代码同等标准。AI 是工具,不是替罪羊。

他拍了一张白板的照片,发到了群里。

大家陆续离开。马德华走的时候,在门口停了一下,回头看了王志远一眼。他的表情很复杂——有愧疚,有感激,也有一点说不清的东西。

"小胖,"他说,"谢谢你画的那张图。"

王志远笑了笑:"德华哥,下次 AI 生成的代码,记得自己看一遍。"

马德华苦笑了一下,点了点头,走了。

十二

凌晨三点半,王志远走出公司大门。

夜风很凉。他站在路边,深深地吸了一口气。

他想起四个小时前,所有人都在焦头烂额地看代码、看日志,毫无头绪。而他一直在旁边默默地听、默默地想。他不是最懂缓存的人,不是最懂 Redis 的人,但他做了一件别人没做的事——他画了一张图。

一张故障排查流程图。

不是什么高深的技术,不是什么天才的灵感。只是把他这几个月学到的东西——画图、结构化思维、先想清楚再动手——用在了一个关键的时刻。

赵北辰说过:"笨功夫才是真功夫。"

今晚,笨功夫派上了用场。

他掏出手机,打开笔记本 app,写下:

Day 135,凌晨 3:30

  1. 线上故障排查的关键:画时间线。把零散的信息串成因果链。
  2. 缓存雪崩:大量缓存同时过期,请求全部穿透到数据库。预防方法:过期时间加随机偏移。
  3. AI 生成的代码不是免责金牌。代码署你的名,责任就是你的。
  4. 无责复盘:对事不对人。找系统性问题,不找替罪羊。
  5. 今晚我做了一件事——在所有人都在看代码的时候,我画了一张图。赵哥说"就是这个"的时候,我觉得这几个月学的东西都值了。
  6. 苏薇说了一句"王小胖,行啊你"。这是她第二次在公开场合夸我。我决定把这句话记下来,以后心情不好的时候拿出来看看。

他收起手机,抬头看了看天。

凌晨的天空没有星星,但路灯很亮。

他忽然觉得,自己好像不再是那个入职第一天连数据库都不敢碰的小胖了。


:::{admonition} 小胖的笔记 :class: tip

本章知识卡片:故障排查与无责复盘

1. 故障排查 SOP(标准操作流程)

1. 确认现象 → 什么异常?程度?范围?
2. 画时间线 → 何时开始?同期有什么变更?突发还是渐变?
3. 缩小范围 → 定位可疑变更,查看代码 diff
4. 验证假设 → 提出假设,用数据验证
5. 修复 → 先止血(回滚),再根治(修复根本原因)
6. 复盘 → 无责复盘,定改进行动

2. 缓存常见问题

问题 描述 预防方法
缓存雪崩 大量缓存同时过期 过期时间加随机偏移
缓存穿透 查询不存在的数据,每次都打到 DB 布隆过滤器 / 缓存空值
缓存击穿 热点 key 过期,大量请求同时打到 DB 互斥锁 / 永不过期 + 异步更新

3. 无责复盘(Blameless Postmortem)

原则:对事不对人。找系统性问题,不找替罪羊。

框架:

1. 事件时间线(Timeline)
2. 影响范围(Impact)
3. 根本原因分析(Root Cause)
4. 修复措施(Resolution)
5. 改进行动(Action Items)

根本原因分析的层次: - 表面原因:马德华的代码给缓存加了过期时间 - 流程原因:代码没有经过 Review - 系统原因:没有自动化测试覆盖缓存策略,没有灰度发布

4. SLA / SLO / SLI

概念 全称 含义 示例
SLI Service Level Indicator 服务质量指标 请求成功率、响应延迟
SLO Service Level Objective 服务质量目标 成功率 > 99.9%,延迟 < 200ms
SLA Service Level Agreement 服务质量协议 与客户签订的承诺,违反有赔偿

关系:SLI 是度量,SLO 是目标,SLA 是承诺。

5. AI 生成代码的审查规范

# 规范
1 AI 生成的代码必须经过人工逐行审查
2 审查标准与手写代码完全相同
3 必须有对应的单元测试覆盖
4 必须经过至少一人 Code Review
5 提交信息中标注 AI 辅助生成的部分
6 AI 是工具,不是替罪羊。代码署你的名,责任就是你的

:::