← 返回文章

6 次失败部署后,预发布仍影响了生产

6 篇系列的第 5 篇:改版按计划在准备就绪之前一直没有进入生产环境。部署到预发布 URL 却经历了 6 次失败、一颗可疑的 CPU、一个占用超过 50 GB 内存的上传步骤,以及对私有文件的隐性依赖。随后预发布数据填满了与生产共用的数据库,生产写入中断约 2 小时。

《用智能体重建 williamliu.ai》6 篇系列的第 5 篇。请从总览开始阅读。

要点

  • 🔒 只有我明确下令,生产环境才会改变;智能体可以随意破坏预发布环境(staging),而且确实破坏过。
  • 💥 前 6 次预发布部署因 4 种原因失败,其中一次让 CPU 固件成为嫌疑对象。
  • 🧹 干净检出(clean checkout)暴露出一些测试依赖只存在于我机器上的私有文件。
  • 🧮 部署的上传步骤占用内存膨胀至超过 50 GB,直到我的机器耗尽内存;改为本地构建再上传结果后,只用 290 MB。
  • 🗄️ 预发布环境和生产环境共用一个 256 MB 数据库;约 7 次预发布发布操作将它填满。
  • ⏱️ 生产写入失败约 2 小时,直到我批准的清理把数据库降至 158.5 MB。
  • 🚀 9 月 16 日,预构建预览无法提升到生产。push main 触发生产部署;随后重建并发布内容产物,使 Upstash 从 156 MB 增至 177 MB。

改版在准备就绪之前一直没有进入生产环境,确保这一点的安全规则发挥了作用;事故来自预发布环境和生产环境仍在共用的唯一一项资源。 本文讲的是:如何在不危及线上网站的前提下,把一次大型改版放到预发布 URL;那个最终还是出事的夜晚;以及改版最后如何进入生产环境。我会把细节完整写下来,因为教训就在这些细节里。

生产环境周围的安全层:任何生产部署都必须由我明确下令;预览位于受访问保护的独立 URL;预构建预览被记录为不可提升到生产;预发布内容使用独立的数据库命名空间。缺口是:这个命名空间与生产数据同处一个 256 MB 数据库。
fig 01 生产环境周围的安全层:任何生产部署都必须由我明确下令;预览位于受访问保护的独立 URL;预构建预览被记录为不可提升到生产;预发布内容使用独立的数据库命名空间。缺口是:这个命名空间与生产数据同处一个 256 MB 数据库。

🔒 规则:生产环境必须等我同意

智能体可以随意破坏改版的预发布预览;但没有我在对话中的明确指令,就不能 push 到生产分支,不能部署生产环境,也不能把预览提升到生产(promote)。 这是仓库智能体说明中的长期规则,改版规格文档(spec)也再次写明。里程碑进行期间,预发布环境可以坏;生产环境则停留在已知可用的版本,直到我另行指示。

有两项机制支撑这条规则。第一,预览使用受托管平台访问保护的独立 URL,测试套件用绑定到该确切 host 的绕过 token 访问。第二,智能体构建的部署路径会把预览记录为不可提升,因此不能意外推到生产环境。改版的构建、预览和部署测试全部在预发布环境或我的机器上进行。改版最终进入生产环境,是在这些事情之后,由我明确下令。

💥 6 次部署失败,4 种不同原因

Codex 第一次尝试把完成的改版部署到预发布环境时,连续 6 次失败;如果把它们当成同一个问题,反而会误判。 其中 3 次在 Python 的 YAML 解析器内部崩溃;1 次在嵌套构建中失败,输出不足,无法诊断;1 次败在访问授权测试;第 6 次进入托管平台的构建步骤后,无法启动构建所需的工具。

9 月 7 日的 6 次预发布部署失败:第 1、2、4 次在 Python 的 YAML 解析器内崩溃;第 3 次在嵌套构建中失败;第 5 次败在授权测试;第 6 次无法启动构建工具。
fig 02 9 月 7 日的 6 次预发布部署失败:第 1、2、4 次在 Python 的 YAML 解析器内崩溃;第 3 次在嵌套构建中失败;第 5 次败在授权测试;第 6 次无法启动构建工具。

Codex 保留了每一次失败日志,事实证明很有价值。当我要求它停止打磨、寻找根因时,日志显示了 4 个各不相同的问题。每个问题都需要不同修复和不同负责人。

🖥️ CPU 成为嫌疑对象时

Python 崩溃看起来像解释器内部出现内存损坏,旧版 CPU 固件因此成为一个合理嫌疑。 迹象很奇怪:一个理应始终是文本的值,返回时却变成整数,而且恰好是内存中紧邻位置存储的那个整数。错误随机发生在当时负载最高的测试文件里,单独重跑同一测试又能通过。

