
目录一、坑的起点开发环境 vs 生产环境的根本差异二、坑 1uvicorn --reload 直接上了生产现象根因解决方案三、坑 2workers 数量配错8 核开了 16 个 worker现象根因实测数据解决方案四、坑 3async 里调了同步库事件循环被堵死现象排查过程根因解决方案五、坑 4CORS 配了 allow_origins[*]现象根因解决方案六、坑 5内存泄漏到 Pod 被 K8s OOMKill现象排查过程解决方案七、正确的生产部署配置模板八、压测数据优化前后对比九、踩坑总结表十、版本依赖和适用边界总结一、坑的起点开发环境 vs 生产环境的根本差异FastAPI 的开发体验确实是 Python Web 框架里最好的——uvicorn main:app --reload一行命令跑起来改代码自动热加载调试极其方便。但问题就出在这开发时用--reload没问题生产环境用它就是灾难。开发时单 worker 没问题生产时不开多 worker 就是浪费 CPU。开发时 CORS 全开没问题生产时全开就是安全漏洞。先看开发环境 vs 生产环境的根本差异维度开发环境生产环境启动命令uvicorn --reloadgunicorn uvicorn workerWorker 数1CPU 核数 × 2 1并发模型单进程异步多进程 每进程异步CORS* 全开白名单域名日志print结构化日志 日志收集异常处理显示完整堆栈屏蔽内部信息连接池默认配置按并发量调优很多人在开发时跑得很好上了生产就各种问题根本原因就是直接把开发配置搬上去了。二、坑 1uvicorn --reload 直接上了生产现象线上 API 偶发卡顿排查发现 CPU 在 30% 左右但响应时间不稳定。看进程# 线上启动命令错误示范 uvicorn main:app --host 0.0.0.0 --port 8000 --reloadQPS 压测数据并发数P50 延迟P99 延迟QPS现象1045ms120ms~180正常50380ms1200ms~95开始卡100900ms3000ms~48严重卡顿根因--reload模式下 uvicorn 会启动一个文件监控线程watchdog每次文件变化都会重启应用。在容器环境里这个文件监控线程本身吃 CPU 不说它还会和事件循环抢占 GIL。并发上来后GIL 切换开销急剧增加。更关键的是——单进程。uvicorn 默认是单进程的你开了--reload只会启动一个 worker8 核 CPU 只用了 1 个核。另外 7 个核在围观。解决方案生产环境必须用 Gunicorn 管理多个 Uvicorn worker# 生产启动命令 gunicorn main:app \ -w 5 \ -k uvicorn.workers.UvicornWorker \ -b 0.0.0.0:8000 \ --timeout 120 \ --graceful-timeout 60 \ --keep-alive 5 \ --access-logfile - \ --error-logfile -或者在 Dockerfile / 启动脚本里# Dockerfile CMD CMD [gunicorn, main:app, \ -w, 5, \ -k, uvicorn.workers.UvicornWorker, \ -b, 0.0.0.0:8000, \ --timeout, 120, \ --graceful-timeout, 60]参数解释-w 5worker 数公式CPU核数 × 2 1。4 核机器用 92 核用 5。别开太多——每个 worker 是独立进程吃内存。-k uvicorn.workers.UvicornWorker用 Uvicorn 的 worker 类让每个 worker 进程内部跑异步事件循环。--timeout 120worker 120 秒不响应就重启。如果接口有长耗时任务调大这个值。--graceful-timeout 60优雅退出 60 秒让正在处理的请求跑完。三、坑 2workers 数量配错8 核开了 16 个 worker现象上线后发现服务器内存吃紧8GB 内存占了 6GBPod 频繁被 K8s 的 OOMKiller 杀掉。# 错误配置8核开了16个worker gunicorn main:app -w 16 -k uvicorn.workers.UvicornWorker根因每个 Uvicorn worker 是独立的 Python 进程每个进程都有独立的内存空间和对象。FastAPI 应用里光 SQLAlchemy 引擎、连接池、模型定义这些就占 200-300MB。16 个 worker 就是 16 × 250MB ≈ 4GB再加上 Python 解释器本身、依赖库、运行时对象8GB 内存很快见底。workers 数量不是越多越好推荐 workers min(CPU核数 × 2 1, 可用内存 ÷ 单进程内存 × 0.8)实测数据Workers内存占用CPU 利用率QPS稳定性10.3 GB12%180稳定但浪费5推荐1.5 GB65%820稳定94核×212.7 GB88%1280稳定16错误4.8 GB92%980不稳定 OOM16 个 worker 反而比 9 个 QPS 低——因为进程间上下文切换的开销超过了并行收益。解决方案# 4C8G 机器推荐配置 gunicorn main:app \ -w 5 \ # 4核×219但内存只有8G取5 -k uvicorn.workers.UvicornWorker \ --max-requests 1000 \ # 每个worker处理1000个请求后重启防止内存泄漏 --max-requests-jitter 100 \ # 加随机抖动避免所有worker同时重启 -b 0.0.0.0:8000关键--max-requests--max-requests-jitter。这是解决 FastAPI 内存泄漏最简单有效的方法。每个 worker 处理 1000 个请求后自动重启释放所有内存。jitter 加 100 的随机量防止所有 worker 同时重启导致服务中断。四、坑 3async 里调了同步库事件循环被堵死现象接口大部分时候响应很快但偶尔整个服务假死——所有请求都超时等 30 秒才恢复。排查过程查日志发现假死前有一条请求访问了数据库。代码是这样的# 错误代码async 接口里调用了同步的数据库驱动 app.get(/user/{user_id}) async def get_user(user_id: int): # psycopg2 是同步库会阻塞事件循环 conn psycopg2.connect(DATABASE_URL) cursor conn.cursor() cursor.execute(SELECT * FROM users WHERE id %s, (user_id,)) result cursor.fetchone() conn.close() return result根因FastAPI 的 async 接口跑在一个事件循环里。事件循环的核心是遇到 I/O 就切走等 I/O 完了再回来。但psycopg2 是同步库——它执行cursor.execute()时会阻塞当前线程事件循环被卡住整个 worker 里的所有请求都被堵死。假死时的请求队列状态事件循环队列 [请求A] → 调用 psycopg2 → 阻塞等数据库响应可能3-5秒 [请求B] → 排队等事件循环空闲等请求A释放 [请求C] → 排队等... [请求D] → 排队等... ... 30秒后请求A完成 → 请求B/C/D才依次执行解决方案方案一用异步数据库驱动推荐# 正确写法用 asyncpg异步 PostgreSQL 驱动 import asyncpg app.get(/user/{user_id}) async def get_user(user_id: int): conn await asyncpg.connect(DATABASE_URL) result await conn.fetchrow( SELECT * FROM users WHERE id $1, user_id ) await conn.close() return dict(result)方案二如果必须用同步库用run_in_executor扔到线程池# 把同步调用扔到线程池不阻塞事件循环 import asyncio import functools app.get(/user/{user_id}) async def get_user(user_id: int): loop asyncio.get_event_loop() # 同步函数扔到线程池执行 result await loop.run_in_executor( None, # 使用默认线程池 functools.partial(sync_db_query, user_id) ) return result def sync_db_query(user_id: int): 同步数据库查询函数 conn psycopg2.connect(DATABASE_URL) cursor conn.cursor() cursor.execute(SELECT * FROM users WHERE id %s, (user_id,)) result cursor.fetchone() conn.close() return result方案三把接口从async def改成defFastAPI 会自动放到线程池# 最简单的修复去掉 asyncFastAPI 自动放到线程池执行 app.get(/user/{user_id}) def get_user(user_id: int): # 不是 async def conn psycopg2.connect(DATABASE_URL) cursor conn.cursor() cursor.execute(SELECT * FROM users WHERE id %s, (user_id,)) result cursor.fetchone() conn.close() return result方案改动量性能推荐场景换异步驱动大★★★★★新项目/有精力重构run_in_executor中★★★★老项目局部修复去掉 async小★★★快速修复上线怎么排查哪些接口有这个问题用asyncio.set_event_loop_policy的 debug 模式import asyncio import logging logging.basicConfig(levellogging.DEBUG) asyncio.get_event_loop().set_debug(True)如果某个同步调用阻塞超过 0.1 秒会打印Executing took 0.234 seconds警告。五、坑 4CORS 配了 allow_origins[*]现象安全扫描报告了一个高危漏洞CORS 配置不当允许任意域名跨域访问 API。根因FastAPI 的 CORS 中间件配置# 错误配置生产环境全开 from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins[*], # 任意域名都能跨域请求 allow_credentialsTrue, # 还带 Cookie allow_methods[*], allow_headers[*], )allow_origins[*]allow_credentialsTrue是一个组合性安全漏洞任意网站的 JS 都可以向你的 API 发请求浏览器会自动带上用户的 Cookie攻击者可以在自己的网站上模拟用户操作你的 API解决方案# 正确配置白名单域名 from fastapi.middleware.cors import CORSMiddleware import os ALLOWED_ORIGINS [ https://app.example.com, # 前端域名 https://admin.example.com, # 后台管理 http://localhost:3000, # 本地开发只在开发环境加 ] # 从环境变量读取方便不同环境不同配置 extra_origins os.getenv(EXTRA_CORS_ORIGINS, ) if extra_origins: ALLOWED_ORIGINS.extend(extra_origins.split(,)) app.add_middleware( CORSMiddleware, allow_originsALLOWED_ORIGINS, allow_credentialsTrue, allow_methods[GET, POST, PUT, DELETE], # 不要用 * allow_headers[Authorization, Content-Type], # 不要用 * )配置项开发环境生产环境allow_origins[*] 可以具体域名白名单allow_credentialsTrueTrue需配合白名单allow_methods[*][GET,POST,PUT,DELETE]allow_headers[*][Authorization,Content-Type]六、坑 5内存泄漏到 Pod 被 K8s OOMKill现象服务运行 2-3 天后内存持续上涨最终被 K8s OOMKill 重启。重启后正常2-3 天后再次泄漏。# K8s 事件 Warning OOMKilling Pod memory exceeded 2Gi 14m (x3 over 2d)排查过程用 tracemalloc 精确定位内存增长import tracemalloc import gc # 在应用启动时开启内存追踪 tracemalloc.start(10) # 保留10层调用栈 app.get(/debug/memory) async def memory_snapshot(): snapshot tracemalloc.take_snapshot() top_stats snapshot.statistics(lineno) return { traced_memory: tracemalloc.get_traced_memory(), top_allocations: [ { file: stat.traceback.frame.filename, line: stat.traceback.frame.lineno, size_mb: round(stat.size / 1024 / 1024, 2) } for stat in top_stats[:10] ] }查出来最大的泄漏源是SQLAlchemy 的 Session 没有正确关闭# 错误代码Session 没关闭 from sqlalchemy.orm import Session app.get(/orders) async def get_orders(): db SessionLocal() # 创建 Session orders db.query(Order).all() # 查询 # 忘了 db.close()Session 里的对象不会被垃圾回收 return orders每次请求创建一个 Session 但不关闭Session 里持有的 ORM 对象含数据库连接、查询缓存、identity map全部留在内存里。每请求约泄漏 0.5-2MB一天 10 万请求就是 50-200GB。解决方案# 正确写法用依赖注入FastAPI 自动管理 Session 生命周期 from fastapi import Depends from sqlalchemy.orm import Session def get_db(): db SessionLocal() try: yield db finally: db.close() app.get(/orders) async def get_orders(db: Session Depends(get_db)): orders db.query(Order).all() return orders # 函数结束后 get_db 的 finally 会自动 close再加上 Gunicorn 的--max-requests兜底# 每个worker处理1000个请求后自动重启 gunicorn main:app \ -w 5 \ -k uvicorn.workers.UvicornWorker \ --max-requests 1000 \ --max-requests-jitter 100修复后内存数据运行时长修复前内存修复后内存1 小时0.8 GB0.35 GB12 小时1.6 GB0.42 GB24 小时2.4 GBOOMKill0.48 GB72 小时-0.52 GB七、正确的生产部署配置模板# gunicorn.conf.py —— 生产环境完整配置 import multiprocessing import os # 自动计算 worker 数限制在 2-9 之间 cpu_count multiprocessing.cpu_count() recommended min(cpu_count * 2 1, 9) bind 0.0.0.0:8000 workers int(os.getenv(GUNICORN_WORKERS, recommended)) worker_class uvicorn.workers.UvicornWorker # 超时配置 timeout 120 # worker 120秒不响应就重启 graceful_timeout 60 # 优雅退出60秒 keepalive 5 # HTTP keep-alive 5秒 # 内存泄漏兜底 max_requests 1000 # 处理1000请求后重启 max_requests_jitter 100 # 随机抖动 # 日志 accesslog - # 输出到 stdout errorlog - # 输出到 stderr loglevel info # 预加载应用减少 worker 启动时间共享数据库连接池 preload_app True # 优雅关闭 capture_output True enable_stdio_inheritance True启动# 使用配置文件启动 gunicorn -c gunicorn.conf.py main:app # Docker 部署 CMD [gunicorn, -c, gunicorn.conf.py, main:app]八、压测数据优化前后对比测试环境4C8G 云服务器wrk 压测接口为数据库查询 JSON 序列化指标优化前优化后提升启动命令uvicorn --reloadgunicorn -c conf-Workers15400%QPS~95~820763%P50 延迟380ms45ms-88%P99 延迟1200ms180ms-85%内存占用持续上涨稳定 0.5GB不再泄漏CPU 利用率12%65%充分利用OOM 频率2-3天一次0消除九、踩坑总结表#坑根因解法影响1--reload 上生产文件监控线程抢占 GIL 单进程用 gunicorn uvicorn workerQPS 暴跌2workers 过多 OOM每个 worker 独立进程内存公式算 workers max-requestsPod 被杀3async 调同步库同步 I/O 阻塞事件循环换异步驱动 / run_in_executor服务假死4CORS 全开allow_origins[*] credentials白名单域名 限定 methods/headers安全漏洞5内存泄漏Session 未关闭 对象累积Depends 管理生命周期 max-requests 兜底定期 OOMKill十、版本依赖和适用边界组件版本备注FastAPI0.115.00.100 均适用Uvicorn0.30.00.25 均适用Gunicorn23.0.021 均适用Python3.113.9 均可3.11 性能最佳SQLAlchemy2.0.02.0 的 async 接口更稳定asyncpg0.29.0替代 psycopg2 的异步方案适用边界此配置适用于4C8G 以上、QPS 100-2000的 API 服务。如果 QPS 超过 2000建议加 Nginx 反向代理 负载均衡多实例部署。如果有长耗时任务30秒考虑用 Celery / Redis 队列异步处理不要让 worker 一直阻塞。如果有 WebSocket 长连接--max-requests会导致长连接断开重连需评估影响。总结FastAPI 上生产的 5 个坑本质上都是开发配置和生产配置混淆导致的进程管理→ 用 Gunicorn 不要用裸 uvicorn资源限制→ workers 公式 max-requests 兜底异步正确性→ async 接口里不要调同步 I/O安全配置→ CORS 白名单methods/headers 精确资源泄漏→ Session 用 Depends 管理 max-requests 重启五板斧走完4C8G 的机器 QPS 从 95 拉到 820内存稳定在 0.5GB不再被 OOMKill。生产部署不是玄学就是把这些细节一个个抠对。