幽灵进程导致 systemd 重启 10637 次:手动 uvicorn 的隐蔽陷阱
本文最后更新于 2026年8月12日 凌晨
凌晨三点,我像往常一样跑 systemctl restart autoquant。日志里一行红字刺眼:service autoquant.service: restart counter 10637 hit max, refusing to start。
10637 次。这个服务在过去 24 小时里被 systemd 反复拉起、反复杀掉,像被谁下了一个死循环的咒。而罪魁祸首,是半年前一个再”顺手”不过的决定——直接 nohup uvicorn ... & 手动启动。
前置条件
复现本文现象,你需要的是这套环境:
- OS:Ubuntu 22.04 / Debian 12
- 系统:systemd 249+,启用了
Restart=on-failure的单元 - 应用:Python FastAPI + uvicorn,端口固定(例如 8000)
- 触发条件:手动
uvicorn或python -m uvicorn曾在某次调试中启动过,且未被正确清理
这个场景在 AutoQuant 迁移期出现过:Docker 拆掉后为了赶进度裸跑,uvicorn --reload 调试忘关;后来写 systemd 单元想”正规化”,却忘了把那条僵尸的 uvicorn 进程杀干净。
最终方案
一句话讲清楚:systemd 单元里不要直接启动 uvicorn,改走 uvicorn 命令行,或显式指定 WorkingDirectory + EnvironmentFile + ExecStart=</path/to/uvicorn>,并确保没有同端口的手动进程残留。
操作步骤
第 1 步:把残留的 uvicorn 全部找出来
1 | |
第 2 步:写一份干净的 systemd 单元
1 | |
关键差异:
| 项目 | 错误写法 | 正确写法 |
|---|---|---|
| 启动方式 | nohup python -m uvicorn ... & |
systemd ExecStart 直起 uvicorn |
| 端口占用 | 手动进程残留,systemd 起不来 | 先 ss 排查,再无残留后 reload |
| 日志 | 丢到 nohup.out |
走 journalctl -u autoquant |
| 重启策略 | systemd 因端口占满一直 fail→restart | 清干净后正常启动 |
第 3 步:加载并启动
1 | |
第 4 步:验证是否真的生效
1 | |
NRestarts=0 才是健康的。如果数字还在涨,说明你漏了什么——继续往下看。
为什么是 10637 次?
systemd 默认 StartLimitIntervalSec=10s(新版)或 10s 内的阈值触发 StartLimit。当 Restart=on-failure 遇上”端口已被占用 → 立刻退出 → 立刻重启”的死循环,一分钟能撞 60 次。24 小时理论上限接近数万。10637 次这个数字,恰好对应”服务挂了 40 分钟左右,每秒失败一次”——也就是一个 uvicorn 占住端口、systemd 又不断起新实例的最小复现窗口。
踩坑记录
坑一:nohup & 留下的”孤儿”
现象:systemctl status autoquant 显示 active (exited) 或反复 activating (auto-restart),端口明明没被别的程序占用,却怎么都起不来。
根因:半年前调试留下的 nohup uvicorn ... & 进程,bash 会话关了、SSH 断了,但进程还在,端口还被占着。systemd 想绑 8000,被占;进程退;systemd 重启;再被占——循环 10637 次就是这么来的。
解法:sudo ss -tlnp | grep 8000 找 PID,kill -9 <pid>。之后写 ExecStart 时明确路径(不要用 -m uvicorn,避免 python 路径解析歧义)。
坑二:WorkingDirectory 不写,uvicorn 找不到 app
现象:ExecStart 里写了 uvicorn app.main:app,结果 journalctl 报 ModuleNotFoundError: No module named 'app'。
根因:systemd 的默认 WorkingDirectory=/,不是你项目目录。
解法:在 [Service] 里显式 WorkingDirectory=/home/tony/projects/autoquant。
坑三:.env 里带空格或 export 前缀,EnvironmentFile 会报错
现象:systemctl start autoquant 后立刻 inactive (dead),日志里 Failed to set environment from file。
根因:EnvironmentFile 期望的格式是 KEY=VALUE,不要 export KEY=VALUE,不要在值里放未转义的空格。
解法:把 .env 改写成 KEY=VALUE 一行一个;或者用 Environment="KEY=VALUE" 在单元里内联。
坑四:重启后 systemd 自动清了残留进程
现象:手动 kill -9 掉了残留 uvicorn,服务正常了; reboot 后又崩。
根因:残留进程来自一个 shell 会话启动的 tmux/screen 或者 .bashrc 里的自启脚本,reboot 后重新起来。
解法:搜 grep -r uvicorn ~/.bashrc ~/.profile /etc/profile.d/ ~/.*rc,把所有”自启 uvicorn”的入口干掉。
顺手自查清单
遇到 systemd 反复重启、RestartCounter 异常升高的时候,依次过一遍这 6 条:
systemctl show <svc> | grep NRestarts—— 先确认是不是真的在爆ss -tlnp | grep <port>—— 端口是否被别的 PID 占ps auxf | grep <app>—— 是否有”手起”的孤儿进程journalctl -u <svc> --since "1 hour ago"—— 看ExecStart的实际 stderrsystemctl cat <svc>—— 单元里WorkingDirectory/EnvironmentFile是否写对systemctl status <svc> --no-pager——Active:是active (running)还是active (exited)
一个值得收藏的排查脚本
把这段扔进 ~/.local/bin/check-service.sh,下次再遇到 10637 次这种数字,先跑它:
1 | |
通用教训:永远不要让”手动启动”和”systemd 托管”两种启动方式共存于同一端口。一旦共存,systemd 会成为你手动进程的”背锅侠”——所有故障日志、重启计数,全记在它的头上,排查时第一眼就会怀疑是系统问题,而真正的元凶(你半年前随手敲的一行
&)早已被遗忘。
结语:给半年前的自己留一句话
如果你正在看这篇文章,说明你也遇到了类似 10637 这个数字。恭喜,你已经站在”只差一步”的位置:清掉手动 uvicorn、重写单元、daemon-reload,世界就安静了。
顺便留个问题给读者:你的生产服务里,有没有那种”当初为了图省事手敲的命令”,现在想来后怕的? 欢迎在评论区留言,把你的”10637”故事讲出来。
参考文献
- systemd.service 手册:
Restart=/StartLimit=/RestartSec=语义. - uvicorn 官方部署文档: Running uvicorn under systemd.
- 系列文章:llama.cpp 从零安装配置指南、Hermes 网关管理踩坑实录、量化系统 Docker→裸跑迁移。