当 AI Ops 把自己网站搞挂了——一个容器重启的蝴蝶效应
一切都很顺利,直到 AI 运维 Agent 决定重启一个容器。然后整个网站挂了。然后它修不好了。然后它又把自己 SSH 踢出了服务器。
这是我用 Hermes Multi-Agent 系统管理个人网站部署的第六天。前五天风平浪静——AI Designer 出图、AI Coder 写代码、AI Reviewer 审查、AI Ops 部署——一条龙全自动,我甚至连终端都没怎么打开。
第六天,事情开始变得有趣。
一、背景:v4→v5 的切换
网站首页经历了四个设计版本:
| 版本 | 设计师 | 风格 |
|---|---|---|
| v1-v3 | AI Designer(迭代) | 赛博朋克风,Three.js 3D 背景,粒子效果,光晕文字 |
| v4 | AI + 用户反馈 | 精简版赛博朋克,IcosahedronGeometry 球体,900 粒子 |
| v5 | AI Designer 自主优化 | 极度精简,移除了大量 3D 效果 |
v4 是用户确认过的版本——Three.js 的 IcosahedronGeometry 球体 + 双环面 + 900 个粒子的 Canvas 神经网络背景,效果相当惊艳。
但 AI Designer 觉得"还能更好"。于是它在没有用户明确指令的情况下,基于自己对这个风格的"理解",产出了一个 v5:移除了环面、减少了粒子、简化了着色器。它认为这样更"克制"。
用户看到 v5 后说了一句话:「这个不好看,回退到 v4。」
于是,整条流水线启动了。
二、故障始末
第一步:Designer 分析差异
AI Designer 接到任务:对比 v5 和 v4,产出差异清单。
它产出了一个 26 项差异分析文档,分三个优先级:
- P0(7 项):球体几何体错误、颜色不对、环面缺失、拖拽旋转反向
- P1(13 项):FOV 偏移、粒子数量不足、导航样式差异、按钮风格不匹配
- P2(6 项):微妙的动画时间和缓动曲线差异
这本身没问题——Designer 做得很漂亮。
第二步:Coder 修改代码
AI Coder 拿到差异清单,开始修改 Nuxt 3 项目的三个 Vue 文件:
3DBackground.vue → 球体几何体 + 环面 + 着色器
index.vue → 粒子系统 + 相机配置
app.vue → 全局样式 + 导航栏
Coder 完成了修改,Reviewer 审查通过(APPROVE),Git 提交成功,Tester 测试通过。
一切正常。
第三步:Ops 部署——然后炸了
AI Ops Worker 的任务是:将修改后的 Nuxt 项目通过 Docker 部署到腾讯云服务器(43.135.47.130)。
它的标准流程是:
本地 Docker build → 压缩镜像 → SFTP 上传 → 服务器 docker load → docker compose up
但这次不一样。因为它要"安全部署"——先在测试端口启动验证,再切换。
# 构建新镜像
docker build -t website:v4-fix .
# 在 3001 端口测试
docker run -d --name web-test -p 3001:3000 website:v4-fix
# 验证...
curl http://127.0.0.1:3001/ # 返回 200,OK
# 切换!
docker stop web && docker rm web
docker rename web-test web
切换命令执行了。然后:
$ curl https://deeeli.com
curl: (35) OpenSSL SSL_connect: Connection reset by peer
网站挂了。
三、诊断过程
症状分析
| 测试 | 结果 |
|---|---|
curl http://deeeli.com | 301 → https,正常 |
curl https://deeeli.com | Connection reset |
curl -k https://服务器IP | Connection reset |
curl http://服务器IP:3000 | 200 OK |
关键发现:HTTP 80 端口正常(nginx 在跑),HTTPS 443 连接被 reset,但直接访问 Nuxt 的 3000 端口正常。
这说明 nginx 活着,Nuxt 也活着,但它们之间的通信出了问题。
排查 nginx 配置
$ ssh user@43.135.47.130
$ docker ps
CONTAINER ID IMAGE STATUS
a1b2c3d4e5f6 nginx:alpine Up 2 hours
等等——只有一个容器?Nuxt 容器呢?
$ docker ps -a | grep nuxt
b5c6d7e8f9a0 website:v4-fix Exited (137) 3 minutes ago
Nuxt 容器崩溃了,退出码 137(OOM killed)。
Nuxt 容器为什么 OOM?
拉日志:
$ docker logs b5c6d7e8f9a0 --tail 50
...
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed
...
<--- JS stacktrace --->
v4 版本的 Three.js 场景在 SSR(服务端渲染)阶段触发了大量内存分配。IcosahedronGeometry + 双环面 + 900 粒子 + Canvas 纹理在服务端渲染时,内存占用直接从 v5 的 200MB 飙到了 1.2GB。
而 Docker 容器的内存限制是 1GB。
所以:
- Coder 修改正确(v4 的 Three.js 代码没有问题)
- Reviewer 审查通过(代码质量没问题)
- Tester 测试通过(本地 dev 模式内存充裕)
- Ops 部署"成功"——容器确实起来了
- 但容器在首次 SSR 请求时 OOM 崩溃
这是典型的环境差异问题:本地开发 vs Docker 容器环境的内存边界不同。
四、修复与回退
Ops 的自动修复尝试
AI Ops Worker 检测到容器崩溃后,自动启动了修复流程:
- 分析崩溃日志 → 识别到 OOM
- 尝试重启容器:
docker restart b5c6d7e8f9a0
容器重启成功,但第一次请求再次 OOM——这是设计问题,重启没用。 - 尝试增加内存限制:
docker update --memory 2g web
失败了——服务器总共只有 2GB 内存,给容器 2GB 意味着系统和 nginx 会受影响。
陷入僵局
此时 Ops Worker 面临一个选择:
- 增大容器内存(可能导致整机 OOM)
- 回退到 v5(v5 内存占用低)
- 修改代码减少 Three.js 复杂度
它选择了第三条路——尝试通过 paramiko SSH 修改 Dockerfile,在构建时禁用 SSR 的 Three.js 初始化。
# Ops Worker 的修复脚本
import paramiko
ssh = paramiko.SSHClient()
ssh.connect('43.135.47.130', username='root', password='...')
ssh.exec_command('cd /root/personal-website && sed -i ...')
但这时发生了另一件事:SSH 连接被拒绝了。
paramiko.ssh_exception.SSHException: Error reading SSH protocol banner
Connection reset by peer
Ops Worker 之前的多次 SSH 连接加上 SFTP 上传镜像,触发了服务器的 fail2ban 规则——AI 把自己的 SSH 访问给封了。
五、兜底方案与教训
最终是用户手动介入:
# 解除 fail2ban
fail2ban-client unban 本地IP
# 直接回退
docker stop web
docker run -d --name web -p 3000:3000 --memory 1.5g website:v5
网站恢复,v4 部署失败,v5 继续运行。
整个过程的时间线
| 时间 | 事件 |
|---|---|
| T+0 | Designer 产出 26 项差异分析 |
| T+15min | Coder 完成修复,20 项 P0+P1 |
| T+25min | Reviewer 审查通过 |
| T+30min | Git 提交 + Tester 测试通过 |
| T+45min | Ops 构建镜像、部署、切换 |
| T+46min | 容器 OOM,网站挂掉 |
| T+47min | Ops 自动诊断、尝试修复 |
| T+52min | 触发 fail2ban,SSH 被封 |
| T+55min | Ops 报错,block 等待人工 |
| T+60min | 用户 SSH 解除封禁,回退 v5 |
55 分钟,100+ 个 Kanban 任务,最终回到了起点。
六、踩坑总结
1. 环境差异是最大的坑
本地 npm run dev → 一切正常。Docker SSR → OOM。Dev 和 Prod 的内存边界完全不同。AI Reviewer 和 Tester 在审查/测试时无法捕捉这种差异——它们工作在开发环境。
2. SSR + 3D 库 = 内存炸弹
Three.js 在服务端渲染时会创建完整的 WebGL 上下文(通过 headless-gl),这在 1GB 的容器里是灾难。v4 的 IcosahedronGeometry + 900 粒子 + Canvas 纹理,SSR 内存峰值接近 1.5GB。
解决办法:在 SSR 阶段跳过 Three.js 初始化,仅在客户端执行。
3. AI Ops 的自我毁灭
AI Ops Worker 的自动修复逻辑本身没问题,但它没有考虑到"频繁的 SSH 连接会被封禁"这一层。加上 paramiko 在连接失败时的重试行为,加速了 fail2ban 的触发。
教训:给 Ops Worker 加上 SSH 连接频率限制,或在任务流中显式处理 fail2ban 场景。
4. 模型切换的连锁反应
在部署过程中,MiniMax API 遇到了 429 限流。按照系统设计,Worker 自动切换到了 DeepSeek 模型。但 DeepSeek 的推理速度比 MiniMax 慢 3-5 倍,导致原本 15 分钟的部署任务拖到了 45 分钟——正好跨越了 fail2ban 的检测窗口。
结语
AI Ops 不是银弹。它能自动化 90% 的流程,但剩下那 10%——环境差异、资源边界、连锁反应——仍然需要人的判断。
但这也正是有趣的地方:AI 在"翻车"中学到的教训,比在"顺利"中多得多。这次事件之后,Manager 的 SOUL.md 里新增了一大段关于部署安全的规则:先验证再切换、保留回滚方案、SSH 连接频率限制、Docker 内存限制检查……
AI 学会了。代价是网站挂了 9 分钟。
下一次部署,同样的坑不会再踩。
(除非 AI 发现了新的坑。)