OpenClaw Gateway 反复断连十几次?V8 GC 内存上限引发的周期性自毁重启排查实录
从 session subscription recovery failed 追到 max-old-space-size 的完整根因链

症状
Gateway 一整天反复断连,前后十几次。前端弹的错误是这样的:
session message subscription recovery failed
gateway request timed out after 30000ms: sessions.messages.unsubscribe
CLIENT_TIMEOUT
gateway request timed out after 30000ms: sessions.messages.subscribesessions.messages.subscribe/unsubscribe 是 Gateway 内部事件订阅的 RPC。它超时 30 秒,说明问题不在网络,而在 Gateway 进程本身。
第一层:先确认不是错觉
我一开始只看了 openclaw gateway status,结果显示服务活着,uptime 30 分钟,于是判断链路正常。这个判断是错的。
gateway status 只能看当前态,看不到刚刚发生过的重启。要判断"是不是刚刚重启过",必须查日志时间线。
正确的第一层检查是三件事并行:
openclaw gateway status
journalctl --user -u openclaw-gateway --since "1 hour ago" --no-pager | tail -n 80
ps aux | grep openclaw | grep -v grep日志一出来,真相就清楚了。10:34:56 WebSocket 断开,code=1012 reason=service restart;10:35:49 restart mode 是 full process restart;10:36:58 新进程启动并清理 orphan runs。
也就是说,用户报错的那几分钟,Gateway 正在重启。前端订阅恢复连续撞 30 秒超时,报的是 subscription recovery failed。
第二层:这不是一次,是循环
用户说"至少断开了十几次"。我按 6 小时窗口重新扫日志,把关键事件拉成时间线:
| 时间 | 事件 |
|---|---|
| 09:56:22 | memory pressure warning(RSS 2.33GB,heap 1.64GB) |
| 10:01:35 | memory pressure critical(RSS 5.15GB,heap 4.17GB) |
| 10:01:38 | webchat disconnected code=4000 reason=tick timeout |
| 10:02:09 | 第 1 次 SIGTERM 重启 |
| 10:04:30 | 重启恢复,session 订阅成功 |
| 10:14:41 | memory pressure warning(RSS 2.11GB) |
| 10:17:00 | memory pressure critical(RSS 4.9GB,heap 4.16GB) |
| 10:21~10:32 | WebSocket 断线/订阅失败 10+ 次 |
| 10:33:49 | 第 2 次 SIGTERM 重启 |
| 10:36:58 | 新进程启动 |
同一套日志里统计两个关键指标:
journalctl --user -u openclaw-gateway --since "06:00" --no-pager \
| grep -c "received SIGTERM; restarting"
# 2
journalctl --user -u openclaw-gateway --since "06:00" --no-pager \
| grep -iE "liveness heartbeat delayed" | wc -l
# 12完整进程重启只有 2 次,但 liveness heartbeat delayed 出现了 12 次。用户感知的"十几次断连"来自这 12 次心跳延迟:每次 delayed 期间,前端 WebSocket 心跳超时(tick timeout)触发重连,用户感知为一次断连。12 次心跳延迟加 2 次完整重启,正好对上"十几次"。

