Skip to content

第 22 章 · 生产部署实战

本章目标:理解生产部署的核心概念模型,掌握多 worker 调优、Docker 镜像构建最佳实践与健康检查、优雅关闭。

22.1 部署的概念模型

部署 FastAPI(或任何 Web API)时,真正要关心的只有六件事:

  1. 安全(HTTPS)——由应用外部的 TLS 终止代理负责加密;
  2. 开机自启——服务器重启后应用自动拉起;
  3. 崩溃重启——进程挂掉后自动恢复;
  4. 副本数(replication)——跑多少个进程来利用多核;
  5. 内存——控制每个进程的用量;
  6. 启动前置步骤——迁移数据库等。

理解一个关键分层:ASGI 服务器(Uvicorn)运行你的应用,但 HTTPS 通常不归它管。TLS 终止代理(Traefik / Caddy / Nginx + Certbot / 云负载均衡)在外层解密流量后转发给 Uvicorn。而"保活与重启"由 Docker/Kubernetes/systemd/Supervisor 这类进程管理器负责。各司其职,不要让 Python 进程去干代理的活。

客户端 --HTTPS--> TLS 终止代理(443) --HTTP--> Uvicorn/FastAPI (8000, 内网)

22.2 fastapi run 与多 worker 调优

开发用 fastapi dev;生产环境官方 CLI 提供对应的生产模式:

bash
# 单进程
fastapi run main.py --port 80

# 多 worker:主进程 + 4 个工作子进程
fastapi run main.py --workers 4

# 等价的原生方式
uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4

# 若部署在 Nginx/Traefik 等代理后面,需要信任代理头
fastapi run app/main.py --port 80 --proxy-headers

--workers 4 会启动 1 个父进程(进程管理器)+ 4 个 worker 进程,各自独立处理请求,从而利用多个 CPU 核。

worker 数量调优经验:

  • CPU 密集型为主 → 接近 CPU 核数
  • I/O 密集型为主(典型 API 应用)→ 可以更高,如 (2 × CPU) + 1
  • 上限受内存约束:每个 worker 是完整独立进程,内存占用线性叠加;
  • 容器/Kubernetes 场景例外:通常每容器只跑 1 个 Uvicorn 进程,横向扩容交给副本数(pod 数),避免双重复制导致资源超卖。

不要用 fastapi dev 上生产

dev 模式带自动重载和调试功能,性能与安全性都不适合生产;生产一律 fastapi run 或裸 uvicorn

22.3 Dockerfile 最佳实践

官方推荐的 Dockerfile 结构(每一行都有讲究):

dockerfile
FROM python:3.13

WORKDIR /code

# ① 先只拷贝依赖清单 —— 它很少变
COPY ./requirements.txt /code/requirements.txt

# ② 再装依赖 —— 依赖清单没变时直接命中 Docker 缓存,秒级完成
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

# ③ 最后才拷贝业务代码 —— 变化最频繁,放最后保证缓存有效
COPY ./app /code/app

# ④ 必须用 exec 形式的 CMD(JSON 数组)
CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"]

四个关键细节:

  1. 分层顺序即缓存策略:requirements.txt 几乎不变,放前面;app/ 天天变,放后面。顺序错了每次改一行代码都要重装全部依赖。
  2. --no-cache-dir 是 pip 的选项,不把下载的包留在镜像里,减小体积。
  3. CMD 必须用 exec 形式["fastapi", "run", ...]):shell 形式(CMD fastapi run ...)会让命令跑在 shell 子进程里,无法正确接收 SIGTERM 信号,优雅关闭失效。
  4. 配套的 .dockerignore 避免把垃圾拷进镜像:
text
.git
__pycache__
*.pyc
.env        # 永远不要打进镜像!
docs
tests
.venv

构建与运行:

bash
docker build -t myapp .
docker run -d --name mycontainer -p 80:80 myapp

22.4 健康检查与优雅关闭

健康检查端点

负载均衡器/K8s 需要一个轻量端点探测进程是否存活:

python
from fastapi import FastAPI

app = FastAPI(lifespan=lifespan)


@app.get("/health", include_in_schema=False)  # 不进公开文档
async def health():
    return {"status": "ok"}

注意存活探针应保持极轻——不要在里面做重量级数据库查询(探针失败会被重启,误判雪崩);深度检查拆成独立的 /ready 就绪探针:

