自建的 API 平台前端页面突然显示 “Server is shutting down”,所有请求返回 503。
背景
这个平台是一个三层架构:FastAPI 后端网关(:8000)管理多个动态服务,前面套了 Cloudflare。后端通过 systemd 管理,启动脚本是一个 bash wrapper(run.sh),负责 build 前端、启动 uvicorn、监听部署重启信号。
几天前推了一批代码更新,包含一个新的配置验证模块,会在生产环境检查 JWT secret 等安全配置。推完之后就没管了。直到发现前端全挂。
排查过程
第一步:连不上服务器
SSH 连接直接被拒:
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@known_hosts 里的主机密钥过期了。清掉旧密钥重新连接:
ssh-keygen -R xxx.xxx.xxx.xxx第二步:进程还活着?
SSH 上去后先看进程:
ps aux | grep uvicorn | grep -v grepuvicorn 主进程和所有子服务进程都还在跑,从 5 天前就没重启过。看起来”活着”,但所有请求都是 503。
第三步:systemd service 启动失败
systemctl status api显示 failed,exit code 203/EXEC。一看 service 文件:
ExecStart=/root/api/venv/bin/python3 /root/api/run.py两个错误:
- venv 路径是
/root/api/venv/,实际是/root/api/.venv/(少了个点) - 入口文件是
run.py,实际是run.sh
修完路径后,exit code 变成了 2 —— Python 能跑了但报错。
第四步:配置验证阻断启动
查日志:
journalctl -u api -n 50 --no-pagerConfig validation error: [jwt_secret] Using insecure default JWT secretConfiguration validation found 1 error(s) and 1 warning(s)SystemExit: Configuration validation failed with 1 error(s).Fix the configuration issues above before starting in production.新代码里加了启动时配置验证,检测到 jwt_secret 还是默认的 "change-me",在 ENV=prod 下直接 SystemExit。
修复:生成一个真实的 secret 写入 .env:
echo "JWT_SECRET=$(openssl rand -hex 32)" >> /root/api/.env第五步:端口被占用
配置验证过了,但又报新错:
[Errno 98] error while attempting to bind on address ('127.0.0.1', 8000): address already in use5 天前的旧 uvicorn 进程还占着 8000 端口。而且这个进程已经处于 shutdown 状态 —— 它收到过 SIGTERM 但没有真正退出,内部的 _shutdown_initiated 标志被设为 True,所有新请求都被中间件拦截返回 503。
第六步:找到真正的 service
用 pstree 追溯旧进程的父进程:
pstree -sp <PID>发现它不属于 api.service,而是 api-platform.service —— 这才是真正在用的 systemd unit。api.service 是后来误建的。
查环境变量确认:
cat /proc/<PID>/environ | tr '\0' '\n' | grep SYSTEMDSYSTEMD_EXEC_PID=1431156MEMORY_PRESSURE_WATCH=/sys/fs/cgroup/system.slice/api-platform.service/memory.pressure第七步:完整还原
# 禁用多余的 servicesystemctl disable apirm /etc/systemd/system/api.servicesystemctl daemon-reload
# 强杀僵死进程kill -9 <旧PID>
# 重启正确的 servicesystemctl restart api-platform根因
一次 webhook 自动部署(git pull)拉取了包含配置验证模块的新代码,触发了 run.sh 的重启流程。重启时:
run.sh给旧 uvicorn 发了 SIGTERM- uvicorn 的
GracefulShutdownMiddleware设置了 shutdown 标志,但进程没有退出 - systemd 重启
run.sh,新的run.sh重新 build 并启动 uvicorn - 新 uvicorn 在启动时被配置验证拦截(
jwt_secret是默认值),SystemExit - 旧进程继续占着端口,处于 shutdown 状态,所有请求返回 503
经验总结
- 配置验证要渐进式上线:新加的
fail_on_errors逻辑直接在生产环境SystemExit,应该先跑一段时间 warning-only 模式,确认所有环境都配好了再切成 hard fail。 - systemd service 文件要版本控制:路径写错、入口文件写错这种问题,如果 service 文件在 repo 里通过部署脚本安装,就不会出现手动编辑导致的漂移。
- graceful shutdown 需要有超时强杀机制:
GracefulShutdownMiddleware设了 shutdown 标志但进程不退出,应该在 grace period 结束后主动调用sys.exit()而不是只拒绝新请求。