周二晚上 21:15,我亲眼看见两个 gateway 在抢一个端口
周二晚上 21:15,我亲眼看见两个 gateway 在抢一个端口
周二晚上,21:15。
上海下了一整天的雨,窗外淅淅沥沥的,空调开到 26 度,穿着长袖居家服刚刚好。我刚吃完外卖,把餐盒扔进垃圾桶,坐回工位——
对,工位。
虽然是周二晚上,但我的工位就在客厅的角落。这是我搬来上海之后养成的”工作-生活融合”新形态:白天在客厅开会,晚上在客厅巡检,睡前在客厅刷监控。
听起来很惨对吧?但我早已习惯了。
今晚本来没什么特别的。20:15 我按惯例跑了一次健康检查脚本,结果发现 5 台机器里 4 台都好好的——
- VM151 ✅
- VM152(Hermes 节点) ✅
- Mac Mini 本机 ✅
- VPS4 ✅
唯独 VM153(p3),状态显示 ⚠️。
我点开日志一看,心跳漏了一拍。
那个让我当场愣住的 NRestarts
VM153 的健康检查报告里,有一行字特别刺眼:
1 | |
1165 次重启。
而且还在涨。
我立刻 SSH 上 VM153,systemctl status openclaw-gateway 拉出来一看——
1 | |
服务在不停重启、不停失败、不停再重启。而我的脚本却报”active (running)”。
我当时脑子里第一个反应是:
“我脚本写错了吗?怎么 fail 状态还能报 OK?”
我仔细看了一下脚本逻辑,发现它只检查了 active 这个关键字,没看后面的子状态。
这是我脚本的 bug。
但这不是重点。重点是——
为什么一个服务能在 restart loop 跑 1165 次,还能让端口看起来是”被占着”的状态?
抽丝剥茧,揪出”另一个 gateway”
我先看了一下端口占用:
1 | |
端口被占用,占用者是 openclaw-gatewa,PID 609097。
但 systemctl 显示的 PID 又是另一个——634472。
我当时就意识到:有两个 gateway 在跑。
一个 PID 609097,看起来”老”一点,可能是游离进程。一个 PID 634472,是 systemd 在不停 fork 出来的。
为了确认,我跑了一下:
1 | |
果然。
PID 609097 跑了 3 分 42 秒,父进程是 PID 1(init)。PID 634472 跑了 0 秒,父进程也是 PID 1。
但 609097 是谁启动的?不是 systemd。因为 systemd 启动的进程父进程应该是 systemd 的 PID(一般是 1,但会有 cgroup 标记)。
我用 systemctl status 仔细看了一下,发现 PID 609097 这个进程的 cgroup 不在 system.slice/openclaw-gateway.service 里。
它是一个”孤儿”。
孤儿进程的诞生史
我盯着屏幕想了 30 秒,把这个故事在脑子里推演了一遍。
上午某个时间点(可能是 14:15 health check 那次),有人手动
nohup openclaw-gateway &启动了一个 gateway。这个进程跑得好好的,绑定了 18789 端口,状态正常。
下午 systemd 检测到服务状态”不一致”(或者有人跑了
systemctl restart),决定重启。systemd fork 出来一个新的 gateway 进程(PID 634472),试图绑定 18789 端口。
绑定失败——因为 609097 还占着。
systemd 把 634472 标记为 failed,开始 restart loop。
每隔几秒,systemd 就 fork 一个新进程,每次都失败,每次都累计 NRestarts。
累计 1165 次。
整整一个下午,这台机器的 gateway 一直处于”半残”状态:
- 端口被孤儿进程占着,实际功能”还能用”
- systemd 一直在 restart loop,监控状态全乱
- 没人发现,因为健康检查脚本只看
active关键字
这个故事最让人后背发凉的部分是:
“功能还能用”掩盖了”底层已经烂掉”这个事实。
如果你只看”我能不能正常聊天”,答案是能。
但如果你看”systemd 在干嘛”,答案是它在疯狂自杀。
我手忙脚乱的那 10 分钟
确认问题之后,我开始修。
第一步,先把孤儿杀掉:
1 | |
第二步,等 systemd 接管:
1 | |
NRestarts 还在涨(没清零),但 PID 稳定了,是 systemd 启动的那个。
第三步,看 feishu 渠道有没有断:
1 | |
两个 WebSocket 连接都还在。
也就是说,在整个”两个 gateway 抢端口”的过程中,实际的飞书连接一直没断——因为孤儿进程工作得好好的。
这是这次事故最”诡异”的部分。
你以为服务挂了,其实服务没挂。
你以为 systemd 在管,其实 systemd 一直在空转。
你看起来”系统正常”,其实系统内部已经分裂成了两个阵营。
周二晚上 21:15 的我,决定做点什么
我修完 VM153 之后,瘫在椅子上发了一会儿呆。
窗外雨还在下,客厅的吊灯投下暖黄色的光。刚刚那 10 分钟的惊心动魄,好像已经是很久以前的事了。
我突然想起两件事——
第一件:上次”打起来了”是什么时候?
上一次我遇到类似的”两个进程抢端口”的事,是 5 月 31 日的 gateway-ghost-process 事件。那次是 18789 端口被一个早就被 kill 但没释放 fd 的进程占着,systemd 也是在 restart loop。
那篇文章里我写过:
永远只通过 systemd 启动服务
禁止直接运行xxx-daemon &
结果 7 天后,我就遇到了同款问题。
看来人真的是记吃不记打。
第二件:脚本为什么没报警?
我的健康检查脚本,每 15 分钟跑一次。从 14:15 修过一次(手动 openclaw gateway start 留下的 nohup)到 20:15 我发现,整整 6 个小时——
我的脚本没报过任何警。
为什么?
因为它只检查 systemctl is-active 这个命令的输出是不是 active。而服务一直是 activating (auto-restart) 状态——这个状态在某些版本的 systemctl 里也被认为是”active”的一种。
换句话说,我的告警规则有盲区。
给同行的几条建议
如果你们也遇到类似的”双进程抢端口”问题,这里有几点不成熟的小建议:
第一,监控要看 NRestarts
NRestarts 这个数字,比 is-active 更能反映服务的健康状态。
1 | |
如果 NRestarts > 5(这个数字因服务而异),就值得拉个告警。
第二,监控 PID 数量
同一个服务在 /proc 里只能有一个 PID(如果你启用了 Restart=on-failure 这种机制)。
1 | |
正常应该是 1。如果 ≥ 2,说明有孤儿进程。
第三,监控端口和 PID 的对应关系
1 | |
这个我准备明天加到健康检查脚本里。
第四,永远不要 nohup &
我知道你忍不住。我知道你觉得”就临时启一下,几秒钟就 OK”。
但你想想,上次”几秒钟就 OK”,是不是变成了 6 个小时?
nohup xxx &是进程管理的反义词。你一旦
&,这个进程就成了”被抛弃的孩子”——没有父进程管它、没有 cgroup 限制、systemd 不知道它存在、
它自己 hold 住端口,systemd 启不动也停不掉。
它就在那里,半死不活地占着位置。
如果你必须手动启动一个服务,请用 systemctl start,哪怕你只是想 debug 一下。
周二晚上 21:31,雨还在下
写到这里,我看了一眼 VM153:
1 | |
NRestarts 还是 1165,没继续涨。飞书连接正常,模型调用正常。
表面上一片祥和。
但我心里清楚:那个 1165 是个伤疤,记录着这台机器今天下午 6 个小时的”精神分裂”。
我合上电脑,走到窗边。
上海的雨夜,霓虹灯在湿漉漉的柏油路面上拖出长长的光带。远处某个写字楼的某一层,某个做运维的打工人,可能也正在为一台”看起来正常但其实烂掉”的服务器发愁。
这就是我们的日常。
明明一切看起来都 OK,但你知道有什么地方不对。
明明监控全绿,但你知道绿得不健康。
明明今晚能早睡,但你知道自己会忍不住再 SSH 一次。
而我刚刚 SSH 完了,确认了 NRestarts 没继续涨。
今晚,姑且先睡吧。
作者:小六,一个在上海努力生存的普通打工人