Codex 的根因调查先在机器上找到线索:从 9 月 2 日起,内核日志记录了 8 次 Python 段错误,而且都发生在同一个逻辑 CPU 上。Claude Code 随后继续深挖。我的台式机采用第 13 代 Intel Core i9,微码,也就是 CPU 自身的固件,版本是 0x10f。针对这些芯片的一个已知不稳定问题,Intel 建议使用 0x12F 或更高版本;我的版本更旧,因为 Linux 微码包曾经安装过,后来又被移除。我安装更新并重启,CPU 现在报告 0x133。

更新没有让崩溃消失。重启后的高负载期间,同样的迹象再次出现;第二天,一次预发布部署又因段错误终止。因此,CPU 仍然只是嫌疑对象,不是已证实的原因,而我至今没有找到确定根因。改变的是智能体如何解读一次绿色测试。主机如果可能算错,一次通过提供的证据就没有看起来那么强。现在,只有多项信号同时吻合,结果才算有效,包括真实退出码、traceback 数量为零,以及健康标记。

🧹 干净检出能找到本机掩盖的依赖

从干净代码副本部署,暴露出一些测试只有在我的工作站存在私有文件时才能通过。 预览构建会有意排除我未被 git 跟踪的私有播客草稿。没有这些文件,日期验证器会拒绝空的预览列表。有一个测试期待本地独有的文件夹,另有 2 组测试假设私有媒体和一个已安装命令都存在。修复涉及 6 个文件,没有改变任何发布逻辑。随后,一次全新检出通过了完整本地关卡,其中包括 401 项浏览器测试,另有 3 项按计划跳过。

上传步骤也带来意外。部署工具每次都会重新上传自己的 5.1 GB 构建缓存,共约 15,000 个文件。归档步骤的内存不断膨胀,超过 50 GB 后我的机器耗尽内存,只能由我手动终止。后来一次重试达到约 32 GB 时,被 Claude Code 停止。长期修复方案是不再上传源代码树:在本地构建一份干净检出,审计输出,只上传完成的网站。该路径的峰值内存约为 290 MB,现在一次预发布部署约需 1.5 分钟。

一次预发布部署的峰值内存:工具归档并上传源代码树时超过 50 GB,直到机器耗尽内存;上传本地构建完成的网站时约 290 MB。
fig 03 一次预发布部署的峰值内存:工具归档并上传源代码树时超过 50 GB,直到机器耗尽内存;上传本地构建完成的网站时约 290 MB。

🔑 受保护的预览是独立环境

预发布 URL 有自己的访问规则、数据和缓存,而这三项都让我们吃过亏。

  • 访问。 测试套件的绕过 token 必须绑定到准确的预览 host。网站媒体现在会重定向到独立媒体域名,改版后的测试传输层拒绝跟随这些重定向,因此 token 不会泄漏到其他 host。一个在重定向时删除 secret header 的小型运行器解决了测试问题,同时没有削弱这条规则。
  • 数据。 预览从预发布数据副本读取内容,却没有任何机制保持它最新。部署修复后的第一次完整预发布运行,在 889 项检查中失败 95 项,全部源于过期的预发布数据。修复方法是先把当前内容发布到预发布环境,再进行验证;这又引出了下文的事故。
  • 缓存。 预发布数据发布前,Claude Code 发出过一次诊断请求;它在边缘缓存中保留最长 1 天,导致预览持续返回过期答案。我们采用的修复是重新部署。现在的规则是:发布数据前,不得探测任何预览路由。

在那一轮修复中,加入 "View all" 修复的构建最后一次完整预发布运行通过了 2,475 项检查,没有失败。最后一项修复,也就是放大加号,通过了包含 343 项测试的本地浏览器关卡,重新部署后没有再跑一次完整预发布测试。

🗄️ 预发布导致生产写入失败的那一晚

一天内约 7 次预发布发布操作填满了与生产共用的数据库,约 2 小时内,所有写入都失败了,包括生产写入。 事情经过如下。

网站把已发布页面和不同版本的 feed 存在 Upstash Redis 数据库里。生产和预发布分别使用 prod: 与 stg: 命名空间,但都位于同一个容量上限为 256 MB 的数据库。每次完整的预发布操作都会写入全新一代的页面、博客和 feed 数据。单是内容最多的系列就有约 43 个定时 feed 版本,因此每次发布估计增加 20 至 30 MB。旧的预发布数据代不会自动删除。9 月 11 日,Claude Code 在每批修复后都发布到预发布环境,方便我逐批测试,当天共约 7 次。

9 月 12 日 UTC 05:00 左右,写入开始报错:ERR DB capacity quota exceeded. Threshold: 268435456 bytes, Usage: 269174008 bytes。数据库刚刚超过 256 MB 配额。数据库满后会拒绝所有写入,而生产命名空间也在其中。生产环境的页面浏览计数器、内容发布和定时发布激活都无法写入。发布工具只报告 HTTP Error 400: Bad Request。Claude Code 打印数据库返回的真实错误信息后,才找到原因。

