凌晨2:47,手机在枕头边上震
我到现在还记得那个时间点,2023年4月11号,2:47。PagerDuty的告警音特别刺耳,是一种你听一次就再也不想听到的频率。我摸黑抓到手机,眯着眼看Grafana那条错误率曲线——从0.3%直接拉到17%,几乎是垂直的。那天我白天刚跑完一个5公里,累得跟狗一样,本来以为能一觉到早上8点。
我爬起来开电脑的同时在群里发了一句「我接了」。后来我复盘过自己的反应速度:从被叫醒到打开第一个看板,用了4分12秒。这个数字其实还行,但在那种状态下人是懵的,我第一件事居然是去翻我们那份47页的《XX服务应急预案V3.2》。翻到第9页才找到「数据库连接异常」这一节,而那一节写的是「请联系DBA」。凌晨2点多,DBA在睡觉。
这次故障最后持续了58分钟,MTTR(平均恢复时间)远超我们对外承诺的30分钟SLA。事后我算过,那58分钟里我大概有11分钟是在翻文档。
第一个反直觉的结论:应急预案越长,出事的时候越废
我后来干的第一件事,是把那份47页的文档删到1页。真的,就一页A4。上面只留三样东西:这个服务最可能挂的3个原因、对应的3条止血命令、以及「先做什么后做什么」的顺序。多一个字都不写。
为什么?因为突发状况下人的认知带宽极其有限。你在凌晨3点、被吓醒、心跳120的状态下,你只能执行「复制粘贴一条命令」这种级别的操作,你不可能去读懂一段300字的背景说明。这跟我们平时想象的「准备充分」完全相反——文档写得越细,出事时越找不到。
我给大家一个具体数字参考:我们那套服务,日常QPS在1.2万左右,数据库max_connections设的是500。故障那天连接数被打满,是因为一个报表接口的慢查询全表扫了1200万行,没走索引,单个查询耗时从20ms飙到8.3秒。这个接口一天只被调用几十次,但它每次调用都占着连接不放。
所以一页纸runbook的第一行应该是:先看 connections_used / max_connections,超过80%直接执行限流脚本。就这一句,能让一个人少懵10分钟。
第二个结论:先止血,根因可以明天再找
我见过太多工程师(包括我自己早期)在故障现场的第一反应是「为什么会这样」。这个本能特别害人。因为你在追问为什么的时候,用户还在那儿刷不出页面。
正确顺序是:止血 → 恢复 → 定位 → 根治。止血的手段其实就那几个,我列一下我们实际用过的:
- 回滚:如果10分钟内有过发布,别想了,直接回滚,不要试图在线上修。我们现在的规矩是「发布后15分钟内出问题,无条件回滚」。
- 限流:把非核心接口的QPS砍掉。那次我们把5个非核心接口限流到原流量的30%,整体QPS从1.2万降到约4000,数据库连接数从500掉到118,错误率在90秒内从17%回落到0.6%。
- 降级:返回缓存、返回兜底数据、甚至返回一个「稍后再试」的静态页。难看,但比整个站点白屏强。
- 扩容:这个排最后,因为扩容慢、而且往往治标不治本。我们K8s节点扩容一次平均要3分40秒,够用户骂你好几轮了。
那次故障我们的转折点就是限流那一下。之后才慢慢查出是那个报表接口的SQL少了索引。加索引花了2分钟,但如果没有前面那90秒的止血,损失会大得多。
第三个结论:复盘会第一句话不该是「谁的责任」
这一点我踩过坑。刚带团队的时候,出了故障我第一反应是「谁改的代码」。结果就是,之后几次小故障,组里没人敢主动在群里说「我刚才发了个东西,好像有点问题」。大家都等监控报警,等别人发现。这个沉默的成本高得吓人。
后来我改了规矩:复盘会开场我固定说一句话——「今天只讨论系统哪里可以被改进,不讨论人的问题」。这不是什么心灵鸡汤,这是很实际的工程考量。一个愿意在故障发生3分钟内主动承认「可能是我这个改动引起的」的人,能帮你省掉20分钟的排查时间。
顺手分享我们的复盘模板,就4个问题:这次事故用户受影响多久?我们的检测是怎么发现的(监控/用户投诉/自己发现)?哪一步可以更快?需要改的3件事分别是什么、谁负责、什么时候完成?最后这一条必须落到具体的人和日期,不然复盘就是聊天。
最后说句实话
工作里的突发状况,从来不是靠「你准备得多充分」解决的,而是靠「你准备得多简单」。我现在的习惯是每个季度把自己负责的服务runbook拿出来读一遍,读的时候只问一个问题:如果我现在刚被叫醒,脑子一片浆糊,这一页纸能不能让我在5分钟内做出第一个动作?
如果不能,就继续删。删到能为止。
那套47页的文档现在还在我们wiki里,我留着的唯一原因是想提醒自己和后来的人:文档的长度和它的有用程度,经常是反着来的。