FastAPI 生产部署
校招考察频度:★ 高频(企业级落地·生产部署实战) 生产导向主线:把 FastAPI 从本地
uvicorn main:app --reload搬到大厂生产 Kubernetes 集群,不仅是敲几行启动命令那么简单。面试官高频必考:Gunicorn + Uvicorn 多进程 ASGI 运行模型与 Worker 核算、Lifespan 异步上下文管理器与优雅停机(Graceful Shutdown)、异步数据库连接池打满排查,以及生产 SSE 流式输出。
一、生产进程模型:Gunicorn + UvicornWorker
在单机或裸机部署时,Python 的 GIL 限制了单进程只能利用 1 个 CPU 核心。生产环境通常使用 Gunicorn 作为主进程管理器(Master),UvicornWorker 作为处理 ASGI 异步请求的工作进程(Worker):
# 生产经典启动命令
gunicorn main:app \
-w 4 \
-k uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000 \
--timeout 120 \
--keep-alive 5 \
--access-logfile - \
--error-logfile -| 维度 | Gunicorn Master 进程职责 | Uvicorn Worker 进程职责 |
|---|---|---|
| 角色 | 监工(Supervisor),不直接处理 HTTP 请求 | 真正干活的 ASGI 异步工作单元 |
| 工作内容 | 绑定监听端口、监控 Worker 状态、处理平滑重启(Reload)、Worker 异常崩溃时自动拉起新进程 | 内部独立运行一个 asyncio 事件循环,接收网络事件、解析 HTTP 报文、执行 FastAPI 路由 |
| 内存关系 | 进程间内存完全隔离,不共享任何全局变量 | 每个 Worker 拥有自己独立的数据库连接池、Redis 连接与内存变量 |
生产 Worker 数量核算公式
- 推荐公式:
Workers = (2 * CPU 核心数) + 1 - 容器化(Docker / K8s)环境例外:
- 在云原生 K8s 环境下,更推荐单 Pod 单 Worker(只用 uvicorn 单进程跑),将多核扩展交给 K8s 的 HPA(Pod 自动横向扩缩容)。这样可以让 K8s 更加精准地监控每个 Pod 的 CPU/内存指标,避免单个 Pod 内多进程争抢资源引发 cgroups 节流。
二、生命周期与优雅停机(Lifespan Context Manager)
FastAPI 弃用了老旧的 @app.on_event("startup") 和 @app.on_event("shutdown"),全面转向基于异步上下文管理器的 Lifespan 模式:
from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.database import engine
from app.redis import redis_client
@asynccontextmanager
async def lifespan(app: FastAPI):
# -------- 1. 服务启动阶段 (Startup) --------
print("[Startup] 初始化数据库连接池与 Redis 客户端...")
await engine.connect()
await redis_client.ping()
yield # 服务正常运行,接收外界请求
# -------- 2. 优雅停机阶段 (Graceful Shutdown) --------
print("[Shutdown] 收到终止信号,开始清理资源...")
# 释放数据库物理连接池,防止连接残留造成 MySQL 僵尸连接
await engine.dispose()
await redis_client.close()
print("[Shutdown] 资源释放完毕,安全退出。")
app = FastAPI(lifespan=lifespan)- K8s 优雅退出配合:
- K8s 在停止 Pod 时,会先向进程发送
SIGTERM信号,等待terminationGracePeriodSeconds(默认 30s)。 - Uvicorn 收到
SIGTERM后,会立即停止接收新的 HTTP 请求,但会等待正在处理中的在途请求执行完毕,随后执行lifespan的yield之后代码释放连接池,最后干净退出,实现真正零报错的无损发布。
- K8s 在停止 Pod 时,会先向进程发送
三、生产血泪坑:异步数据库连接池打满排查
在使用 SQLAlchemy (asyncio) 或 Tortoise-ORM 时,高并发下最常见的报错:
TimeoutError: QueuePool limit of size 5 overflow 10 reached, connection timed out.
1. 产生根因
- 连接泄漏(Connection Leak):在路由中手动获取了 DB Connection,但没有放在
async with块中,发生异常时连接未归还物理池。 - 长耗时计算阻塞连接:在持有数据库事务的期间,调用了耗时的第三方外部 HTTP/RPC 接口,导致数据库连接迟迟无法被其他请求复用。
- 连接池容量与 Worker 数量未对齐:
- 假设后端启动了 8 个 Worker 进程,每个 Worker 默认配了
pool_size=20, max_overflow=10。 - 总连接数最大可能飙升到:
8 * 30 = 240条物理连接!如果 MySQLmax_connections只有 151,瞬间把数据库打崩。
- 假设后端启动了 8 个 Worker 进程,每个 Worker 默认配了
2. 生产优化配置
engine = create_async_engine(
DATABASE_URL,
pool_size=10, # 常驻核心连接数
max_overflow=20, # 允许短时突发扩容的连接数
pool_timeout=30, # 排队等待连接的最大秒数
pool_recycle=1800, # 30分钟自动回收连接,防止 MySQL wait_timeout 强行切断连接引发“MySQL server has gone away”
pool_pre_ping=True, # 每次借出连接前发送 PING,确认连接依然存活
)四、SSE 流式响应与 AI 打字机输出
在大模型对话应用中,使用 StreamingResponse 实现服务端向客户端的单向流式推送(Server-Sent Events):
import asyncio
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
async def fake_llm_stream():
words = ["你", "好", "!", "我", "是", "高", "性", "能", "AI", "助", "手", "。"]
for word in words:
yield f"data: {word}\n\n"
await asyncio.sleep(0.05) # 模拟 LLM 吐词延迟
@app.get("/chat/stream")
async def chat_stream():
return StreamingResponse(
fake_llm_stream(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no", # ★ 关键:禁用 Nginx 反代缓冲,否则打字机会被 Nginx 攒满 4KB 才一次性喷出!
}
)- 生产避坑要点:
- 如果前端通过 Nginx 反向代理访问 FastAPI,必须在响应头中加上
X-Accel-Buffering: no,否则 Nginx 会默认开启proxy_buffering,把流式输出在内存中攒够几十个字符才发给前端,导致页面打字机变成“卡顿一阵突然喷出一大坨”。
- 如果前端通过 Nginx 反向代理访问 FastAPI,必须在响应头中加上
面试高频追问
- Gunicorn 和 Uvicorn 分别扮演什么角色? Gunicorn 是进程管理器(Master-Worker 架构),负责管理进程生命周期、故障拉起与多核绑定;Uvicorn 是真正的异步 ASGI 服务器实现,在单个 Worker 内跑
asyncio事件循环处理请求。 - K8s 中部署 FastAPI,推荐开多个 Worker 还是一个? 推荐单个 Pod 只开 1 个 Uvicorn Worker。将多核横向扩展交给 K8s 的 HPA(Pod 自动弹性伸缩),指标采集更精准,防止 Pod 内部多进程争抢触发 cgroups CPU 限速。
- FastAPI 如何保证服务平滑重启不丢在途请求? 使用现代
lifespan上下文管理器配合 K8sSIGTERM信号。服务停止新请求进入,等待在途请求执行完毕后,执行yield后的代码释放数据库和 Redis 连接池再退出。 - SSE 流式传输过 Nginx 时为什么页面打字机效果失效了? Nginx 默认开启了反向代理缓冲区(
proxy_buffering on),会等待数据攒够一定大小(如 4KB)才发给前端。必须在响应头添加X-Accel-Buffering: no或在 Nginx 配置中关闭该路径的代理缓冲。