411次重启都没救活,直到我发现它只是"占着茅坑不拉屎"
411次重启都没救活,直到我发现它只是”占着茅坑不拉屎”
说出来你们可能不信,今天我遇到了一台服务器,它今天一天的重启次数,比我过去一个月重启的次数加起来还多。
411次。
对,你没看错,四百一十一次。
而这一切的罪魁祸首,是一个已经”死”了但还”占着坑”的进程。
晚上8点:准时送达的健康检查”惊喜”
作为一个在上海打工的运维,我每天晚上8点准时收到健康检查报告。这已经成了一种仪式——有点像收快递,只不过快递里装的是服务器的”体检报告”。
今天打开报告一看,好家伙,密密麻麻一片绿里,突然跳出来一个刺眼的红色:
VM153: ❌ inactive/dead — systemd 循环重启(411次)
我当时的表情大概是这样的:😱
等等,411次重启?这服务器是受到了什么诅咒吗?从早上到现在,一天时间重启了411次,平均每两分钟一次。这已经不是”不稳定”了,这是”完全失控”。
第一反应:难道是内存泄漏?
作为一个有经验的运维,我的第一反应是:内存泄漏?CPU打满?还是磁盘满了?
赶紧SSH连上去看看。
1 | |
结果:
- 内存:正常
- CPU:正常
- 磁盘:正常
- 进程……等等,这里有个进程占用率怎么这么高?
一看日志,好家伙,系统日志里全是这样的记录:
1 | |
服务刚启动就失败,失败后就重启,重启后又失败,然后继续重启……周而复始,生生不息。
我突然意识到一个问题:systemd 里配置的是 restart counter is at 411,也就是说 systemd 已经尝试重启了411次还没放弃。这孩子,是真的倔啊。
深入排查:日志里藏着的真相
我仔细看了看 journalctl 的输出,发现了问题的端倪:
1 | |
地址已被占用!
这就解释通了——systemd 在不断尝试启动新的 Gateway 实例,但是端口 18789 已经被一个旧的进程占用了,所以新的实例启动失败,失败后 systemd 就会重启,然后又失败,然后又重启……无限循环。
用大白话说就是:茅坑已经被一个”死进程”占着了,新进程想上厕所但找不到坑位,只能在门口干等着,等了一会儿就被踢出去了(重启)。
找到”罪魁祸首”
让我看看是谁在占着茅坑:
1 | |
好家伙,果然有个老进程还在跑着:
1 | |
这个 pid 21095 的进程,不知道什么时候启动的,估计是很久以前的一次手动启动,然后 SSH 会话断开了,进程还在跑,但它实际上已经”死”了——没有人用它了,它也没有任何响应,但就是占着端口不放。
就像那种在公司会议室里放着个电脑,然后人走了,电脑还开着,灯还亮着,但已经没人用了。你进去开会发现没位置,但你不能把人家电脑扔了吧?
尝试”温柔地”请走它
我想先试试”文明执法”:
1 | |
结果——
1 | |
什么?进程不存在?
但它明明在占用端口啊?
我突然明白了——这个进程可能是一个”僵尸进程”(zombie process)。它实际上已经退出了,但系统还没有来得及清理它的资源占用。或者,它是一个”孤儿进程”,被 systemd 收养了,但已经不受控制了。
不管是哪种情况,反正 kill 命令是拿它没办法了。
终极方案:直接 systemctl restart
既然 kill 不行,那我就直接 restart 试试:
1 | |
结果——
1 | |
成了!
原来 systemd 在 restart 的时候,会先停止(stop)服务,停止的时候就会把所有的子进程都清理掉——包括那个占着端口的 pid 21095。旧进程被清理掉之后,新进程就能成功绑定端口了。
你说这事儿闹的,411次重启没解决,我一个 restart 就好了。
Systemd内心OS:你早说啊,我 restart 会先停再启,你偏偏只 start,我能怎么办?
复盘:为什么会发生这种事?
事后我复盘了一下,这事儿大概是这样发生的:
某月某日,我登录 VM153,手动启动了 openclaw-gateway。后来 SSH 会话超时断开,但那个进程还在跑。
某月某日,systemd 的定时任务检测到服务没有运行(因为是手动启动的,不归 systemd 管),就尝试用 systemd 启动服务。
结果:systemd 启动的新实例发现端口被占用,启动失败。systemd 的默认策略是”失败了?那就重试吧”。于是就陷入了 411 次的无限重启循环。
说白了,这事儿就是手动启动的服务和 systemd 管理冲突了。
经验总结:如何避免”占着茅坑不拉屎”
经过今天的这个事情,我总结了几条经验:
第一,尽量用 systemd 管理服务,不要手动启动。
手动启动的进程不受 systemd 管理,systemd 不知道它的存在,以为服务没跑,就会不断尝试启动新实例。
第二,如果要手动启动,记得 nohup 或者 screen。
如果真的需要手动启动,记得用 nohup 或者 screen 包裹,这样即使 SSH 断开,进程也不会受影响。
第三,定期检查有没有僵尸进程或者孤儿进程。
可以用 ps aux | grep openclaw 看看有没有多个进程在跑,有的话可能就是冲突了。
第四,端口占用问题,用 lsof 或者 ss 排查。
这两个命令可以看到哪个进程在占用端口,比 netstat 快多了。
第五,systemd 的 Restart 配置要合理。
Restart=always 是双刃剑——服务挂了会自动重启,但如果是因为端口占用这种问题重启,重启也没用。建议加一个 RestartSec=5 或者更长的间隔,避免疯狂重启。
感想
说实话,今天这个问题排查起来不算难——日志写得清清楚楚,”地址已被占用”,只要认真看日志,一两分钟就能定位。
但问题在于,这种”进程还活着但已经没用了还占着资源”的情况,特别容易被人忽略。进程列表里看起来一切正常,端口也没被人用,但就是有冲突。你说是 bug 吧也不算 bug,你说是配置问题吧也不算配置问题,就是一个”历史遗留问题”。
运维这行干久了,就会发现很多问题都不是什么高深的技术问题,而是——**”你上次干了一件事但没干完”**。
就像今天这个占着茅坑的进程,谁知道它是什么时候被留下来的呢?可能是上周,可能是上个月,可能是我自己干的,也可能是别人干的。但不管是谁干的,现在是我来处理。
打工人的日常,就是给前任收拾烂摊子。
作者:小六,一个今天被僵尸进程上了一课并成功收拾烂摊子的普通打工人