这是我第一次独立处理线上告警的完整复盘。那天晚上十一点,监控群里突然弹出红色告警,某个接口的错误率从 0.1% 飙到 18%。作为一个刚接手线上系统不到两个月的人,我手心全是汗。但两小时后,问题被定位、修复、上线、告警解除。这篇日记把那两个小时掰开揉碎写下来,既是给自己留档,也希望给第一次面对线上 Bug 的你一个可参考的排查节奏。
一、现象描述
告警来自订单查询接口,错误信息是 TypeError: Cannot read property 'price' of undefined。错误率从基线的 0.1% 在三分钟内飙升到 18%,受影响的请求集中在某个特定城市。前端表现是用户进入订单列表后页面空白,控制台报 500。
第一时间我确认了三件事:服务进程还活着(不是宕机)、数据库连接正常(不是 DB 抖动)、其他接口正常(不是全局故障)。这就把问题范围圈定到了"订单查询接口 + 特定城市"这个组合上。
二、排查时间线
下面是我事后整理的排查时间线,按分钟记录,方便你看到一个真实的排查节奏。
- 第 0-3 分钟:确认告警真实性,登录监控面板看错误曲线,确认不是误报。
- 第 3-8 分钟:拉取最近 5 分钟的错误日志样本 50 条,发现报错都指向同一行代码
order.price,且请求参数里 city 字段都是同一个值。 - 第 8-15 分钟:用该 city 参数手动请求接口,本地复现了同样的 500 错误,确认问题可稳定复现。
- 第 15-30 分钟:查数据库,发现该 city 下有一批订单的 price 字段为 null,而代码里直接访问了
order.price.amount,没有做空值判断。 - 第 30-45 分钟:追溯这批 null 订单的来源,发现是半小时前一次数据迁移脚本漏写了 price 字段。
- 第 45-70 分钟:写修复方案,加空值兜底 + 数据订正脚本,本地验证通过。
- 第 70-90 分钟:走紧急发布流程,灰度→全量,监控错误率回落到基线。
- 第 90-120 分钟:写复盘文档,登记到事故系统,定级为 P2。
三、根因分析
直接原因是数据迁移脚本写漏了字段,把一批订单的 price 写成了 null。但根因不是脚本 bug,而是更深的三层问题:
- 代码层:业务代码信任了数据完整性,访问嵌套字段前没有做空值判断。一旦数据"脏了",代码立刻崩。
- 数据层:price 字段在数据库里允许为 null,但业务语义上它应该是必填的。schema 没有约束住业务规则。
- 流程层:数据迁移脚本没有经过 review,也没有在预发环境跑全量回归,直接上了生产。
真正要修的,不只是那一行代码,而是这三层都要补上。不然今天修了 price,明天还会崩在别的字段上。
线上 Bug 的根因,往往不在出错的那一行,而在允许它出错的那套流程里。
四、修复方案
代码层的修复是访问嵌套字段前做空值兜底,用可选链 + 默认值:
// 修复前:直接访问,price 为 null 时崩溃
const amount = order.price.amount;
// 修复后:可选链 + 默认值,并打日志便于追踪
const amount = order?.price?.amount ?? 0;
if (!order?.price) {
logger.warn('订单 price 缺失', { orderId: order.id });
}
数据层的修复是给 price 字段加 NOT NULL 约束,并补一份历史数据订正脚本,把所有 null 订单的 price 回填为 0 并打上标记,等业务侧二次核对。
流程层的修复是:所有数据迁移脚本必须经过 review,必须在预发环境跑全量回归,必须由两人确认后才能上生产。这三条写进了团队的发布规范。
五、预防措施
复盘的终点永远是"下次怎么不重演"。我们落地了三条预防措施:
- 加监控:对订单关键字段做空值率监控,超过阈值就告警,而不是等用户报错。
- 加测试:核心接口补上"脏数据"场景的单测,专门喂 null、undefined、空字符串这些边界值。
- 加流程:数据迁移类操作走单独的发布通道,强制预发回归 + 双人确认。
六、写在最后
那次告警之后我没再怕过线上 Bug。不是因为我变强了,而是因为我有了一套节奏:先圈范围、再复现、再定位、再修复、最后复盘补流程。Bug 一定会再来,但只要节奏不乱,它就只是又一个两小时。希望这份时间线,能在你第一次面对红色告警时,给你一点点参考。