目录

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.subscribe

sessions.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:22memory pressure warning(RSS 2.33GB,heap 1.64GB)
10:01:35memory pressure critical(RSS 5.15GB,heap 4.17GB)
10:01:38webchat disconnected code=4000 reason=tick timeout
10:02:09第 1 次 SIGTERM 重启
10:04:30重启恢复,session 订阅成功
10:14:41memory pressure warning(RSS 2.11GB)
10:17:00memory pressure critical(RSS 4.9GB,heap 4.16GB)
10:21~10:32WebSocket 断线/订阅失败 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 次完整重启,正好对上"十几次"。

Gateway 内存压力与重启事件时间线

根因链

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=330

Restart=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 之后,发生了四件事:

  1. GC 触发阈值被推到极高,V8 认为还有大量空间可用,只做力度很小的增量回收
  2. agent 定义、session 上下文、subagent 运行状态、model 响应 buffer、plugin 对象,全部可以放心堆在老生代里
  3. 堆越大,单次 Mark-Sweep-Compact 要扫描的对象越多,GC pause 越长。日志里 liveness heartbeat delayed overdue=130000ms 的 130 秒,是本次观测到的最大一次(完整范围 25~161s),就是一次大 GC 把 event loop 卡死的直接结果
  4. 心跳漏拍,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 完全合理。

V8 分代内存与 GC 触发机制示意图

修复动作

把堆上限从 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]]

参考来源