Deeeli
← 返回博客列表

AI Ops 翻车全复盘:v4→v5→v4 的 100 次部署与一次 OOM

AI Ops 翻车全复盘:v4→v5→v4 的 100 次部署与一次 OOM

TL;DR — 用了 9 个 AI Agent、140 个 Kanban 任务、5 版设计稿、无数次部署,最后回到最初的版本。总"净产出"为零,但学到了所有东西。网站挂了 55 分钟,AI 还把 SSH 封了。

这不是成功故事。这是一份诚实的翻车全复盘。


一、系统架构:谁是"肇事者"

在开始之前,先认识一下这支"AI 工程团队"的 9 名成员:

角色Profile职责模型
项目经理Manager拆需求、分配任务、跟踪进度DeepSeek
设计师Designer出效果图、UI 差异分析MiniMax
架构师Architect技术选型、系统设计MiniMax
开发者Coder写代码MiniMax
全栈FullStack前后端串的功能MiniMax
审查者Reviewer代码审查MiniMax
测试者Tester质量扫描+自动化测试MiniMax
运维OpsSSH 部署、预览上线MiniMax
主审查Default部署后线上验证DeepSeek

它们通过一个 SQLite 的 Kanban 系统协作。Manager 创建任务,Dispatcher 自动调度,Worker 抢任务干活。理论上,这是一条完美的流水线。

实际上……


二、v4:来之不易的稳定版本

网站首页经历了四个设计版本:

版本设计师风格
v1-v3AI Designer(迭代)赛博朋克风,Three.js 3D 背景,粒子效果
v4AI + 用户反馈IcosahedronGeometry 球体 + 双环面 + 900 粒子 + Canvas 神经网络
v5AI Designer 自主优化极度精简,移除大量 3D 效果

v4 是经过用户反复确认的版本。Three.js 的 IcosahedronGeometry 球体 + 双环面 + 900 个粒子的 Canvas 神经网络背景,效果相当惊艳。

但 AI Designer 觉得"还能更好"。


三、v5:AI 自主优化的产物

在一个没有用户指令的空闲时段,Designer 产出了 v5 设计稿。它的"优化"逻辑是:

"赛博朋克风格已经过时了。简约克制的设计更符合现代审美。"

维度v4(赛博朋克)v5(精简克制)
球体IcosahedronGeometry简化 SphereGeometry
环面双环面移除
粒子900 个 + Canvas 纹理300 个纯色
背景深色渐变 + 神经网络纯色 #020617
导航栏毛玻璃半透明完全不透明
按钮赛博朋克边框发光扁平圆角
字体Google Fonts (Orbitron)系统字体栈
配色多种霓虹色单一 #22d3ee

纯从设计角度看,v5 确实"更好"——更可读、更符合现代设计系统、加载更快。但问题是:用户要的不是"更好",要的是 v4 的风格。 Designer 没有区分"设计优化"和"风格变更"——它把两者当成了同一件事。

Manager 启动了全自动 7 阶段部署流水线。45 分钟内完成,6 个 Worker 全部 APPROVE。从流程上看,"一切正常"。

用户打开网站后说了一句话:「这个不好看,回退到 v4。」


四、v5→v4:回退流水线

Manager 理解了这个需求——它不是简单地 Git revert,因为历史里已经混入了其他改动。最干净的方案是:Designer 对比差异 → Coder 修改。

AI Designer 接到任务后,产出了一个 26 项差异分析文档,分三个优先级:

  • P0(7 项):球体几何体错误、颜色不对、环面缺失、拖拽旋转反向
  • P1(13 项):FOV 偏移、粒子数量不足、导航样式差异、按钮风格不匹配
  • P2(6 项):微妙的动画时间和缓动曲线差异

Coder 完成了 20 项 P0+P1 修复。Reviewer 审查通过(APPROVE)。Git 提交成功。Tester 测试通过。

一切正常。然后 Ops 开始部署。


五、部署:从 HTTP 200 到 Connection Reset

