在印度用 systemd 管 Python 服务,重启后端口被占的排查
上周在孟买机房的一台 Ubuntu 22.04 机器上,部署了一个 FastAPI 服务,用 systemd 托管,跑在 8000 端口,日均请求量不大,QPS 峰值 40 左右。上线两周没出事,直到运维做了一次例行内核升级重启,服务起不来了,systemctl status 显示 Address already in use。
先说结论:不是端口被别的进程占了,是 systemd 的 Restart=always 加上我们自己的 TimeoutStopSec 配置,让旧进程在重启窗口里没死透,新进程抢先 bind 失败,然后 systemd 认为启动失败又拉起一次,循环。
先看当时的 unit 配置:
[Service]
Type=simple
ExecStart=/opt/app/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000
Restart=always
RestartSec=2
KillMode=control-group
TimeoutStopSec=90
问题出在 Type=simple。uvicorn 自己会 fork 一个 reloader/worker 进程(我们当时开了 --workers 2),systemd 只盯着 ExecStart 的主 PID,主进程收到 SIGTERM 后优雅退出,但子 worker 还在处理长连接,90 秒内没退完,systemd 到点发 SIGKILL,但那时新实例已经被 Restart=always 拉起来了,两边抢 8000 端口。
排查时最有用的两条命令:
ss -ltnp 'sport = :8000'
journalctl -u myapp --since "2024-06-11 03:00" -o cat | grep -i address
ss 输出里 PID 一栏显示的是旧 worker 的 pid,而不是新主进程,这才确认是残留子进程。
改法分两步。第一,把启动方式改成 Type=exec,让 systemd 明确知道主进程就是 uvicorn 本身,而不是 shell 包装;第二,关掉 uvicorn 自己的多 worker,交给 systemd 模板管:
[Service]
Type=exec
ExecStart=/opt/app/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000
KillMode=mixed
TimeoutStopSec=20
Restart=on-failure
RestartSec=5
KillMode=mixed 的好处是 SIGTERM 只发给主进程,给它机会自己清理 worker,超时后再对整个 cgroup 发 SIGKILL,不会出现主进程死了 worker 还活着的情况。
代价是放弃了热重载,每次改代码要 systemctl restart,在印度这种网络下反而更稳,因为机器本身就不适合跑 dev 模式。
边界条件也得说清楚:如果服务里有超过 20 秒的长任务(比如导出报表),TimeoutStopSec=20 会直接砍掉,得按业务调大;另外 Restart=on-failure 意味着正常退出不会自动拉起,如果你的进程会因为内存压力被 OOM killer 杀掉,这条要改回 always,但那就得确保 KillMode 配对了。我在这台机器上就踩过一次:OOM 后没拉起,第二天早上才发现。
你们在印度这边托管 Python 服务,是用 systemd 直接管 uvicorn,还是套一层 gunicorn 或者 supervisor?多 worker 的场景下优雅退出一般给多少秒?
0 条评论
还没有评论,写下第一条。