一台配备 8GB 内存的树莓派正在运行 OpenClaw,同时有三个 Session 执行任务。系统本身仍能通过 SSH 登录,基础命令也能运行,但三个 Session 突然全部显示 disconnected,网页控制界面几乎失去响应。

过去遇到这种情况,最直接的处理方式往往是强制断电重启。但这一次没有立即重启,而是保留现场、检查进程和内核状态,最终成功让原有 Session 自动恢复,正在执行的任务也没有因为重启而被强制中断。

整个过程展示了一个很典型、也很容易被误判的问题:

系统看起来像“死机”,实际上并没有发生 OOM Kill,而是陷入了由内存软限制、页面回收和低速存储共同造成的内存回收风暴。


一、故障现象

故障出现时,OpenClaw 网页端的三个 Session 同时断线,但操作系统仍然可访问。

系统状态大致如下:

物理内存:约 8GB
可用内存:约 1.2GB
Swap:0
系统负载:快速升高
I/O wait:超过 50%

OpenClaw 服务仍显示:

Active: active (running)
Memory: 约 5.1G
MemoryHigh: 5G
MemoryMax: 6G

进程列表中,大量 openclawopenclaw-hooks 进程处于 D 状态,等待位置包括:

mem_cgroup_handle_over_high
folio_wait_bit_common
wait_on_buffer

这几个信息非常关键。

D 表示不可中断睡眠,通常意味着进程正在等待块设备、文件系统或内核内存回收操作。进程没有退出,也不代表程序逻辑已经崩溃,只是暂时无法获得需要的资源。


二、首先确认:有没有发生 OOM Kill

面对内存不足,首先需要判断的是:

内核有没有真正杀死 OpenClaw 或 Session 对应的进程?

检查内核日志:

journalctl -k --since "最近一段时间" --no-pager \
  | grep -Ei 'oom|out of memory|killed process|memory cgroup'

结果没有任何输出。

随后检查 OpenClaw 服务对应 cgroup 的内存事件:

cg="$(systemctl --user show openclaw-gateway.service \
  -p ControlGroup --value)"

cat "/sys/fs/cgroup${cg}/memory.events"

结果类似:

low 0
high 1622022
max 0
oom 0
oom_kill 0
oom_group_kill 0

这里最重要的是:

oom 0
oom_kill 0

这意味着没有发生 OOM,也没有任何进程被 OOM Killer 杀死。

但另一方面:

high 1622022

说明 MemoryHigh 已经被触发了一百多万次。

这不是普通的“内存有点紧张”,而是服务长时间超过内存软限制,内核正在持续对该 cgroup 进行直接回收和限流。


三、真正的问题:MemoryHigh 引发的回收风暴

systemd 中常见的两个内存控制参数是:

MemoryHigh=
MemoryMax=

两者作用不同。

MemoryHigh 是软限制。服务超过该数值后,内核不会立即杀死进程,而是通过内存回收、限流和压力控制,迫使进程降低内存占用。

MemoryMax 是硬限制。超过该限制且无法回收时,才可能进入 cgroup OOM 处理。

原配置大致是:

[Service]
MemoryHigh=5G
MemoryMax=6G

对于 8GB 内存的设备,这个配置表面上看较为保守,但当 OpenClaw 同时运行多个大任务时,会出现一个问题:

  • 服务实际工作集超过 5GB;
  • 内核开始强制回收;
  • 系统没有 Swap;
  • 被回收的页面稍后又要重新从存储中读取;
  • 多个进程同时等待 SD 卡;
  • 页面刚读回来,又因超过 MemoryHigh 被回收;
  • 形成反复读取、回收、再次读取的循环。

这就是内存回收风暴。


四、PSI 揭示了系统几乎完全停顿

Linux 的 Pressure Stall Information 可以直接反映 CPU、内存和 I/O 压力。

检查服务的内存压力:

cat "/sys/fs/cgroup${cg}/memory.pressure"

故障高峰时的数据类似:

some avg10=96.20
full avg10=93.60

其中:

  • some 表示至少有部分任务因内存压力而停顿;
  • full 表示该 cgroup 中所有可运行任务都因内存压力而停顿。

