那天的凌晨三点,手机在床头柜上震到飞起。我看了一眼来电显示,是值班同事。电话那头第一句话是:“用户下单一直转圈,订单服务已经挂了,支付回调超时一片红。”
那一瞬间脑子是空白的,然后是愧疚——因为我在头一天晚上十点,刚批准了一个“看起来人畜无害”的紧急变更上线。
那个晚上我们七个人忙到早上七点,但真正改变这个团队的,不是那六个小时的抢修,而是三天后复盘会议室里那五个连起来的“为什么”。也是从那次之后,团队立了一条铁规:写代码之前,先问五个为什么。这篇文章就把那晚的完整经过、复盘逻辑和这条铁规的落地方法一次性说清楚。
1. 那次把我从凌晨三点吵醒的告警电话
1.1 事故时间线:从第一个异常日志到全站雪崩
先交代一下背景。当时我们做的是一款面向 C 端用户的电商交易系统,核心链路涉及商品、订单、支付三个服务,部署在自建机房,网关用的是 Nginx + Lua,应用层是 Spring Boot,数据库是 MySQL 一主两从,缓存用的 Redis 哨兵集群。这套架构不算复杂,但也绝对不简单,日常 QPS 高峰期大约在 8000 到 12000 左右。
那个“紧急变更”是什么?一个新上线的营销活动——限时秒杀。产品周五下午提的需求,周六就要上线,排期只剩一个白天加一个晚上。开发同事说已经测过了,性能测试也压过一轮,觉得问题不大。于是周六晚上十点,我们走了紧急变更流程,直接把代码发到了生产环境。
现在回头看,事故的起点不是程序崩溃,而是一个很不起眼的日志异常。周日凌晨 01:23,监控系统开始推送告警,主要是收银台服务的接口响应时间从平均 200ms 飙升到了 1200ms,随后订单创建接口也开始出现超时。02:00 左右,数据库连接池被打满,大量线程阻塞在获取数据库连接的调用上。02:15,Redis 缓存因为集中过期引发了一次穿透,原本被缓存挡住的流量直接灌到了数据库。02:30,订单服务内存飙高,GC 频繁,最终多个节点进入不可用状态,网关开始向上游返回 502。
整条链路就像多米诺骨牌,从第一个接口响应变慢开始,到服务雪崩,前后也就 70 分钟。如果没有值班同学手动摘除问题节点并重启,事故影响范围可能会更大。
1.2 第一轮止血:重启、扩容、降级,都是治标
当时我们做的第一件事,不是查根因,而是止血。这也是我认为所有故障处理最应该遵循的原则:事故发生的时候,先恢复服务,再讨论责任。
凌晨 02:40,值班同学将订单服务的问题节点从负载均衡中摘除,同时把数据库连接池上限临时调高了 20%,并清理了一批 Redis 中已过期的热点 key,让缓存重新生效。03:10,服务基本恢复正常。
但谁都明白,这一套操作就是“脚痛医脚”。连接池调大了,只是让车多走了几条道,并不代表路的终点没有塌方;缓存重新生效了,也只是临时挡住了流量,下一波缓存过期高峰随时可能再来。所以当时我们做了两个决定:一是把所有异常日志完整留存,不允许任何人提前清理;二是把原定早上九点的复盘会提前到当天下午,所有核心开发必须到场。
那个下午的复盘会,成了整个团队心态的转折点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 复盘会议室里,我们被第五个“为什么”问住了
2.1 第一层到第三层的追问:从连接池耗尽的表象到底层 SQL
复盘的开始带着火药味。业务方问为什么秒杀活动上了;运维问为什么缓存集中过期没人发现;开发有点委屈,说功能测试、接口测试都过了,压测报告也出来了,凭什么现在全赖在他头上。
主持人打断争论,说我们不要讨论“谁错了”,我们只做一件事——一个问题接着一个问题往下问。于是我们真的从事故发生时的第一个异常开始,拉了日志,建了一张时间线表格,然后用五步追问一层层往下挖。
第一问:为什么订单服务会挂?
证据指向数据库连接池被打满,大量请求阻塞在等待连接上。
第二问:为什么数据库连接池会被打满?
因为下单接口做了一次全表扫描。那张订单明细表当时有接近 3000 万行数据,活动流量一冲上来,查询全部命中慢 SQL,数据库的 CPU 和 IO 瞬间到顶。
第三问:为什么本来有索引的字段却走了全表扫描?
这里我们查了很久才发现,秒杀功能为了展示活动库存加了一个 stock_deduction_log 表,下单时需要通过 activity_id + sku_id 去查这张表判断是否还有库存。开发同学建表的时候,只设了主键 id,没有给 activity_id 和 sku_id 建联合索引。平时数据量小看不出问题,活动一上线数据量暴涨,这个查询就变成了性能炸弹。
三层问完,问题看起来已经清楚了:表缺索引,上线前没有发现。很多人觉得到这个程度就可以结案了,但我们没有停,因为这几个问题回答不了“为什么带病上线”这个更核心的疑问。
2.2 真正的问题不在代码里,而在流程里
第四问:为什么开发建表没有创建联合索引?
开发给出的回答很“合理”:因为需求排期太紧,他写完代码已经凌晨了,建表语句是手工执行的,当时只关注了主键和普通字段,确实忽略了索引。这个答案听起来就是一次简单的“粗心”,如果复盘停在这里,结论就会变成“批评开发不够仔细,以后注意”。
但再往下追问第五问,问题本质就变了。
第五问:为什么开发一个人就能把表结构改了?为什么建表不用走审核流程?为什么测试环境和压测环境的数据量没有覆盖到这个问题?
答案逐渐清晰:因为这次变更走的是“紧急通道”,跳过了 DBA 审核。原来公司的变更管理规范里有一条——新建数据表必须由 DBA 评审索引设计和数据量预估,两周以内的紧急需求可以通过“线上变更窗口”跳过评审,但前提是必须完成压测。而这次秒杀的压测,因为环境资源不够,只在测试环境用 10 万行数据做了功能验证,根本没有在接近生产数据量的环境里跑过。
会议室安静了几秒。所有人都意识到,真正让系统崩溃的不是某个人的“粗心”,而是流程给这种粗心留了一扇没上锁的门。技术问题很直接,查日志、看 SQL、分析执行计划,半天就能定位;真正的风险藏在看不见的流程缝隙里——什么时候可以跳过评审、压测数据量不达标谁能发现、建表操作有没有第二个人的复核。如果这些缝隙不堵上,今天躲过了秒杀,明天还有可能栽在别的地方。
3. 五层追问的具体方法:如何让根因自己现形
3.1 每一层要问什么、问到什么程度才算停
很多人听到“五个为什么”,觉得就是连续问五遍“为什么”,其实不是。这套方法最早来源于丰田生产系统,核心不在于数量,而在于追问的深度和方向。我个人实践后的理解是:每个“为什么”都要基于事实和证据,而不是凭感觉猜测;层次要一层比一层靠近系统、流程和机制,而不是停留在个人行为上。
为了让大家能直接用,我把我们团队现在使用的追问框架整理成了一张表,每一层都有明确的问题和停下来的标准:
| 层数 | 追问方向 | 典型问题 | 什么情况下可以停止 |
|---|---|---|---|
| 第一层 | 直接的技术表象 | 为什么接口超时?为什么连接池被打满? | 能定位到直接触发的技术行为(如慢 SQL、死锁、内存溢出) |
| 第二层 | 该技术行为为何未被拦截 | 为什么没有索引?为什么缓存没生效?为什么监控没告警? | 找到某一项检查、验证或监控环节的缺失 |
| 第三层 | 流程与规范层面的缺口 | 为什么评审没发现?为什么压测没测出问题? | 找到规范中某个特定场景是否覆盖得不够 |
| 第四层 | 组织与机制层面的原因 | 为什么这次可以跳过评审?为什么压测环境只有 10 万行数据? | 找到类似场景普遍存在的机制漏洞 |
| 第五层 | 文化与长期改进的土壤 | 为什么发现问题时没有人主动上报风险?为什么团队默认紧急更重要? | 能形成一条可执行、可落地的改进措施,而不是停留在批评上 |
需要注意的是,这五层不是死板的。有些问题问到第三层就已经清楚该怎么办,有些则需要挖到第五层。但有一个判断标准始终不变:结论必须能转化为一条具体的动作,比如“建表必须走 DBA 审核”“压测数据量不得低于生产环境的 1/10”“生产环境慢日志阈值由 2 秒调整为 500 毫秒”。如果结论只是一句“以后大家多加注意”,那就等于没挖到根因,只是在表层做了一次无用的总结。
3.2 常见误区:把“人为失误”当成终点
我在复盘过这么多系统事故后发现,最容易犯的错误就是把追问停在“人”上。最常见的说法是“因为某某没注意”“因为开发经验不足”“因为运维操作失误”。表面看确实是人的原因,但人为什么会失误?为什么失误没有被其他人拦截?为什么流程没有兜底?
我之前在另一家团队就见过一个案例。一次发布把配置写错了,导致线上某个功能瞬间降级,负责发布的人被点名批评。结果第二次发布的时候,另一个人又写错了另一个配置,只不过这次运气好,影响范围不大,没有产生告警。这说明什么?说明该修的不是这个人的记性,而是发布系统本身有没有提供配置校验能力。强烈建议所有做复盘的人记住这条原则:把“人”当成终点,是最偷懒、也最无效的复盘方式。
还有另一个误区:过早下结论,然后只找支持这个结论的证据。比如一开始怀疑是缓存问题,就只盯着 Redis 日志看,忽略了数据库慢查询。正确的做法是先把所有相关数据拉齐,按时间线排好,再逐层追问。如果某一层的证据链断掉了,宁愿停下来补充数据,也不要凭直觉跳过去。
4. 从事后复盘到事前拦截:写代码前问 5 个 why
4.1 写之前问什么:需求、方案、改动点、依赖、验证
这次事故给我们的最大警醒不是“多建索引”,而是“为什么这些问题在上线前没有被任何人提出来”。于是团队讨论之后决定:把五层追问从事故复盘搬到写代码之前。不做成复杂的评审流程,就当成编码前的一种思考习惯。
我们总结出的五个问题,不是针对架构师或 Tech Lead 的,而是要求每一个写代码的人都必须自己过一遍。
第一个问题:为什么做这个需求?
不是要你复述产品经理的原话,而是要你去理解这个需求到底解决了用户什么问题。秒杀活动本质上是为了拉新和促活,那么技术上就必须考虑峰值流量、库存一致性、防止超卖、降级策略。如果开发只看到“加一张表、写一个接口”,那他写出来的代码大概率只覆盖了功能路径,完全没有覆盖风险路径。
第二个问题:为什么选这个技术方案?
很多开发拿到需求就动手写,写完才发现在现有架构里这个方案根本跑不通。比如这次秒杀场景,如果一开始就考虑用 Redis 的原子操作预扣库存,也不会让数据库承载那么大的查询压力。技术选型不一定要用最酷的中间件,但要能和团队的现有基础设施、团队成员的维护能力匹配。你在选方案的时候至少要回答:这个方案相比现有模式的优势是什么?它引入了哪些新的依赖或风险?
第三个问题:为什么改在这个位置?
这是检查改动影响面最直接的方法。代码在哪一层改?改的是接口还是数据表?是否涉及旧数据迁移?是否影响其他调用方?连接池要调整吗?缓存结构要变吗?当前代码所在的模块边界是否清晰?很多线上事故都发生在“顺手改一个方法”时,没有理解这个方法被哪些上层服务调用。涉及库表结构变更时尤其要谨慎,任何 DDL 操作都需要提前规划索引,并产生对应的数据字典文档。
第四个问题:为什么这样写代码?
这个问题针对代码本身的正确性与扩展性。异常情况有没有考虑?边界值有没有处理?是否有隐藏的循环等待?并发场景下有没有竞态条件?是否有足够的日志和监控指标?这是最考验基本功的一层,但也最容易被跳过,尤其当开发时间紧张、需求催得急时,大家都想着先把功能跑通,其他后面再说。
第五个问题:为什么能证明这样可以上线?
这是把测试、评审和发布绑在一起的核心问题。你怎么证明你的代码是正确的?是单元测试、自动化测试、性能压测还是人工验证?测试环境的数据量和生产环境的比例是多少?你怎么知道你的改动不会带来新的瓶颈?各种中间件(Redis、MQ、MySQL)在高负载下的表现是否符合预期?如果这一步答不上来,那这个人还没有准备好发布自己的代码。
4.2 用一张“写代码前自查表”把规则固定下来
光在团队里口头喊“写代码前要思考”是没用的,人的记忆在压力面前特别不可靠。我们的做法是把它做成了代码仓库根目录下的一个 Markdown 文件,叫 CODING_CHECKLIST.md,并且用 CI 机器人做了最小化约束:所有 PR 描述里必须按这五个问题逐条填写,有一项没填或写着“N/A”且没有说明理由的,代码评审直接驳回。
自查表的模板长这样:
markdown复制## 变更背景
- 需求来源、预期价值、用户场景
## 一、为什么做这个需求?
(一句话说明需求价值,如果只是“产品让做的”,请重新思考)
## 二、为什么选这个技术方案?
(列举 2-3 个候选方案,说明为什么选当前方案,不选的理由是什么)
## 三、为什么改在这个位置?
(明确改动涉及的服务、模块、数据表/缓存、接口调用方;如有 DDL,附上索引设计)
## 四、为什么这样写代码?
(简述关键逻辑,特别是并发、异常、边界处理方案)
## 五、为什么能证明可以上线?
(列出测试覆盖范围、压测数据量、依赖的监控告警,以及和当前生产环境的差异)
刚开始推行的时候,团队里一片抱怨声,觉得这是形式主义,严重拖慢开发效率。但真正执行两周之后,效果非常明显:不少开发在填写“第三个问题”时,发现自己根本不知道这个接口到底被哪些服务调用,于是主动去翻调用链;不少人在填“第五个问题”时,发现压测环境的数据量根本模拟不了生产环境的海量数据,被迫提前做了数据准备。这两个问题一旦在开发期就被意识到,上线后的风险直接下降一大截。
4.3 代码评审里的 why 文化
除了开发自查,代码评审也是落实“写代码前问为什么”的重要阵地。以前我们团队的代码评审,大家的注意力普遍集中在“这段代码写的对不对、有没有语法错误、有没有明显 bug”上。但“对不对”只是最低标准,真正让代码危险的是“为什么这样做”没有讲清楚。
我们现在对代码评审的要求不是只看代码 diff,而是让提交者在评审描述里把五个问题的答案写清楚。评审者第一遍优先看背景和方案说明,而不是看具体实现;只有当背景和方案逻辑成立,才进入逐行看代码的环节。这样做有个很直接的好处:评审者不会在自己还没搞懂意图的情况下盲目通过或盲目否定代码,而是在上下文一致的基础上提出有效建议。
另外一个细节,是 Commit Message 也要写“为什么”,而不是只写“改了什么”。这一点腾讯和阿里的很多团队都在用,背后逻辑是一样的——代码是团队资产,版本库里的历史记录应该能告诉未来的成员“为什么当初这样做”,而不是让后人对着 git 日志猜。
5. 铁规落地的 90 天:团队发生了什么变化
5.1 新人如何适应这套规则
事故之后的三个月里,团队陆陆续续来了两名新同事,还有两名实习生。在最开始,他们是不理解这套五问规则的——毕竟对于一个刚入门的人来说,能把功能实现出来已经不容易,哪还有精力思考那么多为什么,甚至一个实习生跟我说“写个 CRUD 为什么要这么复杂”。
我的处理方式不是强迫他们看完文档就去背诵那五句话,而是找了一个真实的小需求,手把手带他走了一遍完整的自查表:先讲业务背景,再讲技术选型,然后带着他画改动点的影响图,最后一起写测试和观察日志。走完这一次之后,他至少明白了这些规则不是用来刁难人的,而是用来保护自己的——保护自己不写出让自己凌晨三点爬起来擦屁股的代码。
对新人来说,这套规则还有一个重要的助理:帮助他们快速建立全局视野。一个只关注自己一亩三分地的开发,和知道自己的代码如何影响整个系统的新人,成长速度完全不一样。所以我强烈建议每一个团队在带新人的时候,不是先教他们怎么写代码,而是先让他们学会问“为什么”再写代码。
5.2 规则的边界:什么时候该跳过,什么时候必须停下来
当然,我并不是说所有代码都需要经过五轮灵魂拷问。一个简单的文档更新、一个纯配置变更、一个一行代码的 typo 修复,如果也非要按五问模板走一遍,那确实过分教条了。我们也的确在落地过程中遇到过这种场面:好几个人在一个处理服务重启脚本的小 PR 里,填了一堆无关紧要的“为什么”,看起来认真,实际是浪费时间。
后来我们把规则做了一次细化和分级:
| 变更类型 | 五问要求 | 说明 |
|---|---|---|
| 普通 Bug 修复(改动 < 50 行) | 简化填写,重点写清原因和验证方式 | 避免让小事变重 |
| 新功能开发 / 接口新增 | 必须完整填写五问 | 核心风险场景 |
| 涉及数据库 / 缓存结构变更 | 必须引入 DBA 评审,并附数据量预估 | 任何 DDL 都算 |
| 紧急热修复(线上故障) | 允许事后 24 小时内补填 | 故障优先恢复,但复盘不可免 |
| 纯配置 / 文档修改 | 无需填写 | 减少噪音 |
这个分级规则让团队从“被规则绑架”的状态里走了出来,大家也真正意识到:这套机制是给风险上保险,不是给日常工作添堵。越是高风险、频繁上线、涉及资金交易的业务系统,越要把这类机制当成硬约束,而不是某位 leader 的心血来潮。
5.3 当 AI 开始写代码,why 文化反而更重要
最近圈子里讨论最多的一个话题,就是 AI 编程工具越来越强,很多人担心工程师岗位会被替代。我的观点恰恰相反:AI 做大概率正确的事情越厉害,真正懂“为什么”的人就越值钱。
AI 可以快速生成一段调用数据库的 Spring 代码,但它不会告诉你这个表缺了一个联合索引会影响线上峰值;AI 可以写出一个看起来很完整的接口实现,但它不会判断这个接口是否承载了超出预期的流量、是否需要限流、是否应该走异步。这些“为什么”的追问,底层依赖的是对业务场景的理解、对系统架构的全局认知,以及对数据量和并发量的敏感度。这恰好不能靠 AI 自动生成,而要靠人和系统、团队与流程之间的深度耦合。
我们团队已经开始尝试用 AI 辅助生成代码和测试用例,但同时也明确了一点:AI 生成的每段代码都必须经过五问自查,并且通过代码评审。AI 能帮我们把想法变成代码的时间大幅缩短,但如果没有五问机制把关,就相当于我们拥有了一个更高效制造 bug 的引擎,那才是真正可怕的事情。
5.4 一些在落地途中踩过的真实小坑
最后分享几个我们在执行这件铁规时踩过的坑,希望能帮你少走弯路。
第一个坑是“只强调收益,不解决成本”。一开始强调五问时,大家凭热情执行了一周,但一到需求高峰期就立刻放松,因为没有体会到这套东西对自己的直接好处。后来我们做了两件事扭转局面:一是把已经发现的问题做成了案例库,在周会上分享“这周谁通过五问避免了一个可能的事故”,让大家看到具体的收益;二是简化了填写成本,表单一开始做得太复杂,后来调整成每个问题只需要一两句话就能说明白。
第二个坑是复盘会变成追责会。只要主持人一不留神,复盘就会滑向“谁犯了错”的泥潭。我们现在的复盘会铁律是:所有发言禁止出现人名,只允许描述事实和流程。如果确实需要说明某个环节是某人负责的,用角色代替,比如“应用开发”“值班运维”。这个规则看起来很冷冰冰,但确实让人更容易承认问题,而不是防御性争论。
第三个坑是“只做技术复盘、不做流程改进”。五问如果停在了“开发没有建索引”,那三个月后还会有别的人重犯;只有继续问到了“为什么建表没有 DBA 参与”,后端的改进才能真正落地。我们团队几乎每一个复盘结论最终都会对应到三个东西之一:流程规范、系统工具、人。前两者是长效方案,第三步才是案例学习,次序不能反。
从那次凌晨三点的电话到现在,已经过去大半年了。团队里再没有人觉得“写代码前问五个为什么”是一句空口号。它慢慢变成了大家在 PR 描述里认真填写的几行字,变成了评审对话里自然而然的“你为什么要这样做”,也变成了新人口中那句“我来做之前问了五个问题,发现这个方案有问题”。如果你也是一名带团队的技术 lead,或者正在经历频繁的线上事故,我真的建议你,不用等到系统崩溃那晚才开始行动,今天就把这五个问题带到你的代码仓库里。
