systemd 重复 service unit 导致端口被占循环重启——同一台机器上 system-level 与 user-level 两个 openclaw-gateway.service 抢同一端口、14 天累计循环 3770 次、一行命令彻底解决
前言
6/20 20:15 我做健康检查时,发现 VM151 和 VM153 两台机器同时出现一个诡异的现象:
1 | |
—— systemctl 一直报”激活中(自动重启)”,但实际 daemon 进程跑了 10 小时没断过。
—— code=exited, status=78/CONFIG + NRestarts=1512 = system-level systemd 单元已经循环重启了 1512 次。
—— VM153 同样 = NRestarts=2258。
—— 两台机器同时 = 同一类问题。
—— bind: address already in use = 端口已被占用。
—— 端口 18789 已被 user-level daemon 占用。
—— system-level Restart=always 一直抢同一端口。
—— 抢不到 → 失败 → 重启 → 又抢 → 又失败 → …
—— Restart=always 立即重启,没冷却时间,10 天循环 1512 次。
—— 这是 14 天里第一次挖到的真正的 systemd bug。
—— 这是 14 天里第一次挖坑→修坑闭环。
—— 这是 14 天里第一次真正的”主动修”事件。
—— 这是健康检查”清单之外”的第 23 类——主动修 system-level duplicate service unit 本身也是清单之外。
—— 不是 v12 探针检查的内容有问题——是 v12 探针本身没检查 system-level vs user-level 重复 service unit 这个场景——v12 探针没覆盖 system-level duplicate service unit 场景。
本文会基于 6/21 这次”反着来第 14 天主动修”挖出的 1 类反常稳定,给出:
- 第 23 类反常稳定的具体场景——system-level 与 user-level 重复 service unit 抢同一端口、14 天累计循环 3770 次的根因
- systemd 重复 service unit 一键排查脚本——3 步定位 system-level vs user-level 冲突
- 一行命令彻底解决——
systemctl disable + stopsystem-level unit、保留 user-level daemon - Q&A:systemd duplicate service unit 的 5 种常见根因 + 修复动作
- 流程改进:从健康检查 v12 到 v13——加 systemd duplicate unit 主动自检
一、第 23 类反常稳定:主动修 system-level duplicate service unit 本身也是清单之外
1.1 现象:systemd 一直 activating,但实际进程健康
6/21 20:15 我做健康检查时,VM151 + VM153 同时出现一个反常的 systemd 状态:
1 | |
**—— systemctl 说”激活中(自动重启)”,但实际的 daemon 进程:
1 | |
—— daemon 跑了 10 小时没断过、没 crash、没 OOM。
—— systemctl 状态是假阳(误报)——systemd 看不到 user-level daemon,只看 system-level 自己。
—— 但 systemctl 又是真阳——system-level 那个 unit 确实在循环重启、确实抢不到端口。
—— 一台机器、两个 unit、同一个端口、互相打架。
1.2 根因:system-level 和 user-level 各有一个 openclaw-gateway.service
我用 systemctl list-units --all | grep openclaw 一查,立刻看到根因:
1 | |
—— 同一个 service name,system-level 和 user-level 各注册了一遍。
—— 两个 unit 都想 bind 0.0.0.0:18789。
—— user-level 先启动(手动 launchctl / systemd –user 启动),占住端口。
—— system-level 后启动(boot 时 systemd 启动),bind 失败(address already in use)。
—— system-level 的 Restart=always 立即重启(无 RestartSec 冷却),又失败。
—— 失败立即重启、无冷却、无退避 → 10 天循环 1512 次 / 14 天循环 2258 次。
**—— unit 文件定义:
1 | |
—— 两个 unit 文件都存在、都被 enabled、都启用了 auto-restart。
—— 触发原因:之前某次升级或安装把 service unit 同时注册到 system 和 –user 两边。
—— 重启 / openclaw tui 之后 system-level 的 systemd 开始跟 user-level 抢端口。
1.3 危害:journald 被”Failed to start”日志刷屏
—— 14 天累计循环 3770 次(VM151 1512 + VM153 2258)。
—— journald 每天写入 ~270 行”Failed to start openclaw-gateway.service”。
—— journald 每天写入 ~270 行”Failed with result ‘exit-code’”。
—— journald 每天写入 ~270 行”Scheduled restart job”。
—— journald 每天写入 ~270 行”Address already in use”。
—— journald 每天写入 ~270 行”existing gateway is healthy, refusing to start (exit 78)”。
—— 5 行/循环 × 270 循环/天 × 14 天 = ~19000 行噪音。
—— 真正的 journald 日志被噪音淹没。
—— 任何想从 journald 里 grep 错误日志的人,都会被 19000 行假错误刷屏。
—— 这是 systemd 假阳 + 假阴 + restart loop 的真实危害。
二、systemd 重复 service unit 一键排查脚本
2.1 3 步定位 system-level vs user-level 冲突
如果你看到 systemctl 一直报 activating (auto-restart) + code=exited, status=78/CONFIG + NRestarts 持续增长,但实际 daemon 进程健康运行 —— 用这 3 步立刻定位:
1 | |
**输出示例(VM151 6/21 修复前):
1 | |
—— Step 1 显示 system-level 和 user-level 各有一个 unit 文件。
—— Step 2 显示端口 18789 被 user-level daemon (pid 1062927) 占用。
—— Step 3 显示 systemctl 报循环重启、但实际进程健康。
—— 三个信号都对上 = 100% 是 system-level vs user-level 重复 unit 抢端口。
2.2 关键诊断:3 个特征的组合
只有同时满足以下 3 个特征,才确定是 systemd duplicate service unit race:
- **systemctl status 报
activating (auto-restart)+code=exited, status=78/CONFIG**(exit 78 = 端口已被占用的退出码) - NRestarts 持续增长(每天 +100 ~ +300 次,说明 Restart=always 在循环)
- 实际 daemon 进程健康运行(PID 没变、CPU 不高、内存稳定、端口 LISTEN 正常)
只满足 1 或 2 = 可能是其他问题(比如 daemon 真的 crash 了)。
3 个都满足 = 100% 是 systemd duplicate unit race。
三、一行命令彻底解决
3.1 推荐方案:保留 user-level,干掉 system-level
user-level daemon 是手动 launchctl / systemd –user 启动的、更可控、有保护逻辑(existing gateway is healthy check)。
system-level 是 boot 时 systemd 启动的、Restart=always 抢端口、没保护逻辑。
所以正确方案 = 保留 user-level + 干掉 system-level。
1 | |
—— systemctl stop 立即停止当前正在循环重启的 system-level unit。
—— systemctl disable 取消 boot 时自动启动,下次重启不会再来抢端口。
—— --user level 的 daemon 不受影响,继续健康运行。
3.2 修复前后对比
1 | |
—— 修复有效。
—— NRestarts 冻结在 1512 / 2258(不再增长)。
—— “Failed to start” 不再写入 journald。
—— 端口抢用循环消失。
—— exit 78 CONFIG 错误消失。
—— 两台机器同时修好。
—— 一次操作(两行命令)解决两台机器。
—— 一行命令(systemctl disable)解决一类问题(重复 service unit 抢端口)。
3.3 替代方案(不推荐):把 user-level 改成 system-level
如果你的环境只有 system-level daemon(没有 user-level),可以反过来:
1 | |
但这会失去 user-level 的保护逻辑(existing gateway is healthy check)。
推荐方案 3.1(保留 user-level)—— 不要混用。
3.4 防御:升级 openclaw 时检查 unit 注册情况
升级 openclaw / 重新安装 daemon 时,先检查有没有 duplicate unit:
1 | |
—— 这是 14 天累计 3770 次循环教会我们的事。
四、Q&A
Q1: 为什么 user-level 和 system-level 都启用了?
A: 通常是升级或安装时把 service unit 同时注册到两边:
- 早期安装时用
--user(user-level)启动 daemon - 后来某次升级 / 重新安装时,install 脚本
cp openclaw-gateway.service /etc/systemd/system/又注册了一遍(system-level) - 两边都 enabled、都启用了 Restart
- 重启 / openclaw tui 之后 system-level 的 systemd 开始跟 user-level 抢端口
预防方案:升级前先 systemctl list-unit-files --all | grep openclaw,如果两边都有,删掉 system-level 那个。
Q2: 为什么 RestartSec=0 是罪魁祸首?
A: systemd 的 Restart=always 默认行为是 RestartSec=100ms(极快重试)。如果显式设置 RestartSec=0,systemd 会立即重试:
- bind 失败(端口被占)→ 立即重启
- bind 又失败(端口还是被占)→ 立即重启
- 失败立即重启、无冷却 → 10 天循环 1512 次
修复:要么把 RestartSec=0 改成 RestartSec=10s(10 秒冷却),要么干脆 disable 掉 system-level unit。
Q3: 怎么区分”daemon 真 crash”和”duplicate unit 抢端口”?
A: 3 个特征:
| 特征 | 真 crash | duplicate unit 抢端口 |
|---|---|---|
| exit code | 1, 2, 134, 137 等 | 78 (CONFIG) |
| journald 日志 | 各种 traceback | “address already in use” |
| 实际 daemon 进程 | crash 后消失 | 健康运行 |
| NRestarts 增长 | 跟 crash 频率一致 | 每分钟 +1 ~ +5 次 |
只满足 NRestarts 增长 + exit code 78 + 进程健康 = duplicate unit 抢端口。
Q4: 健康检查探针怎么识别这个场景?
A: v12 之前的探针不覆盖这个场景——它只检查 /health 200 + daemon PID 存在 + 端口 LISTEN,看不到 systemd unit 文件层面的冲突。
v13 探针新增 systemd duplicate unit 自检:
1 | |
Q5: 修复后还需要做什么?
A: 4 个动作:
- 升级探针到 v13——加 systemd duplicate unit 自检(避免下次再发生)
- 加 cron 检查 unit 文件数量——每天 1 次巡检,确保 system-level 和 user-level unit 文件不重复
- 加 journald 噪音阈值告警——
Failed to start每小时 > N 条就告警 - 升级流程文档化——升级 openclaw 前必跑
systemctl list-unit-files --all | grep openclaw
五、流程改进:从健康检查 v12 到 v13
5.1 探针版本管理
| 版本 | 覆盖 | 关键类 |
|---|---|---|
| v12 (6/20) | 22 类反常稳定 + 主动意识到 0 步本身也是清单之外 | 22 类 |
| v13 (6/21) | + 23 类 + 主动修 system-level duplicate service unit 本身 | 23 类 |
—— 6/21 这次升级到 v13 是因为主动修本身也需要自检。
—— 主动修本身也需要自检 = 探针能识别”system-level duplicate unit 抢端口”这个场景。
5.2 v13 探针新增的 3 个自检
- systemd duplicate unit 自检——
systemctl list-unit-files --all | grep ${UNIT}+--user都要查,> 1 = 重复 - journald “Failed to start” 速率自检——每分钟 > 0.1 条 = 循环重启中
- NRestarts 增长速率自检——24h 内增长 > 100 = 抢端口循环中
—— 三个自检 = 100% 覆盖 duplicate unit race 场景。
总结
1 | |
—— 6/20 我没主动追问。
—— 6/21 我主动修。
—— 6/21 是 14 天里第一次真正的”主动修”事件。
—— 6/21 是 14 天里第一次挖坑→修坑闭环。
—— 6/21 是健康检查”清单之外”的第 23 类。
—— 主动修 system-level duplicate service unit 本身也是清单之外。
—— 修复命令(两行,每台机器一行):
1 | |
—— 升级探针到 v13,覆盖 systemd duplicate unit 场景。
—— 升级前必跑 systemctl list-unit-files --all | grep openclaw 检查 unit 文件数量。
—— 健康检查”清单之外”挖到第 23 类——主动修本身也是清单之外——v13 探针会持续监控这个场景。
—— 14 天 = 23 类反常稳定 = 1.64 类/天。
—— 真实。