一、背景
OpenClaw Gateway 作为 AI Agent 的核心服务,长期运行难免会遇到进程崩溃、假死、配置异常等问题。本文记录了搭建两级健康守护体系的完整方案。
二、问题分析
Gateway 异常主要分三种情况:
情况
表现
难度
进程崩溃
进程退出,端口释放
简单,systemd 自带
端口不通
进程在但端口未监听
中等
假死卡顿
进程在、端口在,但不响应请求
最难检测
前两种好处理,第三种最头疼——看起来一切正常,实际上已经不能工作了。
三、方案设计
(一)架构总览
采用两级守护,各司其职:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ┌─────────────────────────────────────────────────────┐ │ Gateway 异常 │ ├──────────────────────┬──────────────────────────────┤ │ 进程崩溃 │ 进程在但卡死(假死) │ │ (异常退出) │ │ │ │ │ │ │ │ ▼ │ ▼ │ │ systemd │ cron 健康检查脚本 │ │ Restart=always │ 每 30 秒检测一次 │ │ 5 秒内自动拉起 │ │ │ │ 检测项: │ │ │ ├── 端口是否监听 │ │ │ ├── HTTP 是否响应 │ │ │ └── /health 接口是否正常 │ │ │ │ │ │ │ ▼ │ │ │ 指数退避重启 │ │ │ 5s → 10s → 20s → 强制清理 │ └──────────────────────┴──────────────────────────────┘
(二)检测流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 cron 触发(每 30 秒) │ ▼ ┌─────────────┐ │ 维护模式? │ └──────┬──────┘ 是 → 跳过退出 否 │ ▼ ┌───────────────┐ │ 人为停止? │ │ (inactive) │ └──────┬────────┘ 是 → 跳过退出 否 │ ▼ ┌──────────────┐ │ 端口监听? │──── 否 ──→ 计数+1 └──────┬───────┘ │ 是 ▼ ┌──────────────┐ │ HTTP 响应? │──── 否 ──→ 计数+1 └──────┬───────┘ │ 是 ▼ ┌──────────────┐ │ /health OK? │──── 否 ──→ 计数+1 └──────┬───────┘ │ 是 ▼ 备份配置,重置计数
(三)指数退避重启流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 连续失败 │ ▼ 第 1 次失败 → 等 5 秒 → 重启 第 2 次失败 → 等 10 秒 → 重启 第 3 次失败 → 等 20 秒 → 重启 │ ▼ ┌──────────────────┐ │ systemctl stop │ │ 杀端口占用进程 │ │ pkill -9 残留 │ │ lsof 二次清理 │ │ systemctl start │ └──────────────────┘
(四)关键原则
不修改 systemd service 文件 ——避免引入新的故障点
职责分离 ——systemd 只管进程崩溃,cron 只管假死检测
尊重人为操作 ——手动 stop 不会被自动拉起
指数退避 ——避免连续失败时雪崩式重启
四、部署步骤
(一)创建健康检查脚本
脚本位置:~/.openclaw/scripts/openclaw-health-check.sh
1 2 mkdir -p ~/.openclaw/scriptsvim ~/.openclaw/scripts/openclaw-health-check.sh
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 #!/bin/bash PORT=18789 SERVICE="openclaw-gateway.service" STATE_FILE="/tmp/openclaw-health-state" LOG_FILE="/tmp/openclaw/health-check.log" MAINTENANCE_FLAG="/tmp/openclaw-maintenance" CONFIG_FILE="$HOME /.openclaw/openclaw.json" CONFIG_BAK="$HOME /.openclaw/openclaw.json.bak" MAX_FAIL=3 export XDG_RUNTIME_DIR="/run/user/$(id -u) " export DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR /bus" mkdir -p /tmp/openclawif [ -f "$MAINTENANCE_FLAG " ]; then exit 0 fi is_active=$(systemctl --user is-active "$SERVICE " 2>/dev/null) if [ "$is_active " = "inactive" ]; then exit 0 fi log () { echo "$(date '+%Y-%m-%d %H:%M:%S') $1 " >> "$LOG_FILE " } read_state () { if [ -f "$STATE_FILE " ]; then fail_count=$(grep -o 'fail=[0-9]*' "$STATE_FILE " | cut -d= -f2) last_fail=$(grep -o 'last=[0-9]*' "$STATE_FILE " | cut -d= -f2) fail_count=${fail_count:-0} last_fail=${last_fail:-0} else fail_count=0 last_fail=0 fi } write_state () { echo "fail=$1 :last=$2 " > "$STATE_FILE " } check_health () { if ! ss -tlnp 2>/dev/null | grep -q ":${PORT} " ; then echo "port" return fi http_code=$(curl -s --noproxy '*' -o /dev/null -w "%{http_code}" --max-time 5 http://127.0.0.1:${PORT} / 2>/dev/null) if [ "$http_code " = "000" ]; then echo "http" return fi health=$(curl -s --noproxy '*' --max-time 10 http://127.0.0.1:${PORT} /health 2>/dev/null) if ! echo "$health " | grep -q '"ok":true' ; then echo "stuck" return fi echo "ok" } do_restart () { local fail=$1 local reason=$2 local wait =$((5 * (2 ** (fail - 1 )))) log "[FAIL] ${reason} ,${wait} 秒后重启 (${fail} /${MAX_FAIL} )" sleep "$wait " systemctl --user stop "$SERVICE " 2>/dev/null sleep 2 fuser -k "${PORT} /tcp" 2>/dev/null sleep 1 fuser -k -9 "${PORT} /tcp" 2>/dev/null sleep 1 pkill -9 -f "openclaw-gateway" 2>/dev/null sleep 1 if ss -tlnp 2>/dev/null | grep -q ":${PORT} " ; then log "[FAIL] 端口仍被占用,lsof 清理..." lsof -ti ":${PORT} " 2>/dev/null | xargs kill -9 2>/dev/null sleep 1 fi systemctl --user start "$SERVICE " log "[FAIL] 重启完成 (第${fail} 次)" } result=$(check_health) if [ "$result " = "ok" ]; then if [ -f "$CONFIG_FILE " ]; then cp "$CONFIG_FILE " "$CONFIG_BAK " fi read_state if [ "$fail_count " -gt 0 ]; then log "[OK] 服务恢复正常" fi write_state 0 0 exit 0 fi read_state now=$(date +%s) if [ "$((now - last_fail) )" -lt 30 ]; then exit 0 fi fail_count=$((fail_count + 1 )) write_state "$fail_count " "$now " if [ "$fail_count " -le "$MAX_FAIL " ]; then do_restart "$fail_count " "$result " fi HOUR_MIN=$(date +%H:%M) if [ "${HOUR_MIN:0:2} " = "03" ] && [ "${HOUR_MIN:3:2} " -lt 2 ]; then find /tmp/openclaw -name "*.log" -mtime +7 -delete 2>/dev/null find /tmp/openclaw -name "*.log" -size +50M -truncate 2>/dev/null log "[MAINT] 日志清理完成" fi exit 0
(二)赋予执行权限
1 chmod +x ~/.openclaw/scripts/openclaw-health-check.sh
(三)设置 cron 定时任务
设置两条错开的 cron,实现 30 秒检测:
添加:
1 2 * * * * * /home/openclaw/.openclaw/scripts/openclaw-health-check.sh * * * * * sleep 30 && /home/openclaw/.openclaw/scripts/openclaw-health-check.sh
(四)备份原版 systemd service(可选)
1 2 mkdir -p ~/.openclaw/backupcp ~/.config/systemd/user/openclaw-gateway.service ~/.openclaw/backup/openclaw-gateway.service.bak
五、功能清单
功能
说明
进程崩溃恢复
systemd Restart=always,5 秒内拉起
假死检测
/health 接口探针,30 秒一次
指数退避重启
5s → 10s → 20s,避免频繁重启
僵尸进程清理
端口占用、残留进程强制 kill
人为停止不干预
检测到 inactive 状态直接跳过
配置自动备份
每次健康检查时覆盖备份 openclaw.json
防抖机制
30 秒内不重复计数
维护开关
一键暂停检测
日志自动清理
每天凌晨清理 7 天以上日志
六、常用命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 cat /tmp/openclaw/health-check.log | tail -10touch /tmp/openclaw-maintenancerm /tmp/openclaw-maintenancecrontab -r && systemctl --user stop openclaw-gateway.service && pkill -9 -f openclaw-gateway && fuser -k -9 18789/tcp cp ~/.openclaw/openclaw.json.bak ~/.openclaw/openclaw.jsoncp ~/.config/systemd/user/openclaw-gateway.service ~/.openclaw/backup/openclaw-gateway.service.bak
七、测试方法
(一)测试进程崩溃
1 2 3 4 5 kill -6 $(pgrep -f openclaw-gateway)sleep 5systemctl --user status openclaw-gateway.service
(二)测试人为停止(不应自动拉起)
1 2 3 4 5 6 7 systemctl --user stop openclaw-gateway.service systemctl --user status openclaw-gateway.service cat /tmp/openclaw/health-check.log | tail -5systemctl --user start openclaw-gateway.service
(三)测试维护开关
1 2 3 4 5 6 7 touch /tmp/openclaw-maintenancesystemctl --user stop openclaw-gateway.service rm /tmp/openclaw-maintenancesystemctl --user start openclaw-gateway.service
八、文件清单
文件
说明
~/.openclaw/scripts/openclaw-health-check.sh
健康守护脚本
~/.openclaw/backup/openclaw-gateway.service.bak
原版 systemd service 备份
~/.openclaw/openclaw.json.bak
配置自动备份(每分钟覆盖)
/tmp/openclaw-health-state
失败计数状态
/tmp/openclaw/health-check.log
守护日志
/tmp/openclaw-maintenance
维护开关文件(存在=暂停检测)
九、总结
这个方案的核心理念是简单可靠 :
不碰 systemd service ——避免引入新的故障点
职责分离 ——systemd 管崩溃,cron 管假死
尊重人为操作 ——手动 stop 不会被自动拉起
指数退避 ——避免连续失败时雪崩式重启
一键维护 ——维护开关 + 紧急停止命令
没有花哨的功能,但足够实用。OpenClaw 官方目前也没有内置方案(GitHub Issue #32917 还在讨论中),社区方案本质上也是类似的 cron + 脚本思路,只是多了配置回滚和告警通知等功能。