根因链
memory pressure: critical(RSS 4.9~5.15 GiB,heap 4.16~4.17 GiB)
→ V8 event loop 被 GC 长时间占用
→ liveness heartbeat delayed(25s ~ 161s)
→ WebSocket tick timeout / session subscribe 30s 超时
→ 前端报 session subscription recovery failed
→ gateway 收到 SIGTERM,drain 300s 后 full restart
→ 数分钟后内存再次上涨,循环这里有个容易被忽略的判断:SIGTERM 是谁发的。
dmesg --time-format=iso | grep -iE "oom|openclaw"
# 空输出内核没有 OOM killer 记录。也就是说不是系统内存不够把进程杀了,而是 Gateway 自己发的 SIGTERM。它是内部的 liveness watchdog 在 event loop 卡死超时后触发的自保护。
再看 systemd unit 配置,确认了这个自杀行为会被自动复活:
Restart=always
RestartSec=5
OOMPolicy=continue
SuccessExitStatus=0 143
StartLimitBurst=5
TimeoutStopSec=330Restart=always 加 SuccessExitStatus=143(143 是 SIGTERM),意味着进程每次自杀都会被 systemd 在 5 秒后重新拉起。这就形成了完整的死循环。
场景切换验证
这里有一个很关键的现场信息:排查过程中,用户手动把运行时从 285 个 agent 定义的全量场景切到了 20 多个 agent 的写作场景,问题不再复现。
我用三个数据交叉验证了这个因果:
| 检查项 | 结果 |
|---|---|
~/.openclaw/agents/ 目录 | 285 个 agent 定义 |
openclaw status 运行时加载(切换后) | 27 个 agent |
10:35 之后日志 | 无新的 memory pressure / SIGTERM / tick timeout |
磁盘上有 285 个 agent,运行时只加载 27 个。全量场景和写作场景的差异,直接体现在堆内存曲线上。切场景之后,内存压力消失,心跳恢复正常,重启停止。
结论很清楚:根因就是 agent 规模导致的堆内存膨胀,不是 WebSocket 实现 bug,也不是那天聊的内容有问题。
同时排除了两个干扰项。用 automations 工具查了全部 automation(56 个),全是备份、监控、skill review 和 heartbeat,没有循环重启任务,lastRunStatus 全是 ok。openclaw memory status --deep 持续报 Managed local embeddings are unavailable,这条日志每次 session update 都打一次,噪音很大,但它在两次 critical 内存峰值中都稳定存在,和重启没有因果关系。
深挖:为什么 15GB 堆上限反而坏事
真正的坑在 Gateway 的启动参数。systemd unit 里写的是:
ExecStart=/usr/bin/node --max-old-space-size=15756 ...15756 MB,也就是 15.4GB。这个值看起来很大方,实际是反向操作。
V8 是两代内存模型。新生代叫 New Space,放短命的小对象,回收叫 Scavenge,很快。老生代叫 Old Space,放大对象和存活久的对象,回收叫 Mark-Sweep-Compact,会暂停 JavaScript 执行,也就是 stop-the-world 的 GC pause。--max-old-space-size 控制的就是老生代上限。
V8 的默认老生代上限是 4GB(64 位环境)。把它调到 15GB 之后,发生了四件事:
- GC 触发阈值被推到极高,V8 认为还有大量空间可用,只做力度很小的增量回收
- agent 定义、session 上下文、subagent 运行状态、model 响应 buffer、plugin 对象,全部可以放心堆在老生代里
- 堆越大,单次 Mark-Sweep-Compact 要扫描的对象越多,GC pause 越长。日志里
liveness heartbeat delayed overdue=130000ms的 130 秒,是本次观测到的最大一次(完整范围 25~161s),就是一次大 GC 把 event loop 卡死的直接结果 - 心跳漏拍,watchdog 判定进程失能,发 SIGTERM
换成大白话,15GB 上限让 V8 变成了懒汉 GC。平时不收,堆大了再收,一收就卡,卡了就被 watchdog 杀掉。260+ agent 场景只是让垃圾的产生速度足够快,15GB 上限让 GC 有足够的空间偷懒。
堆里堆的是什么:agent 元数据、plugin 引用和 skill 绑定(磁盘上 285 个定义);session 对话历史、tool call 记录和 context window(openclaw status 显示 116 个 active);subagent registry 残留、model 响应 buffer、memory plugin 对象。这些不是一次性加载,而是随时间累积。几个小时内堆到 4~5GB 完全合理。

