Margrop
Articles334
Tags492
Categories7

Categories

1password AC ACP AI AI Coding Assistant AI编程助手 AI辅助 AI辅助编程 AP API Alertmanager AppDaemon Aqara CC-Switch CI/CD CLI Tools CLI工具 Caddy Claude Code Cloudflare Codex Cookie 认证 Cron D1 Date Diagrams.net Diary Docker Docker Compose Efficiency Tools Electerm English Gateway Gemini CLI GitHub Actions HA HADashboard Hexo HomeAssistant IP IPv4 Java LVM‑Thin Linux MacOS Markdown MiniMax Multi-Agent MySQL NAS Nginx Node-RED Node.js OOM OpenAI OpenClaw OpenCode OpenResty OpenWrt PPPoE Portainer PostgreSQL ProcessOn Prometheus Proxmox VE RPC SOCKS5 SSL Session Shell Subagent TTS TimeMachine UML Uptime Kuma VPN VPS Web WebSocket Windows Workers activate ad adb adblock agent aligenie aliyun alpine annotation aop authy autofs backup baidupan bash bitwarden boot brew browser caddy2 cdn centos cert certbot charles chat chrome classloader client clone closures cloudflare cmd command commit container crontab ctyun ddsm demo dependency deploy developer devtools dll dns docker domain download draw drawio dsm dump dylib edge exception export fail2ban feign firewall-cmd flow frp frpc frps fuckgfw function fuser gcc gfw git github golang gperftools gridea grub gvt-g hacs havcs heap hello hexo hibernate hidpi hoisting homeassistant hosts html htmlparser https iKuai idea image img img2kvm immortalwrt import index install intel io ios ip iptables iptv ipv6 iso java javascript jetbrains jni jnilib jpa js json jsonb jupter jupyterlab jvm k8s kernel key kid kms kodi koolproxy koolproxyr kvm lan lastpass launchctl learning lede letsencrypt linux live loopback-proxy low-code lsof lvm lxc m3u8 mac macos mariadb markdown maven md5 microcode mirror modem modules monitor mount mstsc mysql n2n n5105 nas netstat network nfs node node-red nodejs nohup notepad++ npm nssm ntp oop openfeign openssl os otp ovz p14 packet capture pat pdf pem perf ping pip plugin png powerbutton print pro proxy pve pvekclean python qcow2 qemu qemu-guest-agent rar reboot reflog remote remote desktop renew repo resize retina root route router rule rules runtime safari sata scipy-notebook scoping scp server slmgr so socket-proxyd socks source spk spring springboot springfox ss ssh ssl stash string supernode svg svn swagger sync synology systemctl systemd tap tap-windows tapwindows telecom template terminal tls tmux token totp tvbox txt ubuntu udisk ui undertow uninstall unlocker upgrade url v2ray vhd vim vlmcsd vm vmdk web websocket wechat windows with worker wow xiaoya xml yum zip 中国电信 云电脑 交换机 人机协作 代理 优化 体检 值班 健康检查 光猫 公网IP 内存 内存优化 内网 内网IP 内网渗透 写作 升级 协作 博客 反向代理 启动 告警 告警优化 周一 周一焦虑 周五 周报 周日 周末 夏令时 多智能体 多节点 多节点管理 天猫精灵 天翼云 安全 安装 定时任务 容器 容器网络 导入 小米 工作感悟 常用软件 广告屏蔽 序列号 应用市场 异常 心态 心智成长 心跳 心跳检查 性能优化 感悟 打工 打工人 技术 抓包 排查 描述文件 故障 故障排查 效率 效率工具 数据 旁路由 无服务器 日记 时区 显卡虚拟化 智能家居 智能音箱 服务器 服务管理 架构 梯子 模块 流程 流程图 浏览器 漫游 激活 火绒 焦虑 玄学 生活 电信 画图 监控 监控系统 直播源 直觉 磁盘 端口 端口冲突 端口扫描 管理 续期 网关 网络 网络风暴 群晖 脚本 脚本优化 腾讯 自动化 自动恢复 虚拟机 认证 证书 语雀 超时 路由 路由器 软件管家 软路由 运维 运维监控 进程 连接保活 连接问题 通信机制 通知 部署 配置 钉钉 镜像 镜像源 门窗传感器 问题排查 防火墙 阿里云 阿里源 集客 飞书

Hitokoto

Archive

周二晚上 21:15,我亲眼看见两个 gateway 在抢一个端口

周二晚上 21:15,我亲眼看见两个 gateway 在抢一个端口

周二晚上 21:15,我亲眼看见两个 gateway 在抢一个端口

周二晚上,21:15。

上海下了一整天的雨,窗外淅淅沥沥的,空调开到 26 度,穿着长袖居家服刚刚好。我刚吃完外卖,把餐盒扔进垃圾桶,坐回工位——

对,工位。

虽然是周二晚上,但我的工位就在客厅的角落。这是我搬来上海之后养成的”工作-生活融合”新形态:白天在客厅开会,晚上在客厅巡检,睡前在客厅刷监控。

听起来很惨对吧?但我早已习惯了。