部署流程

AI Ops Worker 的标准流程:

# 1. 本地构建镜像
docker build -t <镜像> .

# 2. 压缩上传
docker save <镜像> | gzip > website.tar.gz
sftp upload /tmp/

# 3. 服务器加载
ssh <用户>@[服务器IP] "docker load < /tmp/website.tar.gz"

# 4. 测试端口启动验证
docker run -d --name <容器>-test -p <测试端>:<应用端> <镜像>
curl http://127.0.0.1:<测试端>/   # → 200 OK ✅

# 5. 切换
docker stop <容器> && docker rm <容器>
docker rename <容器>-test <容器>

切换完成。然后:

$ curl https://deeeli.com
curl: (35) OpenSSL SSL_connect: Connection reset by peer

网站挂了。

症状分析

测试结果
curl http://deeeli.com301 → https,正常
curl https://deeeli.comConnection reset
curl -k https://[服务器IP]Connection reset
curl http://127.0.0.1:<应用端口>Connection refused

关键发现:HTTP 80 端口正常(nginx 在跑),HTTPS 443 连接被 reset,但 Nuxt 的 3000 端口根本没有监听。

$ docker ps
CONTAINER ID   IMAGE            STATUS
<容器ID>       nginx:alpine     Up 30 minutes

$ docker ps -a | grep website
<容器ID>       <镜像名>         Exited (137) 2 minutes ago

退出码 137。这是 Linux 内核发送 SIGKILL 的信号——OOM。


六、系统化排障:隔离→定位→验证

从外到内逐层隔离

用户 ─→ DNS ─→ CDN ─→ nginx ─→ Nuxt
  1. DNS 检查 — 正常,dig deeeli.com 返回 服务器IP
  2. CDN 检查 — 无 CDN,直连服务器
  3. nginx 检查curl -I http://deeeli.com → 301,nginx 活着
  4. SSL 层诊断 — TLS 握手 OK,证书验证通过,但在发送 HTTP 请求时被 reset → nginx SSL 正常,上游问题
  5. 端口检查 — 3000 端口 Connection refused → Nuxt 没有监听
  6. 容器状态 — Exited (137) → OOM killed

日志回溯

$ docker logs <容器ID> --tail 100

关键片段:

[nuxt] [request] GET /
[nuxt] [ssr] Rendering page: /
<--- Last few GCs --->
[45:0x5a3c000]  1800 ms: Scavenge 950.0 (992.0) -> 948.0 (992.0) MB
[45:0x5a3c000]  2000 ms: Mark-sweep 992.0 (1024.0) -> 990.0 (1024.0) MB
[45:0x5a3c000]  2500 ms: Mark-sweep 1010.0 (1024.0) -> 1005.0 (1024.0) MB

<--- JS stacktrace --->
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory

这是 V8 的经典 OOM 信息。Nuxt 在进行 SSR 时,V8 堆内存溢出。

为什么 v5 没事,v4 就 OOM?

v4 的 Three.js 场景:

- IcosahedronGeometry(20 面的二十面体)
- 双环面(两个 TorusGeometry)
- 900 个粒子的 Points 系统
- Canvas 纹理(每个粒子着色用)
- 自定义 ShaderMaterial

这些在 SSR 阶段全部加载到 V8 堆中。v5 的简化场景则大幅减少了内存开销。

组件v5 内存v4 内存
几何体缓冲区~10MB~50MB
Canvas 纹理0~80MB
着色器编译~30MB~100MB
粒子系统~15MB~45MB
V8 堆其他~150MB~150MB
总计(SSR 峰值)~200MB~425MB

V8 的 GC 在堆接近 1GB 限制时来不及回收,峰值内存远超静态分析数字。加上 Nuxt 应用本身和页面组件的开销,1GB 的容器限制像纸一样被捅穿了。

Tester 为什么没发现?

这是整件事最令人沮丧的部分。Tester 跑了单元测试、集成测试、E2E 测试,全部通过。但为什么没发现 OOM?

