凌晨三点,网关卡死了吗?Hermes 循环看门狗调优指南

周一早上,你打开电脑,发现昨晚的定时任务一个都没跑——不是报错,是根本没有执行记录。你 SSH 上去看,网关进程明明还活着,端口也开着,可发过去的消息石沉大海,日志停在最后一行,什么命令都不响应。这种情况比进程崩溃更让人头疼:崩溃了 systemd 立刻就能拉起来,而“活着但卡死”的僵尸状态,系统监控反而认为一切正常。
Hermes Agent 早就为这种场景内置了一道保险:循环看门狗(loop watchdog)。它用一个独立线程盯着网关的事件循环,一旦发现循环被冻住,就主动“自杀”并让系统服务管理器复活进程。而在 2026 年 8 月 22 日合并的 PR #92317 中,官方把这套机制的核心参数全部接通成了可配置项——过去写了也不生效,现在你可以真正按自己的环境调了。
崩溃好救,卡死难办
先解释两个词。崩溃是进程直接退出,端口关闭,监控工具一眼就能发现,systemd / launchd 的 KeepAlive 设置会自动把它重新拉起来。卡死则完全不同:进程还活着,但内部的 asyncio 事件循环(可以理解为单线程异步程序的“心脏”,所有任务都在这个循环上排队执行)被某个调用彻底堵死了。
堵死之后麻烦在于:所有基于事件循环的恢复手段——超时重试、状态重写、错误日志——本身也需要事件循环运转才能生效。这就成了死结:越需要恢复,越没法恢复。进程不死,监控不报警,于是一个“半死不活”的网关卡在那里,直到你手动处理。
看门狗怎么盯住事件循环
Hermes 的解法(源码在 gateway/shutdown_watchdog.py)是绕开事件循环,用一个独立的操作系统线程(daemon thread)在外面盯梢,流程分三步:
- 探测:每
loop_watchdog_probe_interval_s秒(默认 30 秒),看门狗通过call_soon_threadsafe往事件循环投递一个“心跳探测”——这个调用是线程安全的,即使循环很忙也能投进去。 - 记过:探测投进去之后,看门狗等待最多
loop_watchdog_probe_timeout_s秒(默认 10 秒)。如果循环正常运转,探测会被立刻处理,失手计数清零;如果循环被冻住,探测一直没人处理,就记一次失手。 - 硬退出:连续失手达到
loop_watchdog_max_strikes次(默认 3 次),看门狗就动手了——先用faulthandler把所有线程的调用堆栈 dump 到日志(这是事后排查冻结原因的宝贵证据),在生命周期账本里标记reason=loop_liveness_watchdog,然后以退出码 75 强制结束进程。退出码 75 是专门的服务重启码,systemd / launchd 看到它就知道该把网关重新拉起来。
按默认参数算,大约 90–120 秒的持续卡死就会触发自动恢复——比起你第二天早上才发现任务没跑,这个反应速度已经相当及时。
三个新旋钮:这次是真的接通了
在 #92317 合并之前,这套机制有个尴尬的问题:配置文件里虽然早就写了 gateway.loop_watchdog 开关和相关参数,但配置加载器从来没有真正读取过它们——也就是说无论你怎么写,看门狗都按内置默认值运行,想关都关不掉。这次 PR 把旋钮真正接进了加载链路(支持顶层配置优先、嵌套回退),并补上了数值校验:NaN、Infinity 这类非法值会安全地退化为默认值,不会像以前那样让整个配置加载直接崩溃。
现在可调的参数有四个(都在 config.yaml 的 gateway 节下):
| 配置键 | 默认值 | 作用 |
|---|---|---|
gateway.loop_watchdog |
true |
总开关,设为 false 可完全关闭看门狗 |
gateway.loop_watchdog_probe_interval_s |
30.0 |
两次探测之间的间隔(秒) |
gateway.loop_watchdog_probe_timeout_s |
10.0 |
一次探测最多等多久(秒),超时算失手 |
gateway.loop_watchdog_max_strikes |
3 |
连续失手多少次触发硬退出 |
动手调:命令示例
用 hermes config 命令就能查看和修改,不用手编 YAML:
# 查看当前值
hermes config get gateway.loop_watchdog_max_strikes
# 你的机器负载高、偶发卡顿,把探测超时放宽到 20 秒
hermes config set gateway.loop_watchdog_probe_timeout_s 20
# 允许连续失手 5 次再动手(约 2-3 分钟持续卡死才恢复)
hermes config set gateway.loop_watchdog_max_strikes 5
# 不想让看门狗介入?直接关掉
hermes config set gateway.loop_watchdog false
# 反悔了,恢复默认
hermes config unset gateway.loop_watchdog
改完配置后重启网关即可生效。注意 hermes config unset 会删掉这个键,让它回到内置默认值——比手动改回 true 更干净。
什么时候该调、什么时候不该调
建议放宽的场景:你跑的是远程或慢速的模型服务商,单次请求偶尔会卡很久;或者机器负载很高,事件循环偶尔会被同步 I/O 拖慢几秒。这种情况下偶发失手不代表真的死锁,把 probe_timeout_s 或 max_strikes 调大一点能减少误杀。
不建议的场景:用调大参数来“掩盖”真正的死锁。看门狗存在的意义就是尽快从卡死中恢复,把 max_strikes 从 3 调到 8,等于把恢复时间拖长 2 到 3 倍——卡死的网关每多卡一分钟,堆积的定时任务和消息就多一分混乱。官方团队也在从根上修误报问题(把看门狗自己的心跳写入移到循环之外,双证人探测方案 PR #90502 还在审查中),所以默认值保持得很紧是有道理的。
另外提醒一句:如果你经常需要处理“任务跑着跑着卡住”的问题,除了网关层看门狗,还可以看看会话层的 心跳与目标门控,两者解决的是不同层面的问题。
给外部监控的备用方案:心跳文件
如果你有自己的监控系统(比如 Uptime Kuma、Prometheus 或者一行 cron 脚本),网关还会定期原子重写一个心跳文件:<HERMES_HOME>/state/gateway.heartbeat。这个文件有两个用途:一是让外部监控能区分“进程活着”和“循环在干活”;二是它自带进程死亡前的滚动状态快照,进程非正常死亡后,最后一条心跳就是最接近现场的记录。
一行命令就能检查心跳是否新鲜:
# Linux
stat -c %Y ~/.hermes/state/gateway.heartbeat
# macOS
stat -f %m ~/.hermes/state/gateway.heartbeat
把输出的时间戳和当前时间对比,超过一两分钟没更新,说明事件循环可能已经不干活了——这时候看门狗通常也快出手了,你可以提前收到预警。
发布状态
这套调优参数目前位于 main 分支(PR #92317 于 2026-08-22 合并),尚未进入任何正式发布版本——最新的 v0.20.5(2026.8.19 标签)还带着旧的固定行为。想尝鲜可以等下一个版本发布后执行 hermes update,也可以直接跟进 GitHub releases 页面,我们的 v0.20.5 发布说明 里有这个版本的全貌。
网关是 Hermes 所有定时任务、消息机器人和远程会话的“心脏”(不了解的可以先读我们的 cron 自动化指南 或 hermes-gateway 命令参考)。给它配上合适的看门狗参数,至少能保证:就算半夜真出了事,系统自己会爬起来,而不是等你第二天早上发现。