247 次。
一个容器一天被 cgroup OOM 杀了 247 次。每次 kill 加 supervisord 拉起新进程,22 秒的启动窗口里所有 API 调用都在超时。累计掉线时间超过 1.5 小时。而这只是冰山一角——整个故障链条里还有八个独立的缺陷在同步放大。
2026-05-24 下午 17:30 起,大量用户反馈掉线。表面上是三种完全不同的症状:网关无响应、插件拿不到 token、WebSocket 频繁断线重连。如果顺着单一假设深挖——比如"是不是 token 服务故障"——永远也理不清楚。症状看起来毫无关联,但其实源于同一条触发链的不同环节失效。
为什么症状分散,根因却统一
SSH 进服务器第一件事:
sudo dmesg -T | grep "Killed process.*openclaw" | head -20
输出:
[21:35:14] Killed process 1409836 (openclaw) total-vm:1570176kB, anon-rss:512000kB
[21:35:15] Killed process 1409924 (openclaw)
[21:35:52] Killed process 1413381 (openclaw)
2 分钟 15 次 kill。内存限制 500MB 的容器在高负载下不够用,系统开始清理。gateway 进程死后,supervisord 重启它,22 秒内启动完成才能服务请求。这个窗口里 panel API 全部超时。现象一:网关掉线。
但这还解释不了"token 过期"。进容器看日志,大量这样的:
[21:39:39] [reload] config watcher error: EMFILE: too many open files
[21:44:10] [skills] watcher error: EMFILE: too many open files
同时 registerCpaConnectorMcp 失败。查 DB 对比,容器的 openclaw.json 里的 token 与数据库的 active token 不一致。这说明配置写入本身出了问题。
检查代码 apps/api/src/connector/mcp-register.ts:
if (r.exitCode !== 0) {
logger.warn({...}, 'mcp register exited non-zero');
return { status: 'error', reason: `exit=${r.exitCode}` };
}
openclaw CLI 用 exit code 1 表示"完成但有警告"。代码把所有非零当失败。虽然 stderr 里有 Config overwrite 痕迹(文件其实已写入),但这个误判触发了 clearReconcileCooldown,下次 WebSocket 重连时又重试一遍。每次重试就是一次 SIGTERM。最终统计:1366 次 mcp register 报失败(大部分是假失败),对应 1366 次虚假 SIGTERM。现象二:网关频繁掉线。
现象三:WebSocket 4-9 分钟就断一次。查 CDN 日志,Tencent EdgeOne 对 idle 连接有强制切断策略。客户端自动重连,形成流量风暴。
三个症状,三个独立的源头,但都指向同一条链路的不同节点失效。正因为如此,修复不能是单一 patch——需要同时拆除这条链上的所有地雷。
九个缺陷与它们的叠加效应
做完全景取证后,缺陷清单出现了:
- B1 容器内存限制 500MB 严重不足 → gateway 进程 OOM kill → 22s 启动窗口 → panel 调用超时
- B2 CLI exit code 1 被误判为失败 → 1366 次虚假 SIGTERM → 指数级恶化掉线
- B3 ulimit nofile=1024 太低 → EMFILE → file watcher 失败 → 配置无法热重载
- B4 mcp reconcile cooldown 在 skip 时也被清空 → 每次 WS 重连重试 → 无效 SIGTERM 更多
- B5 registry kick-on-replace → 多客户端互踢 → 工具调用中途断裂
- B6
/instance/provision不幂等 → 新用户注册失败 → 14 次 AlreadyExists 错误/天 - B7 event loop 阻塞 7-18 秒 → tools/call 5 分钟超时 → 整机压力指数级增加
- B8 CDN 对 WebSocket idle 4-9 分钟强制切断 → 客户端循环重连 → 流量风暴
- B9 7 个用户仍跑老 EXE → 跟 Tauri 1.0.8 共存互踢 → 特定用户反复掉线
单独看,B3(文件描述符限制)只会导致某些 watcher 失败。B4(cooldown 清空)只是重试次数多一些。但当 B1(内存溢出)把整机资源挤到极限时,B2(虚假 SIGTERM)的流量放大、B4 的无限重试、B8 的 CDN 断线形成了一个正反馈循环。12GiB swap 活跃交换,主线程不断触发 page fault,event loop 阻塞最高达 18.8 秒。整个服务器的 system load 从正常的 2-3 飙升到 18.96。
不止受害的那个 247 次 OOM 的容器宕掉了。所有 372 个容器的网关进程都在 event loop 阻塞中挣扎,形成连锁故障。
凌晨部署与并行推进
修复分两条线并行。
基础设施层(立即上线):部署 WebSocket 直连入口,绕过 CDN 的 idle timeout。DNS 添加新直连域名的 A 记录,Caddy 仅暴露 /api/connector/ws 和 /health。客户端 1.0.9 OTA 时读 ws_endpoint 字段,动态切换通路。
业务代码层(业务低谷部署):五个 patch,530 行改动:
- B1:500MB → 2GB hard limit
- B2:exit code 判定加 stderr marker 检查
- B3:ulimit nofile 1024 → 65536
- B4:cooldown skip 时不清
- B6:
/instance/provision加 AlreadyExists 幂等处理
P1 级(稍后):B5 改为拒绝新连接而非踢掉旧连接;B9 的老 EXE 用户做 revoke 和自检。
凌晨 02:00 开始。前置备份 30 分钟(PG、repo、Caddyfile、image、swarm spec、用户数据)。
阶段 1 代码部署:pnpm build,rsync dist 到服务器,原子切换(mv),restart cpa-api。全程 5 秒,用户感知的掉线比预估的 30 秒更短。
阶段 2 监控验证:部署后 5 分钟内检查指标。B2 生效(1366 失败 → 9h 内 132)、B4 生效(cooldown 日志出现)、B6 生效(新注册零错误)。全部通过。
阶段 3 滚动升级:优先升级 6 个重灾户(含 247 次那个),5.5 分钟完成。升级后检查 dmesg,无 OOM kill。剩余 ~366 个容器一次 10 个并行,间隔 30 秒。单容器 docker service update 耗时 60 秒(含 swarm scheduler delay)。总耗时 6h42min(预估 3 小时,实际慢一倍)。但系统压力从升级到 ~80 个容器时就开始回落。
0 failed,0 回滚。
数据说话
部署 24 小时后对比:
| 指标 | 部署前 | 部署后 9h | 改善 |
|---|---|---|---|
| 容器 OOM kill(重灾户) | 247 次/天 | 0 | 100% |
| Swap 使用 | 12 GiB | 9 MiB | -99.9% |
| System load(1min) | 18.96 | 8.46 | -55% |
| 可用内存 | 58 GiB | 95 GiB | +64% |
mcp register 真失败 | 1366/天(混+假) | 132/9h | ~88% ↓ |
mcp register 假失败救活 | 0 | 26 次 | 新指标 |
| AlreadyExists 新用户错误 | 14/天 | 0 | 100% |
connection replaced 工具中断 | 18/天 | 0 | 100% |
tools/call exception | 35/天 | 2 | ~95% ↓ |
用户反馈中"网关掉线"消失,"插件用不了"归零。
防回归与巡检
五个 patch 配套 19 个单元测试 case(vitest,135ms 跑完),覆盖 B1/B2/B3/B6 的临界条件。新增 PG 自动备份 cron(每日 03:00),swarm spec 全量快照存档用于灾难重建。部署 checklist 和回滚脚本纳入标准流程。
但多缺陷叠加的"系统级阈值"仍缺持续巡检——这不能用传统的单点告警解决。需要在后续单独设计。
调查多缺陷故障的方法
多缺陷叠加时,症状看起来统一但根因分散。不能顺着单一假设深挖(比如假设"一定是 token 问题"),会被带进死胡同。正确的做法:
- 全景取证:把所有异常信号(OOM、EMFILE、token 不一致、event loop 阻塞、WebSocket thrash)列出来,不预判因果关系。
- 分类而非追踪:不要从某个症状开始追链条,而是把信号按系统层级分组,看哪些是独立根因、哪些是被放大的现象。
- 按影响面排优先级:单个 bug 的修复收益不一定最大。有时修复跨层级放大的系统级缺陷(比如错误的 exit code 判定 + 无 cooldown 保护)的收益,远超修复看起来最直接的单个 bug(比如内存限制)。
这次故障的真正杀伤力不来自 B1 的内存溢出,而来自 B2(虚假 SIGTERM)+ B4(cooldown 清空)的组合——它们把单个容器的故障放大成了全站连锁反应。
■