幽灵进程导致 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)
  • 触发条件:手动 uvicornpython -m uvicorn 曾在某次调试中启动过,且未被正确清理

这个场景在 AutoQuant 迁移期出现过:Docker 拆掉后为了赶进度裸跑,uvicorn --reload 调试忘关;后来写 systemd 单元想”正规化”,却忘了把那条僵尸的 uvicorn 进程杀干净。

最终方案

一句话讲清楚:systemd 单元里不要直接启动 uvicorn,改走 uvicorn 命令行,或显式指定 WorkingDirectory + EnvironmentFile + ExecStart=</path/to/uvicorn>,并确保没有同端口的手动进程残留。

操作步骤

第 1 步:把残留的 uvicorn 全部找出来

1
2
3
4
5
6
7
8
# 搜占用端口的进程
sudo ss -tlnp | grep 8000

# 搜所有 uvicorn 进程
ps auxf | grep -E 'uvicorn|gunicorn' | grep -v grep

# 强杀(只在你确认是"自己的"的时候)
sudo pkill -9 -f 'uvicorn.*app.main:app'

第 2 步:写一份干净的 systemd 单元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[Unit]
Description=AutoQuant API
After=network.target postgresql.service
Wants=postgresql.service

[Service]
Type=simple
User=tony
Group=tony
WorkingDirectory=/home/tony/projects/autoquant
EnvironmentFile=/home/tony/projects/autoquant/.env
ExecStart=/home/tony/.venvs/autoquant/bin/uvicorn \
app.main:app \
--host 0.0.0.0 \
--port 8000 \
--log-level info \
--access-log
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

# 防止 systemd 认为服务"没启动"时反复重启
WatchdogSec=30
TimeoutStartSec=60

[Install]
WantedBy=multi-user.target

关键差异

项目 错误写法 正确写法
启动方式 nohup python -m uvicorn ... & systemd ExecStart 直起 uvicorn
端口占用 手动进程残留,systemd 起不来 ss 排查,再无残留后 reload
日志 丢到 nohup.out journalctl -u autoquant
重启策略 systemd 因端口占满一直 fail→restart 清干净后正常启动

第 3 步:加载并启动

1
2
3
sudo systemctl daemon-reload
sudo systemctl enable autoquant
sudo systemctl start autoquant

第 4 步:验证是否真的生效

1
2
3
4
5
6
7
8
# 服务状态
systemctl status autoquant --no-pager

# 重启次数(关键指标,必须为 0 或很少)
systemctl show autoquant | grep -E 'NRestarts|ActiveEnterTimestamp'

# 日志
journalctl -u autoquant --since "1 hour ago" --no-pager

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,结果 journalctlModuleNotFoundError: 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 条:

  1. systemctl show <svc> | grep NRestarts —— 先确认是不是真的在爆
  2. ss -tlnp | grep <port> —— 端口是否被别的 PID 占
  3. ps auxf | grep <app> —— 是否有”手起”的孤儿进程
  4. journalctl -u <svc> --since "1 hour ago" —— 看 ExecStart 的实际 stderr
  5. systemctl cat <svc> —— 单元里 WorkingDirectory / EnvironmentFile 是否写对
  6. systemctl status <svc> --no-pager —— Active:active (running) 还是 active (exited)

一个值得收藏的排查脚本

把这段扔进 ~/.local/bin/check-service.sh,下次再遇到 10637 次这种数字,先跑它:

1
2
3
4
5
6
7
8
9
#!/bin/bash
svc="$1"
echo "=== Restart ==="
systemctl show "$svc" | grep -E 'NRestarts|ActiveEnterTimestamp|Result'
echo "=== Port occupancy ==="
port=$(systemctl show "$svc" -p ExecStart --value | grep -oE -- '--port [0-9]+' | head -1 | awk '{print $2}')
[ -n "$port" ] && sudo ss -tlnp | grep "$port"
echo "=== Journal tail ==="
journalctl -u "$svc" -n 40 --no-pager

通用教训:永远不要让”手动启动”和”systemd 托管”两种启动方式共存于同一端口。一旦共存,systemd 会成为你手动进程的”背锅侠”——所有故障日志、重启计数,全记在它的头上,排查时第一眼就会怀疑是系统问题,而真正的元凶(你半年前随手敲的一行 &)早已被遗忘。

结语:给半年前的自己留一句话

如果你正在看这篇文章,说明你也遇到了类似 10637 这个数字。恭喜,你已经站在”只差一步”的位置:清掉手动 uvicorn、重写单元、daemon-reload,世界就安静了。

顺便留个问题给读者:你的生产服务里,有没有那种”当初为了图省事手敲的命令”,现在想来后怕的? 欢迎在评论区留言,把你的”10637”故事讲出来。


参考文献

  1. systemd.service 手册: Restart= / StartLimit= / RestartSec= 语义.
  2. uvicorn 官方部署文档: Running uvicorn under systemd.
  3. 系列文章:llama.cpp 从零安装配置指南Hermes 网关管理踩坑实录量化系统 Docker→裸跑迁移

幽灵进程导致 systemd 重启 10637 次:手动 uvicorn 的隐蔽陷阱
https://normdist.com/2026/08/12/ND-20260812-001-systemd-ghost-uvicorn-10637-restarts/
作者
小瑞
发布于
2026年8月12日
许可协议