跳到内容

2026-6-11 事故报告

794TD
TOGAPhotos 网站管理

距离上一次撰写事故报告已经过去两年。在这两年里,虽然TOGAPhotos也出现过一些功能 Bug 和短暂的服务中断,但严重程度都不足以让我再动笔。这很大程度上要归功于上一次事故后,我们开始建设的自动化测试与自动部署体系。

但人并不总是那么聪明。

事故表现及影响

这次事故发生在北京时间的6月11日下午三点左右,许多用户都收到了一封标题为[TOGAPhotos]图片审核结果通知的邮件,内容包含了该用户自 TOGAPhotos 上线以来几乎所有的图片审核结果。

先说大家最关心的问题:有没有额外的后续影响?答案是,没有。除了被错误群发的邮件之外,这次事故没有造成任何用户数据或图片的污染。作为用户你不需要进行任何额外操作。这次事故也不会影响账号状态或图片审核结果。每位用户收到的仍然是自己账号下的审核结果,邮件没有发送给其他用户,也没有造成用户之间的数据混淆。

在一切发生之前

TOGAPhotos在2025年11月上线了邮件通知功能,将近期的审核结果通过邮件告知用户。该功能经历一轮内测和一轮灰度测试后全量推送给所有用户,此后也一直运行良好。

最近TOGAPhotos有两次较大的更新:第一次是新增了飞机的历史信息查询功能,第二次则是新增了一个给第三方提供图片服务的API。(另外宣传一下我们和JetMark的合作)

在第一次更新之后,服务器程序开始频繁的触及PM2的自动重启。后来经过快速排查,是程序的内存占用超过了早期设定的500MB上限,进而触发PM2自动重启。尽管这种自动重启并不会造成太大的影响,但我们还是开始着手进行优化。

混乱的桌子

在修复内存问题的同时,我们还同步开发着第三方图片服务 API。一片混乱之中,我们错误地修改了用于筛选待通知图片的条件。

简单来说,每张图片在 TOGAPhotos 的数据库中都通过 notify 字段来标记是否已发送邮件通知。事故发生前,这个字段有三个合法值:null(空)、0 和 1。0表示尚未通过邮件通知用户,1表示已经通知过。而 null 则完全是历史遗留问题——当初添加这个字段时,既没有配置非空约束,也没有设置默认值,导致所有历史记录都被数据库默认赋为了 null

原本的判断逻辑是:如果 notify 为0,则准备发送邮件。那些值为 null 的老记录就像化石一样安静地躺在数据库里,不会被误判为需要发送。

但在 6 月 11 日的一次代码提交中,这个条件被错误地修改为:如果 notify 为 0null,则发送通知邮件。

事故

6 月 11 日下午,包含错误判断条件的代码被合并到了 Release 分支。CI/CD 流水线照常运行,自动化测试全部通过,并判定代码没有异常,随后新版本被放行进入生产环境。

服务器在北京时间下午 3 点接收到新代码并执行重启更新。紧接着,定时任务被触发,海量的邮件请求瞬间涌入处理队列,内存用量快速上升并触及 PM2 配置的重启阈值,触发了进程自动重启。

我们做对了什么又做错了什么

在这次事故中,我们有一些地方做得不错:

虽然误发了大量邮件,但除此之外没有造成任何其他影响。

同时,面对海量请求冲击,PM2 的自动重启机制勉强维持了服务的持续在线,异步消息队列也如最初设计的那样完成了削峰填谷,没有造成全面瘫痪。

但这次事故也暴露了几个关键问题:

  1. 数据库设计不规范:notify 字段允许 null 且语义模糊,使得查询条件极易产生歧义,这为事故埋下了最深的一颗雷。

  2. 代码审查与测试盲区:对涉及历史数据的筛选条件变更,没有执行针对性的回归测试。CI/CD 流水线虽能通过,却完全无法评估异步任务可能引发的海量操作。

  3. 缺乏队列异常预警:当队列请求数瞬间暴增时,没有任何告警通知。

我们已经做了的事情

事故发生后,我们已经回滚相关代码,并暂停了邮件通知任务;同时确认本次误发邮件仅发送至对应账号邮箱,没有发生跨用户发送或数据混淆。后续我们会在完成字段清理、测试补充和队列告警后再恢复相关任务。

我们计划的改进措施

  • 数据库检查:检查现有表结构,消除语义不明的可空字段,补充默认值并增加必要约束,杜绝 null 再次成为逻辑陷阱。

  • 增强异步队列的测试与监控:在 CI 流程中加入针对定时任务和队列处理的回归测试,尤其要覆盖历史数据批量操作的场景。同时,为队列积压数量、处理速率等指标设置实时告警,一旦出现异常流量立即通知开发团队。

  • 强化批量操作变更的审查:任何涉及数据筛选条件的变更,都必须附带影响范围评估,并经过人工审查,而不再仅仅依赖自动化测试的绿灯。

写在最后的碎碎念

When I see people saying 99% of our code is written by AI, I literally get angry. Because those same people — I can pretty much guarantee — 100% of their code is written by compilers. But they never say that.

-- Linus Torvalds

老实说,这次事故还有一个更直接的解释:我没有仔细审查 Codex 生成和修改的代码。但这既不准确,也不能帮助我们避免下一次事故。

“我们会更仔细的检查AI生成的代码”是一句正确的废话。

无论代码最初来自人手写、AI 生成,还是任何其他工具辅助,最终把它合并、部署并运行在生产环境中的,都是我们自己。对于用户来说,服务出错就是出错,邮件被误发就是被误发。他们不需要关心代码是谁写的,也不应该为我们的工程流程缺陷承担后果。

更新于:

由 VitePress 驱动