一周两炸

一周两炸

风格:叙事复盘体(Netflix/Stripe 工程博客)


3 月 2 日,星期一:十个 commit

一天之内,我给 ClawChat 的后端推了十个 commit。

这是一个聊天机器人项目——用户在 Telegram 上发消息,后端调用 OpenClaw(我自己搭的 AI agent 框架)处理请求,再把结果返回去。整个后端是那天从零搭起来的,用 AI 辅助开发,速度非常快。每次调通一个功能点就 commit 一次,十个 commit 到晚上全部上线。

那天的感觉很好。像一个人同时扮演了产品经理、后端工程师和 DevOps,而且每个角色都在高效运转。AI 帮我写了大部分代码,我做架构决策和最终验证。从构思到部署,整个链条在一天内闭合。

我没有写任何异常处理逻辑。没有加 fallback。没有检查服务器的容量参数。这些事情在当时看起来完全不紧急——功能还没跑通,谁会去想失败路径?

三天后我为此付出了代价。


3 月 5 日,星期四:ClawChat 不说话了

下午我发现 ClawChat 不回消息了。发什么都是同一句话:“暂时不可用”。

排查了一会儿,发现问题出在上游。ClawChat 的后端通过 HTTP 调用 OpenClaw,而 OpenClaw 的进程是通过 macOS 的 launchd 托管启动的。当天 launchd 重启了这个进程,但 launchd 环境下的 PATH 和我手动启动时不一样——它调用了一个错误版本的二进制文件,导致 OpenClaw 实际上没有正常运行。

HTTP 请求全部超时。ClawChat 的后端没有任何回退逻辑——上游不可达,它就直接返回一个固定文案。没有尝试 CLI 调用,没有尝试缓存,没有尝试降级,没有告警。静默地、优雅地、彻底地失败了。

修复本身不难。两步:修正 launchd 的 PATH 配置,加一个环境变量 OPENCLAW_CLI_FALLBACK_ENABLED 让 HTTP 失败时自动回退 CLI。

但修完之后我坐在那里想了一会儿。这个 fallback 逻辑应该在三天前就写好的。不,应该在 3 月 2 日那十个 commit 里的某一个就包含了。我当时为什么没写?

因为没有发生过。你不会给一个还没出过故障的系统写故障恢复逻辑——这是人类的一个结构性盲区。失败路径的回报是“什么都没有发生”,所以它在任何优先级排序中都会排在最后。


3 月 6 日,星期五上午:六个番茄消失了

ClawChat 的事还没消化完,番茄 App 出事了。

事情是这样的:番茄钟 App 的 token 过期了,需要重新登录。App 在重登流程里清除了本地缓存——包括一个叫 pendingSync 的数据结构。这个结构里存着 6 条已经完成但尚未同步到服务器的番茄钟记录。

清除 = 永久丢失。没有确认弹窗。没有警告提示。没有本地备份。6 条记录,大约 3 个小时的专注时间,安静地消失了。

我当时的第一反应是恼怒。第二反应是——我还记得那六个番茄分别是什么时候做的、做了什么。于是我打开 MySQL 客户端,手写 SQL 把它们一条一条补录回去。

补录成功了。但这件事让我后背发凉。成功的前提是三个条件碰巧同时满足:我还记得、数据量小到可以手动恢复、我会写 SQL。这三个条件任意一个不满足——比如丢了 60 个而不是 6 个,比如过了一周我才发现——数据就真的没了。

事后补录不应该是数据保护的手段。它是所有自动保护都失败后的最后手段。而我的系统里,它成了唯一的手段。


3 月 6 日,星期五下午:雪崩

番茄的事还在处理,后端彻底炸了。

番茄 App 的后端跑在阿里云一台 1 核 2GB 的 ECS 上,用的 Spring Boot + Tomcat。Tomcat 的默认线程池大小是 200——我当时设成了 120,觉得已经“调低了”。HikariCP 的连接池是默认值 10。

触发雪崩的直接原因是一条慢 SQL。这条 SQL 在正常情况下执行时间可接受,但当时数据库锁等待时间变长,单条 SQL 的执行时间从几十毫秒涨到了几秒。

然后是连锁反应:

  1. 慢 SQL 导致处理线程长时间阻塞
  2. 120 个线程迅速被占满
  3. 新请求全部排队等待
  4. 客户端没有退避逻辑,超时后立即重试
  5. 重试请求涌入,线程池更加堵塞
  6. 数据库连接池也被占满,新请求直接报错
  7. 系统进入完全不可用状态

整个过程大约十分钟。从第一条慢 SQL 到全面雪崩,十分钟。