full avg10 接近 94%,意味着过去十秒内,整个服务几乎有九成以上时间无法正常推进。

此时网页断线并不奇怪。Gateway 本身也被困在同一个 cgroup 中,连维持 WebSocket 响应都变得困难。


五、为什么三个 Session 看起来断了,实际上还活着

进程树检查显示,OpenClaw Gateway、Codex app server,以及多个 Hook 和 Session 相关进程仍然存在。

日志也仍能看到类似状态:

state=processing
activeWorkKind=model_call
lastProgress=...
recovery=none

网页端出现的只是:

webchat disconnected

随后又可以看到:

webchat connected
sessions.list
sessions.usage

因此,真正断开的只是前端 WebSocket 连接,不是 Session 本体。

这也是为什么不应立刻重启服务或强制断电。

只要进程还在,Session 状态仍在,正在执行的任务就可能继续恢复。


六、第一步救援:增加临时 Swap

系统当时完全没有 Swap:

Swap: 0B

于是创建一个临时 4GB Swap 文件:

swap=/swapfile-rescue

fallocate -l 4G "$swap" ||
    dd if=/dev/zero of="$swap" bs=16M count=256 status=progress

chmod 600 "$swap"
mkswap "$swap"
swapon -p 100 "$swap"

启用后,Swap 很快开始被使用:

Swap used:
1MB
40MB
100MB
800MB
1.2GB

与此同时,物理内存的 available 从约 1.1GB 上升到 2GB 以上。

这说明内核开始把暂时不活跃的匿名页面移入 Swap,为当前活跃任务腾出物理内存。

此时系统没有立刻完全恢复,但已经避免继续向 OOM 方向恶化。


七、仅有 Swap 仍然不够

加入 Swap 后,进程虽然没有被杀死,但系统一度仍然存在严重 I/O 抖动。

vmstat 显示:

b: 28~36
wa: 90%~98%

b 表示等待 I/O 的阻塞进程数量。

wa 接近 100%,说明 CPU 几乎没有在执行有效计算,而是在等待 SD 卡。

进程等待位置也变成:

rq_qos_wait
folio_wait_bit_common

这说明 Swap 解决了“无处安放内存页”的问题,但没有解决 MemoryHigh=5G 持续强制回收的问题。

内核一边把页面写入 Swap,一边又因为任务继续访问这些页面而将其读回,形成明显的交换抖动。


八、第二步救援:临时放宽 MemoryHigh

为了停止过度回收,将运行时 MemoryHigh 从 5GB 提高到 6GB:

systemctl --user set-property --runtime \
  openclaw-gateway.service \
  MemoryHigh=6G

此操作不会重启服务,也不会杀死已有进程。

核验结果:

MemoryHigh=6442450944
MemoryMax=6442450944

调整后,系统状态迅速发生变化。

内存 PSI 从:

some avg10 ≈ 95%
full avg10 ≈ 89%

下降到:

some avg10 ≈ 22%
full avg10 ≈ 15%

vmstat 中的 I/O wait 也从长期 90% 以上,大幅降至个位数。

同时可以观察到:

si 较高
so 接近 0

这意味着系统开始把之前换出的页面从 Swap 读回内存,而不再持续向 Swap 写入更多页面。

CPU 使用率也明显上升,说明进程终于从“等待磁盘”恢复为“实际执行计算”。


九、Session 自动恢复

随着压力解除,内存状态逐渐恢复:

Mem used: 约 2.1GB
Mem available: 约 5.5GB
Swap used: 约 700MB

网页重新可以访问,原来的 Session 自动连接回来。

TUI 中显示:

finishing context | connected

Session 名称没有改变,说明恢复的是原来的会话,而不是新建会话。

其中的耗时包含了此前被内存回收和 I/O 阻塞的时间,因此即使显示运行十几分钟,也不代表恢复之后又正常执行了十几分钟。

这是典型的“任务仍然活着,只是曾经长时间无法获得资源”的表现。


十、永久解决方案

确认系统恢复后,将临时救援方案整理为正式配置。

1. 创建标准 4GB Swap

最终使用标准路径:

