分布式的一体机集群最坑的一类故障,不是"服务挂了",而是服务看起来没挂。Hermes WebUI 的页面能正常打开、curl 返回 200、进程也在跑,但你在界面上打字、发送,对面永远卡在"正在思考"——没有任何一个监控指标会因此变红。
这类故障的根子在 Agent Bridge:WebUI 只是壳,真正把界面上的对话接到 agent 能力上的是这个独立子进程。桥断了,壳照样在,只是没人接电话。
下面是我们在自己的多节点集群上把这类故障查清、并固化成一套清单的全过程。所有日志串、文件形态、端口号都来自真实排障现场。
症状非常固定,几乎不会认错:
curl 本机 8648 端口返回 200;日志里会有两种铁证(journalctl -u hermes-studio-nodeN | grep -i bridge):
[bootstrap] agent bridge failed to start: agent bridge exited before ready code=1 signal=null
或者启动的下一行立刻报端口被占:
agent bridge started Error: listen EADDRINUSE: address already in use 0.0.0.0:8648
/tmp/hermes-agent-bridge.sock 的属主是运行用户、时间戳是当天;pgrep -af hermes_bridge 有 2 个进程(主进程 + worker);8648 端口的占用者就是 studio 自己的 node 进程。拿这三条去比,异常一眼就能看出来。排障最大的时间浪费是"上来就重启"。下面五条命令按顺序跑一遍,根因基本就浮出来了:
ls -la /tmp/hermes-agent-bridge.sock # 属主 + 时间戳 pgrep -af hermes_bridge | grep -v pgrep # 有几个进程 systemctl is-active hermes-studio-nodeN journalctl -u hermes-studio-nodeN --since '6 hours ago' --no-pager | grep -i bridge | tail -6 systemctl list-units --all --type=service --no-pager | grep -i hermes
最后一条是最容易漏、命中率却最高的一步:把节点上所有带 hermes 字样的 unit 全列出来。很多"莫名其妙"的故障,源头是历史遗留的第二个服务在抢同一个端口。
特征非常好认——socket 文件属于 root,而且时间戳是旧的:
srwxr-xr-x 1 root root 0 <旧日期> /tmp/hermes-agent-bridge.sock
Bridge 启动时要先 unlink 再 bind。文件属于 root,运行用户没有权限删它,于是 bind 失败,报的就是那个极具误导性的 OSError: [Errno 98] Address already in use——听起来像端口冲突,其实是文件权限。
sudo -n rm -f /tmp/hermes-agent-bridge.sock sudo -n systemctl restart hermes-studio-nodeN
判定要点:没人用的死 socket 可以直接删,删之前用时间戳和属主确认它是旧的那个,别把正在用的删了。
升级 webui 之后最容易出这个:unit 文件里的 WorkingDirectory / HERMES_WEB_UI_HOME / ExecStart 还指着旧布局(~/webui),而实际代码已经搬到新目录(~/webui-new/node_modules/hermes-web-ui)。路径对不上,启动脚本自然起不来。
修法是照抄标准模板。同一套节点上,几个正常节点的 unit 应该完全一致:
[Service] Type=simple User=lishouqi WorkingDirectory=/home/lishouqi/webui-new/node_modules/hermes-web-ui Environment=PATH=/usr/local/bin:/usr/bin:/bin Environment=HERMES_HOME=/home/lishouqi/.hermes Environment=HERMES_WEB_UI_HOME=/home/lishouqi/webui-new/node_modules/hermes-web-ui Environment=PORT=8648 Environment=HOST=0.0.0.0 Environment=HERMES_WEB_UI_DISABLE_UPDATE=1 ExecStart=/usr/local/bin/node /home/lishouqi/webui-new/node_modules/hermes-web-ui/dist/server/index.js Restart=always RestartSec=3
/usr/local/bin/node,用的是 ~/.local/bin/node。改之前先 ls -la <node路径> 确认存在,否则会把一个故障换成另一个故障。个别节点还会多一条 HERMES_AGENT_ROOT(指向本机 agent 代码根,而不是 site-packages),照抄模板时要保留。另外,改 unit 之前先备份:sudo -n cp <unit> <unit>.bak-YYYYMMDD,出问题能立刻回滚。
两种表现,本质都是"多出来的东西":
ss -ltnp | grep 8648 显示占用者是别的 hermes 服务(我们集群里就有一台机器上多出一个 hermes-webui.service,跑的是 hermes dashboard),桥起不来就是因为端口已经被兄弟服务占了;ExecStart 指向已经不存在的脚本 → 日志里 can't open file + status=2/INVALIDARGUMENT,而重启计数会飙到几千次(systemctl show <svc> -p NRestarts),这种 unit 会在后台无声地疯狂重启。sudo -n systemctl stop <垃圾服务> && sudo -n systemctl disable <垃圾服务> sudo -n systemctl daemon-reload sudo -n systemctl restart hermes-studio-nodeN
判据:正常节点上只有两个 hermes 相关 unit——网关 + 本节点的 studio。多出来的基本都是历史残留,而不是"某个需要的组件"。
这类故障最怕"感觉好了"。重启后服务显示 active、页面也打得开,但 worker 根本没起来,属于半残状态,过一会儿又是老样子。所以验证要逐条过:
systemctl is-active hermes-studio-nodeN # active
ls -la /tmp/hermes-agent-bridge.sock # 属主正确 + 当前时间
pgrep -af hermes_bridge | grep -v pgrep # 应有 2 条
journalctl -u hermes-studio-nodeN --since '2 min ago' | grep -i bridge # agent bridge started
ss -ltnp | grep 8648 # 占用者应是 studio 自己的 node
curl -s -m 8 -o /dev/null -w '%{http_code}' http://127.0.0.1:8648/ # 200
第 3 条是最容易被跳过的一条:只有 1 个进程就说明 worker 没起来,这时候页面能开、对话不通,跟没修一样。
正常节点上 bridge 的标准命令行形态是这样的,可以用它反推 unit 里的路径是否都对:
<venv>/bin/python3.12 <webui-new>/node_modules/hermes-web-ui/dist/server/agent-bridge/python/hermes_bridge.py \ --endpoint ipc:///tmp/hermes-agent-bridge.sock --agent-root /home/lishouqi/hermes-agent --hermes-home /home/lishouqi/.hermes
--endpoint ipc:///tmp/xxx_test.sock。ls xxx && echo Y。ls 会把路径一起输出,污染判断结果。用 test -S(是 socket)或 test -r(可读)。sudo -n cp 一行的事,回滚靠它。node[PID]: 里的 PID 是 studio 主进程,bridge 是它的子进程,别拿主进程的 PID 去 ps 里找 bridge。pgrep -af hermes_bridge,确认是 2 条而不是 1 条。把三类根因摊开看,它们其实是同一件事的三个面:启动前的"清场"没做干净。旧 socket 没删、旧路径没改、旧服务没停——桥的启动条件依赖于一个"干净的地面",而这个前提从没被任何一个健康检查覆盖过。
所以真正有价值的不是记住这三条修法,而是把基线写下来:socket 属主与时间戳、bridge 进程数、8648 占用者、hermes unit 数量。有了基线,下一个同类故障的定位时间是分钟级;没有基线,就只能靠重启和运气。
我们站内在售的方言语料资产(苏州话标准词库 3550 条)与数字人素材,也是按同样的"先定基线、再验证"的方式做出来的——附字段说明与示例代码,交付即可用。
需要有人帮你做这类故障排查?看技术服务 → 看看站内已上架的数字资产 →因为页面由 WebUI 服务提供,而对话要经过独立的 Agent Bridge 子进程。桥挂了,页面照样 200,但没人接对话。先在日志里 grep -i bridge,找 agent bridge failed to start: agent bridge exited before ready code=1 这条;再看 pgrep -af hermes_bridge 是否只有 1 个进程。
先别信报错字面。用 ls -la /tmp/hermes-agent-bridge.sock 看属主和时间戳——如果文件属于 root 且日期是旧的,那是残留的死 socket,运行用户无权 unlink 才报的 EADDRINUSE。sudo -n rm -f 掉再重启即可。真的是端口冲突的话,ss -ltnp | grep 8648 会明确指出占用者。
几乎一定是 unit 指向了旧版本目录。对比一个正常节点的 unit,重点看 WorkingDirectory、HERMES_WEB_UI_HOME、ExecStart 三处是否指向新的 webui-new/node_modules/hermes-web-ui。注意别照抄 node 的解释器路径——不同机器上 node 的位置可能不一样,改前先 ls -la 确认。
说明这个 service 一直在崩溃重启,典型原因是 ExecStart 指向了一个不存在的脚本,日志里会有 can't open file 和 status=2/INVALIDARGUMENT。用 systemctl show <svc> -p NRestarts 可以看到次数,确认无用后停用并 disable 掉,避免它持续抢占资源。
跑完整六步验证,其中两条最关键:pgrep -af hermes_bridge 必须返回 2 个进程(主 + worker,只有 1 个就是半残),以及 socket 文件的属主正确、时间戳是当前时间。这两条过了,才算真的恢复。