修复过程里我学到了一个事实:对一台 1 核 CPU 的机器来说,120 个线程不是“调低了”,而是荒谬地高。单核 CPU 同一时刻只能执行一个线程,其余全在排队和上下文切换。合理值是 4 到 8 个。120 线程的 Tomcat 跑在 1 核机器上,就像一个只有一个窗口的银行叫了 120 个号——你以为叫得越多处理越快,实际上窗口一次只能服务一个人,其余 119 个人全在大厅里互相踩脚。

我从来没有想过要检查这个参数。在公司里,这是 SRE 或 DBA 的事。他们帮你做容量评估、帮你调参数、帮你设告警。等你一个人做项目时,你根本不知道有这层工作要做。


3 月 6 日,星期五晚上:数据裸奔

修完雪崩之后,我做了一件以前从来没做过的事:盘点所有项目的数据备份状态。

结果让我出了一身冷汗。

番茄的 MySQL 备份了,因为之前出过事。digital-assets 文档在 Gitee 上有 Git 备份。但 ClawChat 的对话数据——跑在腾讯云的 MySQL 上——完全没有备份。没有 mysqldump,没有快照,没有任何恢复手段。如果那台服务器的硬盘坏了,所有对话记录全量丢失。

OpenClaw 的本机配置和会话数据,备份状态不明。腾讯云服务器有没有开快照,我也不确定。

这就是“事故驱动型”数据保护的必然结果:出过事的地方有了防护,没出过事的地方一片裸奔。你以为自己是安全的,直到你花十分钟做一次盘点,发现到处都是洞。


3 月 7 日,星期六:清算日

第二天是周六。我花了一整天做了一件事:把这一周的所有痛点、修复过程和教训提炼成系统性认知。

不是写一篇“本周总结”那种流水账。是坐下来,把每一次刺痛拆开,问自己:这个问题是个案还是结构性的?如果是结构性的,最小化的防护原则是什么?

六个小时之后,出来了六份文档:

一、失败路径三条底线。 不追求覆盖所有失败路径——那是不可能的——但三条底线必须做到:用户数据不能静默丢失,上游变慢时不要把自己拖死,关键路径至少有一个 fallback。

二、单核小机器容量基线。 一张速查表:Web 线程池 4–8,DB 连接池 3–5,请求超时 10–30 秒,连接泄漏检测 10–15 秒。不是精确值,但比“框架默认值”安全一百倍。

三、自愈优先原则。 一个人运维时,自愈比监控更有价值。进程挂了自动重启,请求超了自动拒绝,上游不可达自动回退。给 AI 提需求时,默认先问“能不能做成自愈的”。

四、数据安全三级台阶。 第一级补齐空白(有 > 无),第二级确认有效(能恢复 > 有备份),第三级自治运转(无需人工干预)。手动补录是最后手段,不是常规手段。

五、人机协作分层验收。 AI 验代码正确性、功能可用性、线上真实性。人只验体验判断——用起来顺不顺、交互合不合理、下一步该做什么。AI 说“完成了”不等于真的完成了,需要部署后自动验证。

六、知识体系三级进化。 每日微进化(自动生成知识卡片),每周大进化(跨天复盘提炼原则),每月超进化(淘汰过时认知、合并散点为专题)。微进化产出原材料,大进化产出结构。没有大进化,微进化只是日记。


后记:速度与债务

回头看这一周,我想说的其实只有一件事。

AI 让开发变快了。这是真的。一个后端服务一天上线,一套知识体系一周建起来,这在两年前不可想象。但“跑通”和“跑稳”之间的差距没有被缩小。AI 加速了代码编写,但测试、环境适配、容量校准、失败路径设计,这些东西没有被等比例加速。

结果就是:你以三到五倍的速度制造系统,也以三到五倍的速度制造运维债务。每上线一个新服务,就是签了一份永久运维合同。开发时的兴奋会掩盖运维时的痛苦——写代码有即时反馈,运维的回报是“什么都没坏”。

3 月 2 日那十个 commit 的兴奋感,到 3 月 6 日的雪崩面前一文不值。

我不是要说“别用 AI”或者“慢下来”。减速是反人性的。我想说的是:速度之后,要有一个主动补课的环节。每个项目从“跑通了”进入“我开始依赖它”的阶段时,花半天时间,过一遍失败路径、容量参数、数据备份、自愈机制。

半天时间。能防住几个月的痛。

这一周我丢了六个番茄、经历了一次雪崩、发现数据在裸奔。作为交换,我得到了六份原则性文档和一个核心认知:

速度是杠杆。杠杆放大的不只是产出,也是风险。知道自己在负债,比负债本身更重要。

(完)