那天的凌晨三点,手机在床头柜上震到飞起。我看了一眼来电显示,是值班同事。电话那头第一句话是:“用户下单一直转圈,订单服务已经挂了,支付回调超时一片红。”
那一瞬间脑子是空白的,然后是愧疚——因为我在头一天晚上十点,刚批准了一个“看起来人畜无害”的紧急变更上线。
那个晚上我们七个人忙到早上七点,但真正改变这个团队的,不是那六个小时的抢修,而是三天后复盘会议室里那五个连起来的“为什么”。也是从那次之后,团队立了一条铁规:写代码之前,先问五个为什么。这篇文章就把那晚的完整经过、复盘逻辑和这条铁规的落地方法一次性说清楚。
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,数据库的