修复动作
把堆上限从 15756 降到 8192:
cp ~/.config/systemd/user/openclaw-gateway.service \
~/.config/systemd/user/openclaw-gateway.service.bak-20260914
sed -i 's/--max-old-space-size=15756/--max-old-space-size=8192/' \
~/.config/systemd/user/openclaw-gateway.service
systemctl --user daemon-reload
systemctl --user restart openclaw-gateway.service不是不涨内存,而是涨到一定程度就强制清垃圾,不让它堆到 5GB 还不管。降到 8GB 之后,GC 触发阈值下降,垃圾堆积到 8GB 之前就会被强制回收,单次 GC pause 更短,event loop 被卡住的概率大幅降低。
这次修复踩的两个坑
坑一:重启命令被中断,留下混合态
systemctl --user restart 执行到一半被 SIGTERM 中断,结果 systemd 的 unit 文件已经改成 8192,但进程切换没干净完成。当时状态是:
- 旧主进程 107069 还在 deactivating (stop-sigterm)
- 18789 端口仍被占用
- 新参数版本没有接管
后来 gateway 恢复成 active running,但 systemctl status 显示:实际运行的主进程 128439 用的是 --max-old-space-size=8192,而 unit 文件内容已经被我回滚成 15756。
ExecStart=/usr/bin/node --max-old-space-size=15756 ... # unit 文件
Main PID: 128439 --max-old-space-size=8192 # 实际进程配置和运行态不一致。下一次 gateway 因为任何原因重启,它按 unit 文件读到的还是 15756,修复就白做了。
教训:改 systemd unit 的修复,必须先确认进程切换完成(systemctl is-active + ps 里核对实际参数),再检查 unit 文件内容,两边一致才算修完。
坑二:Gateway Manager.desktop 会不会双实例
排查时顺手看了桌面上那个 Gateway Manager.desktop,担心它和 systemd user service 构成双启动源,导致端口冲突。
实际检查下来它只是 GUI 壳:.desktop → launch-gui.sh → gateway-manager.py,而 gateway-manager.py 管理的仍是 openclaw-gateway.service。ps -ef 里只有一个 gateway --port 18789 进程,没有双实例。这个方向可以排除。
复盘
这次排查最大的收获是两个排查习惯。
openclaw gateway status 只能证明"现在活着",不能证明"刚才没死过"。判断服务是否发生过重启,必须看日志时间线里的 restart/drain/disconnected 事件,而不是看当前态。
把用户描述换算成日志指标是这次排查的关键一步——12 次 heartbeat delayed 加 2 次 SIGTERM,完全对应了"十几次断连"的感知。有了这个换算,才能判断是单次故障还是循环故障。
还有一条:改启动参数的修复,验收标准不能只是"unit 文件改对了",必须是"实际运行进程的参数也对了"。systemd 的 daemon-reload 只更新管理器的认知,不改变已经在跑的进程。
遗留项
三个还没处理:
- unit 文件和运行态不一致:8192 的修复在运行中生效,但 unit 文件是 15756。下次重启会回退。需要在维护窗口统一。
- local embeddings 未配置:
Managed local embeddings are unavailable持续刷屏,要消除噪音需要openclaw configure配 llama.cpp,或暂时禁用 memory plugin。这是独立变更,不建议和内存修复一起动。 - openclaw-disaster-recovery skill 缺 description:只在启动加载时打印一次,不是重启源,优先级最低。
如果以后还要回到 260+ agent 完整场景,堆上限降到 8192 只能缓解 GC pause 的时长,不能解决垃圾产生速度本身。更彻底的做法是分批加载 agent、清理长期不用的 agent 目录、或者把 subagent 的 registry 残留定期清理。
关联阅读
- [[2026-08-03-1706-openclaw-gateway-restart-troubleshooting-auditd-memory-pressure]]
- [[2026-07-22-1046-openclaw-memory-bloat-troubleshooting]]
- [[2026-07-28-1530-openclaw-production-ops-full-review]]
梦行志