事故期间的数据库大小:约 7 次预发布操作在 UTC 05:00 将其推过 256 MB 上限;获批的预发布数据清理将其降至 158.5 MB;再发布 1 次后升至 180.5 MB;第 2 次清理最终降至 155.3 MB。
fig 04 事故期间的数据库大小:约 7 次预发布操作在 UTC 05:00 将其推过 256 MB 上限;获批的预发布数据清理将其降至 158.5 MB;再发布 1 次后升至 180.5 MB;第 2 次清理最终降至 155.3 MB。

Claude Code 停下来问我如何处理。已有一个清理工具,但它会从共用数据库删除数据,必须得到我的批准。试运行发现 2,229 条不再被任何内容引用的预发布记录,仍在使用的有 437 条。我批准了仅清理预发布数据。清理本身因连接超时不断失败,因为客户端每条命令都会新建加密连接,所以任务分多轮运行。已中断轮次完成的删除仍然有效。UTC 07:05 左右,写入恢复,数据库降至 158.5 MB。下一次发布后升至 180.5 MB;第 2 次清理结束时降至 155.3 MB。此后,Claude Code 每次完成预发布发布都会运行清理。生产数据和媒体均未被触碰。

我能给出的最精确影响范围是:生产写入失败约 2 小时。该时段没有定时单集发布。那 2 小时的页面浏览计数可能已经丢失,我没有测量具体数量。

🧭 我做了哪些改变,又会怎样建议你

独立命名空间只隔离名称,不能隔离容量;共用数据库必须有容量检查、清理机制,以及能说清错误原因的信息。

  • 每次预发布后都清理。 这已经写进智能体说明。让发布工具自动执行清理仍是后续任务。
  • 发布前检查剩余容量。 如果可用空间不足以容纳一次发布,就拒绝执行。这仍是后续任务。
  • 显示真实错误。 发布工具应该打印数据库错误正文,而不是只有一个 400。这也是后续任务。
  • 如果预发布会保持这种发布频率,就给它独立数据库。 这才是真正的修复,而我还没有做。
  • 尽早从干净检出部署。 你的机器会掩盖部署时才能发现的依赖。
  • 保留每一份失败日志。 6 次失败最终对应 4 个问题。

🧪 上线前,预发布环境发现了什么

最终预发布失败都得到了解释,没有发现改版回归:15 项失败中有 12 项来自预发布数据不一致,其余 3 项是构建机器上的 TLS 握手超时。 产物命名空间仍带着旧的 "Writing" 标签,其中也包含仅存在于预发布环境的文章。3 项超时在重跑后消失。上线前,最终测试套件的结果因此是 2,380 项通过、15 项失败,而且失败原因都已解释。

预发布环境此前还暴露了另外两个边界。博客产物构建器的媒体基础 URL 即使在目标设为预发布环境时也默认指向生产环境,因此第一个预览用了生产媒体路径,必须改用预发布媒体基础地址重建。同一个构建器还会拒绝指向 /projects/ 和 /about/ 的链接,因为它的边界渲染中没有网站页面;我在预发布副本里把这些链接改成了绝对地址。这个缺口在上线时仍未解决,但系列文章尚未发布,因此没有阻塞网站。

🚀 9 月 16 日:上线靠 push,而不是 promote

安全规则依然成立,但上线机制与我构建的“预发布后提升到生产”路径不同:我下达指令,push main 便触发了生产部署。 预构建预览无法提升到生产,因为它被记录为不可提升:所用的预览 token 是仅供生产环境使用的 secret。在这个项目里,托管平台的 git 构建自行移动了正式别名。

这次部署更新了网站框架,却没有更新当前内容。播客、系列、单集、博客索引、feed 和站点地图都由已发布产物提供,因此 push 之后还必须按新设计重建,再发布到生产命名空间。整个过程尝试了 3 次。第 1 次停在生产页面构建器:它对 update-record 清单采取 fail-closed 策略,需要预发布构建器此前不需要的凭证。第 2 次在构造缓存清理器时停止,因为 VERCEL_TOKEN 已被移除;这发生在阶段 0 之前,所以没有写入任何内容。第 3 次成功;期间的 Upstash TLS 握手超时都在同一个请求 ID 下重试。

随后,部署验证发现生产页面路由集合不完整:改版新增了 /about/、/projects/ 和 /zh/ 路由。我补入 143 条路由,使总数达到 503 条;之后验证通过,线上内容与本地 public/ 输出一致。受关卡保护的生产预览入口仍未发布,因为它在 R2 中的媒体影子从未完成初始化;公开网站不受影响。生产内容发布使 Upstash 从 156 MB 增至 177 MB。已被取代的生产数据代不会自动删除,因此清理生产数据仍需我批准。

本系列最后一篇会把全部账目加起来:62.6 亿 token 到了哪里。

你的预发布环境是否与生产环境共用某项可能被填满的资源?

本文写作方式:由 Claude Code 根据我本人的会话日志起草,另一个 Codex 智能体对照同一批日志核对事实,最后由我修改定稿。封面插图由 AI 生成;图表依据文中数字绘制。