898 字
4 分钟
生产环境 503 "Server is shutting down" 排查:五个坑叠在一起

自建的 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 里的主机密钥过期了。清掉旧密钥重新连接:

Terminal window
ssh-keygen -R xxx.xxx.xxx.xxx

第二步:进程还活着?#

SSH 上去后先看进程:

Terminal window
ps aux | grep uvicorn | grep -v grep

uvicorn 主进程和所有子服务进程都还在跑,从 5 天前就没重启过。看起来”活着”,但所有请求都是 503。

第三步:systemd service 启动失败#

Terminal window
systemctl status api

显示 failed,exit code 203/EXEC。一看 service 文件:

ExecStart=/root/api/venv/bin/python3 /root/api/run.py

两个错误:

  1. venv 路径是 /root/api/venv/,实际是 /root/api/.venv/(少了个点)
  2. 入口文件是 run.py,实际是 run.sh

修完路径后,exit code 变成了 2 —— Python 能跑了但报错。

第四步:配置验证阻断启动#

查日志:

Terminal window
journalctl -u api -n 50 --no-pager
Config validation error: [jwt_secret] Using insecure default JWT secret
Configuration 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

Terminal window
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 use

5 天前的旧 uvicorn 进程还占着 8000 端口。而且这个进程已经处于 shutdown 状态 —— 它收到过 SIGTERM 但没有真正退出,内部的 _shutdown_initiated 标志被设为 True,所有新请求都被中间件拦截返回 503。

第六步:找到真正的 service#

pstree 追溯旧进程的父进程:

Terminal window
pstree -sp <PID>

发现它不属于 api.service,而是 api-platform.service —— 这才是真正在用的 systemd unit。api.service 是后来误建的。

查环境变量确认:

Terminal window
cat /proc/<PID>/environ | tr '\0' '\n' | grep SYSTEMD
SYSTEMD_EXEC_PID=1431156
MEMORY_PRESSURE_WATCH=/sys/fs/cgroup/system.slice/api-platform.service/memory.pressure

第七步:完整还原#

Terminal window
# 禁用多余的 service
systemctl disable api
rm /etc/systemd/system/api.service
systemctl daemon-reload
# 强杀僵死进程
kill -9 <旧PID>
# 重启正确的 service
systemctl restart api-platform

根因#

一次 webhook 自动部署(git pull)拉取了包含配置验证模块的新代码,触发了 run.sh 的重启流程。重启时:

  1. run.sh 给旧 uvicorn 发了 SIGTERM
  2. uvicorn 的 GracefulShutdownMiddleware 设置了 shutdown 标志,但进程没有退出
  3. systemd 重启 run.sh,新的 run.sh 重新 build 并启动 uvicorn
  4. 新 uvicorn 在启动时被配置验证拦截(jwt_secret 是默认值),SystemExit
  5. 旧进程继续占着端口,处于 shutdown 状态,所有请求返回 503

经验总结#

  • 配置验证要渐进式上线:新加的 fail_on_errors 逻辑直接在生产环境 SystemExit,应该先跑一段时间 warning-only 模式,确认所有环境都配好了再切成 hard fail。
  • systemd service 文件要版本控制:路径写错、入口文件写错这种问题,如果 service 文件在 repo 里通过部署脚本安装,就不会出现手动编辑导致的漂移。
  • graceful shutdown 需要有超时强杀机制GracefulShutdownMiddleware 设了 shutdown 标志但进程不退出,应该在 grace period 结束后主动调用 sys.exit() 而不是只拒绝新请求。
生产环境 503 "Server is shutting down" 排查:五个坑叠在一起
https://blog.lishuyu.top/posts/api-platform-503-debugging/
作者
猫猫魔女
发布于
2026-03-16
许可协议
CC BY-NC-SA 4.0