简介:这份资源围绕FastAPI部署展开,面向具备一定Python基础、希望将FastAPI应用从本地开发推进到生产环境的后端开发者与全栈工程师,同时兼顾需要与JavaScript前端联调的读者。内容涵盖环境准备、应用创建、API逻辑编写、Uvicorn配置、生产部署及CI/CD等环节,并说明FastAPI如何通过OpenAPI文档与JavaScript前端交互。资源以zip压缩包形式提供,整体约6.87MB,文件类型以Python源码与配置文件为主,便于直接运行和二次修改。目前已有1012人学习,适合想系统掌握FastAPI部署流程、补齐生产环境配置与前后端协作经验的开发者参考。
1. fastapi-test 到底在测什么:从本地 uvicorn 到线上容器的最后一公里
很多人第一次听到 fastapi-test 这个词,会以为它只是某个测试脚本的名字。实际上它指向的是一类非常具体的工程动作:把用 FastAPI 写好的服务,从本地uvicorn main:app --reload的开发态,推进到能被外部稳定访问、能扛住并发、能滚动更新的部署态。这中间隔着的不是一行命令,而是一整套关于进程模型、反向代理、健康检查、依赖锁定和日志采集的决策。
我见过太多项目卡在这一步:本地跑得好好的,一上服务器就 502,或者容器起来了但请求全超时。问题往往不在 FastAPI 本身,而在于部署路径没选对、参数没调对。这篇笔记就按我实际落地的顺序,把 fastapi-test 这个动作拆成可复现的步骤,从选型讲到排错,让新手能照着走,熟手能看到边界在哪。
2. 部署形态怎么选:uvicorn 单进程、gunicorn 多 worker 还是容器编排
2.1 三种常见形态的适用边界
FastAPI 基于 ASGI,最底层的运行方式是uvicorn。开发时用--reload没问题,但生产环境如果只跑一个 uvicorn 进程,CPU 多核根本用不上,一个慢请求就能把整个服务堵住。所以生产部署的第一层决策就是:用单进程 uvicorn、用 gunicorn 管理多个 uvicorn worker,还是直接上容器编排。
我一般按并发量和运维复杂度来分。日请求量在几千以内、团队没有专职运维的,用 gunicorn + uvicorn worker 就够了,进程管理简单,日志也集中。如果已经有 Kubernetes 或类似的容器平台,那就把每个 FastAPI 实例做成一个容器,用编排层做副本数和滚动更新,这时候 gunicorn 的 worker 数反而要调小,避免单容器内进程过多导致内存和 CPU 争抢。
| 形态 | 适用场景 | 进程管理 | 典型 worker 数 |
|---|---|---|---|
| uvicorn 单进程 | 本地调试、极低并发内部工具 | 手动 | 1 |
| gunicorn + uvicorn worker | 中小规模生产、无编排平台 | gunicorn 主进程 | CPU 核数 × 2 + 1 |
| 容器 + 编排 | 已有 K8s、需要弹性伸缩 | 编排层 | 每容器 2~4 个 |
选型时还有一个容易被忽略的点:FastAPI 里的后台任务和 WebSocket。如果服务里用了BackgroundTasks或者长连接,gunicorn 的 worker 超时设置会直接影响这些任务的生命周期。我遇到过 worker 被 gunicorn 超时杀掉,导致后台任务只执行了一半的情况,排查了很久才定位到--timeout参数。
2.2 用 gunicorn 拉起 uvicorn worker 的最小命令
下面是我在无编排平台时最常用的启动方式。先确保依赖装好:
pip install fastapi uvicorn gunicorn然后写一个 gunicorn 配置文件gunicorn_conf.py:
# gunicorn_conf.py import multiprocessing # 监听地址和端口 bind = "0.0.0.0:8000" # worker 数量:CPU 核数 * 2 + 1,这是 IO 密集型服务的经验值 workers = multiprocessing.cpu_count() * 2 + 1 # 使用 uvicorn 的 worker 类,保持 ASGI 兼容 worker_class = "uvicorn.workers.UvicornWorker" # 每个 worker 处理请求的超时时间,超过会被主进程杀掉重启 timeout = 120 # 优雅重启时等待正在处理的请求完成的时间 graceful_timeout = 30 # 保持连接时间,配合反向代理的 keepalive keepalive = 5 # 日志输出到标准输出,方便容器采集 accesslog = "-" errorlog = "-" loglevel = "info"启动命令:
gunicorn -c gunicorn_conf.py main:app这里的main:app对应项目里main.py中的app = FastAPI()实例。worker_class必须指定为uvicorn.workers.UvicornWorker,否则 gunicorn 默认用同步 worker,FastAPI 的异步特性就废了。timeout设 120 秒是因为有些报表接口确实跑得久,设太短会频繁重启 worker,设太长又会让卡死的 worker 拖累整体。graceful_timeout是给滚动更新用的,收到重启信号后,worker 会等正在处理的请求结束再退出,避免用户看到 502。
2.3 容器化部署的 Dockerfile 与启动参数
如果走容器路线,Dockerfile 的写法直接影响镜像大小和启动速度。我习惯用多阶段构建,把依赖安装和运行分开:
# 第一阶段:安装依赖 FROM python:3.11-slim AS builder WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir --prefix=/install -r requirements.txt # 第二阶段:运行 FROM python:3.11-slim WORKDIR /app COPY --from=builder /install /usr/local COPY . . EXPOSE 8000 CMD ["gunicorn", "-c", "gunicorn_conf.py", "main:app"]这里--prefix=/install把依赖装到独立目录,第二阶段直接拷贝,避免把编译工具链带进运行镜像。requirements.txt里一定要锁版本,FastAPI 和 uvicorn 的小版本升级有时会改变行为,不锁版本在滚动更新时容易出玄学问题。
容器启动时,gunicorn_conf.py里的workers要改小,因为编排层会起多个副本。我一般设成 2,然后通过副本数来横向扩展。另外bind保持0.0.0.0:8000,让容器外部能访问。
3. 反向代理与健康检查:Nginx 配置和 /health 端点的正确写法
3.1 Nginx 作为反向代理的关键参数
FastAPI 直接暴露 8000 端口不是不行,但生产环境前面通常有一层 Nginx 或类似的代理,负责 TLS 终止、静态文件、限流和请求缓冲。Nginx 配置里和 FastAPI 配合最紧密的几个参数是proxy_set_header和proxy_read_timeout。
upstream fastapi_backend { server 127.0.0.1:8000; keepalive 32; } server { listen 80; server_name _; location / { proxy_pass http://fastapi_backend; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 120s; proxy_connect_timeout 10s; } }proxy_http_version 1.1和proxy_set_header Connection ""是为了启用长连接,减少每次请求建立 TCP 连接的开销。X-Forwarded-For和X-Forwarded-Proto必须传,否则 FastAPI 里拿到的客户端 IP 全是 127.0.0.1,生成的 URL 也会是 http 而不是 https。proxy_read_timeout要和 gunicorn 的timeout对齐,Nginx 这边设短了,后端还在处理,前端已经 504 了。
3.2 健康检查端点为什么不能只返回 200
很多教程里的健康检查就是写一个/health返回{"status": "ok"}。这在简单场景下够用,但如果服务依赖数据库或 Redis,这个端点会在依赖挂掉时依然返回 200,导致编排层以为实例健康,继续把流量打进来。我一般会写一个分层的健康检查:
from fastapi import FastAPI, status from fastapi.responses import JSONResponse import asyncpg app = FastAPI() @app.get("/health") async def health(): # 基础存活检查,不查依赖 return {"status": "alive"} @app.get("/ready") async def ready(): # 就绪检查,查关键依赖 try: conn = await asyncpg.connect("postgresql://user:pass@db:5432/mydb") await conn.execute("SELECT 1") await conn.close() return {"status": "ready"} except Exception as e: return JSONResponse( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, content={"status": "not ready", "detail": str(e)} )/health给编排层做存活探针,只要进程还在就返回 200,避免依赖抖动导致实例被反复重启。/ready给负载均衡做就绪探针,依赖不通时返回 503,把流量摘掉。这两个端点分开,能避免很多“服务明明活着却被杀掉”的翻车现场。
3.3 优雅停机的信号处理
容器编排在停止实例时,会先发 SIGTERM,等一段时间再发 SIGKILL。FastAPI 默认不会处理 SIGTERM,正在处理的请求会被直接切断。我一般会在应用启动时注册信号处理:
import signal import asyncio from contextlib import asynccontextmanager from fastapi import FastAPI @asynccontextmanager async def lifespan(app: FastAPI): # 启动时的初始化 yield # 关闭时的清理,比如关闭数据库连接池 await app.state.db_pool.close() app = FastAPI(lifespan=lifespan) def handle_sigterm(*args): # 触发 uvicorn 的优雅关闭流程 raise KeyboardInterrupt() signal.signal(signal.SIGTERM, handle_sigterm)配合 gunicorn 的graceful_timeout,这样在滚动更新时,正在处理的请求有机会跑完,用户不会看到连接被重置。这个细节在低流量时看不出差别,一旦有长请求,就是 502 和 200 的区别。
4. 避坑与排查:fastapi-test 部署中最容易翻车的五个点
4.1 现象:容器启动后立刻退出,日志只有一行 “Application startup complete”
原因通常是 gunicorn 的worker_class没设成uvicorn.workers.UvicornWorker,或者main:app的路径写错了。如果 worker_class 是默认的同步 worker,FastAPI 的异步启动流程会直接报错退出。解决方法是检查gunicorn_conf.py里的worker_class,并确认main.py里app变量存在且是 FastAPI 实例。
4.2 现象:请求偶尔 502,重启后又正常
这是 worker 被 gunicorn 超时杀掉的典型表现。timeout设得太短,某个慢查询超过阈值,worker 被主进程杀掉,正在排队的请求就 502 了。解决方法是把timeout调到比最慢接口的 P99 还大,同时检查接口里有没有同步阻塞操作。FastAPI 里如果用了requests这种同步库,会阻塞事件循环,让整个 worker 变慢。换成httpx的异步客户端能缓解。
4.3 现象:Nginx 日志里大量 499,客户端提前断开
499 是客户端在服务端返回前主动断开连接。常见原因是 Nginx 的proxy_read_timeout比后端处理时间短,或者客户端等不及了。先对齐 Nginx 和后端的超时时间,然后检查接口是不是真的需要那么久。如果确实有长任务,考虑改成异步任务加轮询,而不是让 HTTP 连接一直挂着。
4.4 现象:内存持续增长,最终 OOM
FastAPI 本身不会泄漏内存,但如果在全局变量里缓存了大量数据,或者数据库连接池没设上限,就会慢慢涨。检查asyncpg或sqlalchemy的连接池配置,max_size不要超过数据库的最大连接数除以副本数。另外 gunicorn 的max_requests可以设一个值,让 worker 处理一定数量请求后重启,作为兜底。
4.5 现象:日志里看不到请求体,排查问题全靠猜
gunicorn 的 access log 只记录状态码和路径,不记录请求体。如果需要在出错时看到请求内容,可以在 FastAPI 里加中间件,但要注意别把敏感信息写进日志。我一般只在调试环境开请求体日志,生产环境用结构化日志记录关键字段,配合 trace id 串联。
5. 进阶技巧:用预热和连接池把冷启动压到最低
部署完成后,真正影响用户体验的往往是冷启动。容器刚起来时,数据库连接池是空的,第一个请求要等连接建立,延迟可能是正常情况的几倍。我的做法是在lifespan里做预热:
@asynccontextmanager async def lifespan(app: FastAPI): # 启动时预建连接池 app.state.db_pool = await asyncpg.create_pool( "postgresql://user:pass@db:5432/mydb", min_size=5, max_size=20, command_timeout=60 ) # 预热:执行一次简单查询,确保连接可用 async with app.state.db_pool.acquire() as conn: await conn.execute("SELECT 1") yield await app.state.db_pool.close()min_size=5让连接池在启动时就建好 5 个连接,第一个请求直接复用,不用等。max_size=20是上限,根据数据库的max_connections和副本数来定,一般留 20% 余量。command_timeout防止某个查询卡死连接。
另一个技巧是给 gunicorn 加--preload,让主进程先加载应用代码,再 fork worker。这样多个 worker 共享同一份代码内存,启动更快,内存占用也更低。但要注意,如果应用在模块级别有状态,--preload会导致状态被复制,可能出问题。我一般只在纯无状态服务上用。
验证预热效果的方法很简单:在容器启动后立刻发一个请求,看响应时间。如果第一次请求和后续请求的延迟差很多,说明预热没做到位。我习惯在部署脚本里加一个循环,等/ready返回 200 后再把流量切进来,这样用户永远不会碰到冷启动。
最后说一个我踩过的坑:有次滚动更新,新实例的/ready很快就返回 200,但数据库连接池其实还没完全建好,结果第一批请求大量超时。后来把/ready改成检查连接池的get_size()是否达到min_size,才彻底解决。部署这件事,细节都在这些不起眼的地方。希望帮到你。
本文还有配套的精品资源,点击获取