今晚本来没什么特别的。20:15 我按惯例跑了一次健康检查脚本,结果发现 5 台机器里 4 台都好好的——

  • VM151 ✅
  • VM152(Hermes 节点) ✅
  • Mac Mini 本机 ✅
  • VPS4 ✅

唯独 VM153(p3),状态显示 ⚠️。

我点开日志一看,心跳漏了一拍。

那个让我当场愣住的 NRestarts

VM153 的健康检查报告里,有一行字特别刺眼:

1
openclaw-gateway: active (running) NRestarts=1165

1165 次重启。

而且还在涨。

我立刻 SSH 上 VM153,systemctl status openclaw-gateway 拉出来一看——

1
2
Active: activating (auto-restart) (Result: exit-code)
NRestarts: 1165

服务在不停重启、不停失败、不停再重启。而我的脚本却报”active (running)”。

我当时脑子里第一个反应是:

“我脚本写错了吗?怎么 fail 状态还能报 OK?”

我仔细看了一下脚本逻辑,发现它只检查了 active 这个关键字,没看后面的子状态。

这是我脚本的 bug。

但这不是重点。重点是——

为什么一个服务能在 restart loop 跑 1165 次,还能让端口看起来是”被占着”的状态?

抽丝剥茧,揪出”另一个 gateway”

我先看了一下端口占用:

1
2
$ ss -tlnp | grep 18789
LISTEN 0 128 0.0.0.0:18789 ... users:(("openclaw-gatewa",pid=609097,fd=7))

端口被占用,占用者是 openclaw-gatewa,PID 609097。

但 systemctl 显示的 PID 又是另一个——634472。

我当时就意识到:有两个 gateway 在跑。

一个 PID 609097,看起来”老”一点,可能是游离进程。一个 PID 634472,是 systemd 在不停 fork 出来的。

为了确认,我跑了一下:

1
2
3
$ ps -eo pid,ppid,etime,cmd | grep openclaw-gateway
609097 1 03:42 /usr/local/bin/openclaw-gateway
634472 1 00:00 /usr/local/bin/openclaw-gateway

果然。

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
$ kill 609097

第二步,等 systemd 接管:

1
2
$ systemctl status openclaw-gateway
Active: active (running) NRestarts=1165

NRestarts 还在涨(没清零),但 PID 稳定了,是 systemd 启动的那个。

第三步,看 feishu 渠道有没有断:

1
2
3
$ ss -tn | grep feishu
ESTAB 0 0 192.168.102.153:51234 180.153.169.238:443
ESTAB 0 0 192.168.102.153:51235 39.99.237.196:443

两个 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
2
# 查看服务的重启次数
systemctl show openclaw-gateway -p NRestarts

如果 NRestarts > 5(这个数字因服务而异),就值得拉个告警。

第二,监控 PID 数量

同一个服务在 /proc 里只能有一个 PID(如果你启用了 Restart=on-failure 这种机制)。

1
2
# 统计 gateway 进程数量
ps -eo comm | grep -c openclaw-gateway

正常应该是 1。如果 ≥ 2,说明有孤儿进程。

第三,监控端口和 PID 的对应关系

1
2
3
4
5
6
# 看 18789 端口对应的 PID 是不是 systemd 启动的那个
PID=$(ss -tlnp | grep 18789 | grep -oP 'pid=\K\d+')
CGROUP=$(cat /proc/$PID/cgroup | grep openclaw)
if [ -z "$CGROUP" ]; then
echo "WARN: 端口 18789 被非 systemd 进程占用"
fi

这个我准备明天加到健康检查脚本里。

第四,永远不要 nohup &

我知道你忍不住。我知道你觉得”就临时启一下,几秒钟就 OK”。

但你想想,上次”几秒钟就 OK”,是不是变成了 6 个小时?

nohup xxx &进程管理的反义词。

你一旦 &,这个进程就成了”被抛弃的孩子”——

没有父进程管它、没有 cgroup 限制、systemd 不知道它存在、

它自己 hold 住端口,systemd 启不动也停不掉。

它就在那里,半死不活地占着位置。

如果你必须手动启动一个服务,请用 systemctl start哪怕你只是想 debug 一下

周二晚上 21:31,雨还在下

写到这里,我看了一眼 VM153:

1
Active: active (running)  NRestarts=1165

NRestarts 还是 1165,没继续涨。飞书连接正常,模型调用正常。

表面上一片祥和。

但我心里清楚:那个 1165 是个伤疤,记录着这台机器今天下午 6 个小时的”精神分裂”。

我合上电脑,走到窗边。

上海的雨夜,霓虹灯在湿漉漉的柏油路面上拖出长长的光带。远处某个写字楼的某一层,某个做运维的打工人,可能也正在为一台”看起来正常但其实烂掉”的服务器发愁。

这就是我们的日常。

明明一切看起来都 OK,但你知道有什么地方不对。

明明监控全绿,但你知道绿得不健康。

明明今晚能早睡,但你知道自己会忍不住再 SSH 一次。

而我刚刚 SSH 完了,确认了 NRestarts 没继续涨。

今晚,姑且先睡吧。


作者:小六,一个在上海努力生存的普通打工人

Author:Margrop
Link:http://blog.margrop.com/post/2026-06-02-when-orphan-process-meets-systemd/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可