/swapfile

创建并启用:

fallocate -l 4G /swapfile ||
    dd if=/dev/zero of=/swapfile bs=16M count=256 status=progress

chmod 600 /swapfile
mkswap /swapfile
swapon -p 100 /swapfile

写入 /etc/fstab

/swapfile none swap sw,pri=100 0 0

重新加载 systemd:

systemctl daemon-reload

验证:

findmnt --verify
swapon --show
free -h

findmnt 可能提示 Swap 来源是普通文件:

non-bind mount source /swapfile is a directory or regular file

只要最终结果是:

0 parse errors, 0 errors

就不属于配置错误。


2. 放宽 OpenClaw 内存限制

最终配置调整为:

[Service]
MemoryHigh=6G
MemoryMax=6656M

也就是:

  • 6GB 开始进行软限制;
  • 6.5GB 作为硬上限;
  • 给操作系统、SSH、systemd 和其他服务保留约 1GB 以上空间;
  • 4GB Swap 作为极端情况下的缓冲。

配置文件写入后:

systemctl --user daemon-reload

为了不重启当前 Gateway,可同步应用运行时限制:

systemctl --user set-property --runtime \
  openclaw-gateway.service \
  MemoryHigh=6G \
  MemoryMax=6656M

核验:

systemctl --user show openclaw-gateway.service \
  -p MemoryCurrent \
  -p MemorySwapCurrent \
  -p MemoryHigh \
  -p MemoryMax

十一、为什么不应直接断电

强制断电确实可以让系统快速回到“表面正常”,但代价很大:

  • 所有正在执行的 Session 会立即中断;
  • 尚未落盘的结果可能丢失;
  • Session 状态文件可能处于不完整状态;
  • SD 卡文件系统存在损坏风险;
  • 后续很难判断任务究竟执行到了哪里;
  • 原本可以恢复的进程会被无条件终止。

这次故障中,没有任何进程被 OOM Killer 杀死。三个 Session 只是被困在内存回收和存储等待中。

如果直接断电,实际上等于主动杀死了本来还有机会恢复的任务。


十二、以后再次遇到类似情况,应检查什么

当 OpenClaw 网页无响应或多个 Session 同时断线,但 SSH 仍能登录时,可以依次检查:

是否发生 OOM

journalctl -k --since "最近一段时间" --no-pager \
  | grep -Ei 'oom|out of memory|killed process|memory cgroup'

cgroup 内存事件

cg="$(systemctl --user show openclaw-gateway.service \
  -p ControlGroup --value)"

cat "/sys/fs/cgroup${cg}/memory.events"

重点关注:

high
max
oom
oom_kill

内存压力

cat "/sys/fs/cgroup${cg}/memory.pressure"

进程是否仍然存活

pstree -ap "$(systemctl --user show \
  openclaw-gateway.service -p MainPID --value)"

是否大量卡在 D 状态

ps -eo pid,ppid,stat,rss,etime,wchan:32,cmd \
  --sort=-rss |
grep -E 'PID|openclaw|codex|MainThread'

是否发生 I/O 风暴

vmstat 1 10

如果发现:

oom_kill=0

而大量进程仍存在,只是处于 D 状态,就不应立即重启。

这通常意味着任务还有恢复希望。


结语

这次故障最有价值的结论,不是简单地“增加 Swap 就解决了”,而是厘清了几个容易混淆的状态:

  • 网页断线不等于 Session 死亡;
  • 内存不足不等于已经发生 OOM;
  • 进程处于 D 状态不等于进程已经崩溃;
  • Swap 可以避免 OOM,但无法单独解决过度严格的 MemoryHigh
  • 软限制配置不当,也可能制造比直接 OOM 更漫长的系统停顿;
  • 只要进程仍在、oom_kill=0,就值得优先尝试无重启救援。

最终,一台看似已经“卡死”的 8GB 树莓派,在没有重启、没有杀进程、没有丢失 Session 的情况下恢复了正常运行。

相比直接断电,这种处理方式更慢一些,但保住了正在执行的工作,也避免了文件系统和任务状态损坏。

Leave a Reply

Your email address will not be published. Required fields are marked *