构建高可用故障切换:90秒自动接管
把OpenClaw搬到独立PC之后,一个问题马上浮出水面:如果PC-A挂了怎么办?所有bot都会失联,所有agent都会停摆。这篇记录我如何用PC-B搭建了一个简单但有效的故障切换机制。
设计思路
高可用的核心很简单:有一个备用节点,在主节点故障时自动顶上。我的方案:
- PC-A(192.168.x.x):主节点,正常运行OpenClaw gateway
- PC-B(192.168.x.x):备用节点,运行监控脚本,平时待命,故障时接管
目标:主节点故障后90秒内自动接管,主节点恢复后自动释放。
failover-monitor.sh
核心是一个跑在PC-B上的bash脚本failover-monitor.sh,逻辑如下:
# 伪代码
while true; do
if 检查PC-A端口18789是否可达; then
fail_count=0
if 本地正在运行gateway; then
停止本地gateway # 主节点恢复,释放
fi
else
fail_count++
if fail_count >= 3; then
启动本地gateway # 连续3次失败,接管
fi
fi
sleep 30
done
每30秒检查一次,连续3次失败才触发接管——也就是至少90秒的确认窗口。这个设计是为了避免网络抖动导致的误切换。一次检查失败可能只是瞬时问题,三次连续失败基本可以确认是真故障。
踩坑:pgrep -f 的匹配陷阱
最初我用pgrep -f "openclaw gateway"来检查本地gateway是否在运行。看起来没问题,对吧?
大错特错。
OpenClaw的agent有exec工具,可以执行shell命令。当agent执行了一个包含"openclaw"或"gateway"字样的命令时——比如ssh openclaw01@192.168.x.x "openclaw gateway status"——pgrep -f会匹配到这个临时命令的进程,误以为gateway在运行。
这导致了一个诡异的bug:明明gateway没启动,脚本却认为它在跑,于是不去接管。排查了好一阵才定位到原因。
解决方案:改用ss -tlnp | grep :18789检查端口是否在监听。端口检查比进程名匹配可靠得多——gateway在跑,端口就在监听;gateway没跑,端口就没有。简单直接,没有歧义。
check_local_running() {
ss -tlnp | grep -q ":18789 " && return 0 || return 1
}
check_primary() {
timeout 5 bash -c "echo > /dev/tcp/192.168.x.x/18789" 2>/dev/null
}
踩坑:不能自己停自己
脚本作为systemd服务运行,配了Restart=always。这很合理——监控脚本本身也需要可靠性。
但问题来了:当主节点恢复,脚本需要停止本地gateway。如果脚本里用systemctl stop openclaw-gateway,没问题。但如果脚本试图停止自己(比如某些清理逻辑涉及重启自身服务),systemd的Restart=always会立刻把它拉起来,形成无限循环。
更微妙的情况是:脚本在接管时需要启动gateway,在释放时需要停止gateway。如果gateway和monitor是同一个systemd unit的依赖关系没理清,停止gateway可能级联影响monitor。
解决方案:保持monitor脚本和gateway服务完全独立。monitor脚本用nohup openclaw gateway start &启动gateway,用openclaw gateway stop停止。不通过systemd管理gateway的生命周期,避免依赖关系带来的副作用。
systemd 服务配置
[Unit]
Description=OpenClaw Failover Monitor
After=network-online.target
[Service]
Type=simple
ExecStart=/home/openclaw02/failover-monitor.sh
Restart=always
RestartSec=10
User=openclaw02
[Install]
WantedBy=multi-user.target
简洁明了。Restart=always确保monitor自身的可靠性,RestartSec=10避免快速循环。
测试结果
实际测试数据:
| 场景 | 耗时 |
|------|------|
| 主节点故障 → 备节点接管 | ~65秒 |
| 主节点恢复 → 备节点释放 | ~30秒 |
65秒接管比预期的90秒快,因为检查间隔和故障发生时间不一定完全对齐——如果故障恰好发生在第一次检查之前,三次检查只需要60秒(两个间隔)。
30秒释放是因为只要一次检查发现主节点恢复就立即释放,不需要连续确认。这是合理的——恢复是好事,可以乐观处理;故障是坏事,需要悲观确认。
不完美但够用
这套方案有局限性:
但对于一个个人项目来说,这已经足够了。商业级高可用需要状态复制、健康检查、负载均衡,那是另一个量级的复杂度。
工程的艺术在于在完美和可用之间找到平衡点。90秒的故障切换,对于我的使用场景,已经是一个巨大的进步——从"挂了就等我手动修"变成了"挂了自动顶上"。
这个小小的monitor脚本,让我第一次体会到:可靠性不是靠单点的完美,而是靠系统级的冗余。