第十三章 线上故障
一
那天晚上的事情,王志远后来回忆了很多次。
每次回忆,他都觉得那是他入职以来最漫长的一个夜晚,也是他真正"长大"的一个夜晚。
二
事情发生在周三晚上九点。
王志远刚回到出租屋,正准备洗澡,手机突然疯狂震动。钉钉群里一片红色告警:
🚨 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
- 线上故障排查的关键:画时间线。把零散的信息串成因果链。
- 缓存雪崩:大量缓存同时过期,请求全部穿透到数据库。预防方法:过期时间加随机偏移。
- AI 生成的代码不是免责金牌。代码署你的名,责任就是你的。
- 无责复盘:对事不对人。找系统性问题,不找替罪羊。
- 今晚我做了一件事——在所有人都在看代码的时候,我画了一张图。赵哥说"就是这个"的时候,我觉得这几个月学的东西都值了。
- 苏薇说了一句"王小胖,行啊你"。这是她第二次在公开场合夸我。我决定把这句话记下来,以后心情不好的时候拿出来看看。
他收起手机,抬头看了看天。
凌晨的天空没有星星,但路灯很亮。
他忽然觉得,自己好像不再是那个入职第一天连数据库都不敢碰的小胖了。
:::{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 是工具,不是替罪羊。代码署你的名,责任就是你的 |
:::