python
from fastapi import Response, status
from fastapi.responses import JSONResponse
from sqlalchemy import text


@app.get("/ready", include_in_schema=False)
async def ready(response: Response):
    """就绪探针:检查真实依赖(数据库)是否可用。"""
    try:
        engine = app.state.engine
        async with engine.connect() as conn:
            await conn.execute(text("SELECT 1"))
        return {"status": "ready"}
    except Exception:
        # 依赖不可用时不返回 200,让 K8s 摘除该实例的流量
        return JSONResponse(status_code=503, content={"status": "not ready"})

部署后还可以写一个冒烟脚本,在发布流水线里验证新版本真的健康:

python
# smoke_test.py —— 发布后自动运行,任一断言失败则回滚
import sys
import httpx

BASE = "https://api.example.com"

resp = httpx.get(f"{BASE}/health", timeout=5.0)
assert resp.status_code == 200, f"健康检查失败: {resp.status_code}"

resp = httpx.get(f"{BASE}/openapi.json", timeout=5.0)
assert resp.status_code == 200 and "/items/" in resp.text, "OpenAPI 文档异常"

print("✅ 部署冒烟测试通过")
sys.exit(0)

程序化启动与优雅关闭

滚动发布时,旧进程要"处理完手头的请求再退出"。Uvicorn 收到 SIGTERM 后会停止接收新连接、等待存量请求完成再退出,等待上限可用参数控制:

bash
uvicorn main:app --timeout-graceful-shutdown 20  # 最多宽限 20 秒

这也是 CMD 必须写 exec 形式的原因:PID 1 直接是 uvicorn 进程才能收到信号;包在 shell 里信号就丢失了,只能被强杀,正在处理的请求全部中断。

除了命令行方式,也可以在 Python 里程序化启动生产服务器,方便嵌入自定义启动逻辑:

python
# serve.py —— python serve.py 等价于 uvicorn main:app --workers 4
import uvicorn

if __name__ == "__main__":
    uvicorn.run(
        "main:app",
        host="0.0.0.0",
        port=8080,
        workers=4,
        proxy_headers=True,               # 信任 Nginx/Traefik 传来的 X-Forwarded-*
        timeout_graceful_shutdown=20,     # 优雅关闭宽限 20 秒
        access_log=False,                 # 生产可关访问日志,交给代理记录
    )

uvicorn.run() 内部同样会安装 SIGTERM/SIGINT 处理器:收到信号后停止 accept 新连接 → 等待存量请求完成(不超过宽限期)→ 执行 lifespan 的 shutdown 段释放资源 → 退出。K8s 滚动更新正是依赖这条链路做到零请求损失的。

22.5 本章小结

  • 部署六要素:HTTPS、自启、重启、副本、内存、前置步骤;HTTPS 由外层 TLS 终止代理负责,Python 不管加解密;
  • 生产用 fastapi run(非 dev),多 worker 利用多核,数量按 CPU/IO 特征权衡,容器内通常单进程+多副本;
  • Dockerfile 黄金顺序:先 COPY requirements → 装依赖 → 再 COPY 代码;CMD 用 exec 形式保证信号可达;
  • .env 与敏感信息绝不进镜像,用 .dockerignore 兜底;
  • 提供 /health 存活探针;Uvicorn 默认响应 SIGTERM 优雅排空请求,--timeout-graceful-shutdown 控制宽限时长。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. HTTPS 加密(TLS 终止)在生产架构中通常由谁负责?

2. 为什么 CMD 要用 exec 形式 CMD ["fastapi","run","main.py"]?

3. Dockerfile 中先 COPY requirements.txt 安装依赖、最后才 COPY 业务代码,目的是?

4. 在 Kubernetes 中部署 FastAPI,关于进程副本的推荐做法是?

🛠️ 动手实践

  1. 把你的 todos 应用写成完整 Docker 工程:.dockerignore + 分层 Dockerfile,构建后 docker run 验证 /health 可访问。
  2. 在 docker-compose.yml 中编排 FastAPI + Nginx:Nginx 监听 443(自签证书即可)做 TLS 终止并反代到应用容器。
  3. 写一个耗时 3 秒的慢接口,分别在 shell 形式与 exec 形式 CMD 下 docker stop 容器,对比日志观察优雅关闭的差异。

恭喜完成 FastAPI 全部 22 章学习!建议回到课程导学回顾知识地图,并用动手实践项目巩固。