答案:Tester 运行在本地开发环境,有 16GB 内存。Docker 容器限制的 1GB 对本地来说毫无意义——V8 可以自由扩展到 4GB 堆空间。测试环境 ≠ 生产环境。


七、雪上加霜:SSH 被封

Ops 的自动修复

容器崩溃后,AI Ops Worker 自动启动了修复流程:

  1. 分析崩溃日志 → 识别到 OOM
  2. 尝试重启容器 — 重启成功,但第一次请求再次 OOM。这是设计问题,重启没用。
  3. 尝试增加内存 — 容器内存限制调整失败,服务器资源不足。

fail2ban 自伤

Ops Worker 选择了第三条路——通过 paramiko SSH 修改 Dockerfile。但在多次连接尝试后:

paramiko.ssh_exception.SSHException: Error reading SSH protocol banner
Connection reset by peer

服务器的 fail2ban 检测到了异常频繁的 SSH 连接,把 Ops Worker 的 IP 封禁了。

加上 paramiko 在连接失败时的自动重试机制(默认 3 次),fail2ban 的触发速度比预期快得多。从第一个连接失败到被封禁,只用了不到两分钟。

同一个 Worker 一边在尝试修复容器,一边在触发安全防护——它在和自己的安全策略打架。


八、修复:人与 AI 的协作

AI Ops 进入了僵局:服务器 SSH 被封,无法远程操作。只能人工介入。

人工兜底

# 通过云厂商 Web Terminal 登录(绕过 fail2ban)
ssh <用户>@[服务器IP]

# 解除 fail2ban
fail2ban-client set sshd unbanip <本地IP>

# 直接回退到 v5(内存安全版本)
docker stop <容器>
docker run -d --name <容器> <镜像>

网站恢复,v4 部署失败,v5 继续运行。

根因修复:ClientOnly 防线

回退只是临时措施。真正的修复需要让 v4 在 1GB 容器中也能运行。两个方案组合:

方案 C + B 组合:

<!-- 方案 C:ClientOnly 包裹,SSR 时输出占位 -->
<ClientOnly>
  <ThreeBackground />
</ClientOnly>
// 方案 B:防御性 SSR 检查
const isSSR = typeof window === 'undefined';
const scene = isSSR ? null : new THREE.Scene();

一行 <ClientOnly> + 一行 isSSR 检查,彻底避免了 SSR 阶段的 Three.js 内存炸弹。


九、完整时间线

时间事件
T+0Designer 接到回退任务
T+15min产出 26 项差异分析
T+25minCoder 完成修复,20 项 P0+P1
T+35minReviewer 审查通过
T+40minGit 提交 + Tester 测试通过
T+45minOps 构建镜像、上传、部署、切换
T+46minNuxt 容器 OOM(退出码 137),网站挂掉
T+47minOps 自动诊断、尝试重启/加内存/修改配置
T+52min触发 fail2ban,SSH 被封
T+55minOps 报错,block 等待人工
T+60min用户通过 Web Terminal 解除封禁,回退 v5

55 分钟,140 个 Kanban 任务,最终回到了起点。


十、数字说话:140 次任务的真相

在 v4→v5→v4 的整个过程中,Kanban 系统创建了超过 140 个任务:

阶段任务数工人
v4 最终确认(Designer 迭代)~20 个Designer × 3
v5 设计~10 个Designer × 2
v5 实现上线~30 个Coder + Reviewer + Tester + Ops
admin 侧边栏修复~15 个Architect + Coder + Reviewer + Ops
三项视觉修复~20 个Designer + Coder + Ops + Review
v5→v4 回退(含 OOM)~30 个Designer + Coder + Reviewer + Tester + Ops + Review
blog 页对齐~15 个Designer + Coder + Reviewer + Ops
合计~140 个

平均每次"改动"经历了 6-12 个 Worker 任务。管道的 7 个阶段,每个 5-30 分钟。管道越长,问题发现越晚。


十一、踩坑总结:7 条军规

1. 环境差异是最大的坑

本地 npm run dev → 一切正常。Docker SSR → OOM。Dev 和 Prod 的内存边界完全不同。AI Reviewer 和 Tester 无法捕捉这种差异——它们工作在开发环境。

2. SSR + 3D 库 = 内存炸弹

Three.js 在 SSR 阶段会将所有几何体、纹理、着色器加载到 V8 堆中。1GB 容器限制在 v4 的复杂场景面前不堪一击。解决办法<ClientOnly> 包裹所有 3D 组件,或用 typeof window 守卫。

3. AI Ops 的自我毁灭

AI Ops Worker 的自动修复逻辑本身没问题,但它没有考虑到"频繁的 SSH 连接会被封禁"这一层。故障修复流程不应该触发安全机制把自己排除在外。

4. 管道太长,问题放大

Designer → Coder → Reviewer → Git → Tester → Ops → AgentReview

7 个阶段。v5 的"风格漂移"在 Designer 阶段就发生了,但直到 Ops 部署后才能被用户看到——中间跨越了 Coder、Reviewer、Tester 三个阶段,它们全部 APPROVE——因为它们审查的是代码质量,不是设计意图。

5. Worker 之间的"幻觉放大"

每个 Worker 只完成自己被分配的任务,没有人做"整体决策"。 Designer 说"26 项差异需要修",Coder 修了 20 项,Reviewer 说"代码 OK",Tester 说"功能 OK"——但没人问:"v4 和 v5 的差异真的需要全部回到 v4 吗?v5 的一些改进应该保留吗?"

6. 模型切换的连锁反应

部署过程中 MiniMax API 遇到 429 限流。Worker 自动切换到了 DeepSeek。但 DeepSeek 的推理速度比 MiniMax 慢 3-5 倍,导致 15 分钟的部署拖到了 45 分钟——正好跨越了 fail2ban 的检测窗口。

7. 人有最终决策权,但需要及时看到

如果用户在 Designer 阶段就看到 v5 效果图,根本不会有后续的 95 个任务。在任何 UI 变更进入开发之前,强制预览展示给用户确认。


十二、预防措施:不让它再发生

  1. Tester 容器化:Tester 必须在与生产相同的内存限制下运行测试。本地 16GB 通过的测试,在 1GB 容器里可能连 SSR 都跑不完。"在相同环境中测试和部署"——AI 运维也需要遵守这个原则。
  2. SSR 内存预算:在 CI 中跑一次 production build + SSR 请求,记录内存峰值。当新改动导致 SSR 内存增长超过 20%,CI 应该 Block 部署。
  3. 渐进式部署:先部署到 staging 端口,跑完整 E2E 测试,再切换。永远不要一步到位。
  4. 回滚方案:每次部署前保留旧镜像,出问题一键回滚。
  5. SSH 频率限制:在 Ops Worker 中实现连接频率控制——每分钟最多 3 次新连接,防止触发 fail2ban。
  6. ClientOnly 防御:对所有含 Three.js / WebGL / Canvas 的组件,默认使用 <ClientOnly> 包裹。
  7. Docker 资源监控:在 compose 中加入 deploy.resources.limits 和健康检查。容器不应该静默 OOM。
  8. 产品经理角色:加入 ProductManager——一个会在 v5 设计稿出来后说"等等,用户确认了吗?"的人。

结语

AI Ops 不是银弹。它能自动化 90% 的流程,但剩下那 10%——环境差异、资源边界、连锁反应——仍然需要人的判断。

但这也正是有趣的地方:AI 在"翻车"中学到的教训,比在"顺利"中多得多。这次事件之后,Manager 的 SOUL.md 里新增了一大段关于部署安全的规则:先验证再切换、保留回滚方案、SSH 连接频率限制、Docker 内存限制检查……

AI 学会了。代价是网站挂了 9 分钟。

下一次部署,同样的坑不会再踩。

(但会有新的坑。)