后端工程FastAPI异步高并发连接池限流降级
FastAPI 高并发与后端工程
Python 异步后端的关键坑:async def 里不能有阻塞调用、worker 模型、连接池、HTTP 客户端复用、限流熔断降级、幂等,以及 MySQL/Redis/JWT/SSE 速记。
FastAPI 高并发与后端工程
FastAPI 是 Python 高性能异步框架,但「会写」和「能在高并发下正确运行」是两回事。
最高频事故:async def 里的阻塞调用
FastAPI 的 async def 端点跑在事件循环里。如果里面出现阻塞调用(同步的数据库查询、requests 同步请求、CPU 密集计算、time.sleep),会卡死整个事件循环,所有请求都被拖慢——这是「看起来 LLM 慢、其实是自己堵住」的元凶。
# ❌ 错误:async def 里用同步阻塞调用
@app.get("/ask")
async def ask(q: str):
result = requests.post(llm_url, json={"q": q}) # 阻塞!卡死事件循环
return {"answer": result.json()}
# ✅ 正确:同步调用丢给线程池,或用异步客户端
@app.get("/ask")
async def ask(q: str):
result = await asyncio.to_thread(requests.post, llm_url, json={"q": q})
return {"answer": result.json()}
该怎么选
| 场景 | 写法 |
|---|---|
| 纯 IO、下游有异步客户端 | async def + await |
| 只能同步(同步 DB 驱动) | 用 def,FastAPI 会自动丢到线程池执行,不阻塞事件循环 |
| CPU 密集(embedding、解析、图像处理) | 进程池 / 独立推理服务 / 消息队列,绝不放事件循环 |
Worker 模型与连接池
- worker 模型:
uvicorn单进程单事件循环。生产两种:gunicorn + uvicorn worker(按 CPU 核数起,通常2×核数+1起手再压测校准),或 K8s 每容器 1 worker + 多副本(对 HPA 更友好) - 连接池:池大小要与 worker 数、每 worker 并发匹配——池不是越大越好,数据库有最大连接数上限,池过大反而把 DB 打挂。获取连接的超时必须设,否则请求会无限等待
- HTTP 客户端复用:
httpx.AsyncClient全局复用一个实例(连接池 + keep-alive),别每次请求新建——这也是 TTFT 优化的一部分
真实踩坑
AI 小灵踩过「单连接被多线程并发复用导致 InterfaceError」的坑,修法是连接池 + 每请求借一条连接(保证单连接同时只有一个使用者)。
超时三件套(必须设)
- 上游 LLM 调用超时
- 连接池获取超时
- 整体请求超时
没有超时的系统在故障时会无限堆积。
高并发七层手段
graph LR
A[缓存] --> B[信号量]
B --> C[队列]
C --> D[优先级]
D --> E[多副本/凭证路由]
E --> F[降级]
F --> G[背压]
- 缓存:精确缓存(相同 query)+ 语义缓存(相似 query),命中即 0 成本 0 延迟
- 并发信号量:
asyncio.Semaphore限住对上游的实际并发数,保护自己和上游的第一道闸 - 队列削峰:入队即返回,worker 匀速消费——长任务必须异步化
- 优先级排队:交互式请求优先,离线批处理让路
- 多副本 + 多凭证路由:多副本水平扩展;多个 API Key/供应商轮询分摊配额
- 降级:换小模型、返回缓存结果、模板兜底——有损但可用,不能直接 500
- 背压与快速失败:队列满就明确拒绝(含
Retry-After),队列无限堆积比直接拒绝更糟
限流 / 熔断 / 降级三件套
| 手段 | 目的 | 关键点 |
|---|---|---|
| 限流 | 挡掉过量请求 | 令牌桶/漏桶/滑动窗口;按用户/租户/接口/全局分层;网关粗 + 应用细 |
| 熔断 | 防止故障扩散 | 三态:关闭 → 打开 → 半开;打开期间快速失败 |
| 降级 | 牺牲非核心保核心 | 模型降级(strong→fast→缓存→模板兜底)、功能降级、结果降级(部分结果 + 风险提示) |
Agent 特有:任务级预算——一次 Agent 任务可能调几十次 LLM,必须把 token 预算和时间预算作为整个任务的硬约束,超预算就中止并返回当前最优结果 + 风险提示。
幂等与并发写
- 幂等:唯一约束 / 幂等键 + 状态机,同一幂等键重复提交返回首次结果
- append-only 表的并发安全不靠「单连接串行化」,而靠:① 每条是独立原子
INSERT(无读改写、无长事务,天然免锁)② 唯一约束防重 ③ 连接池保证单连接同一时刻只有一个使用者
后端速记(MySQL / Redis / JWT / SSE)
MySQL
- B+ 树索引:叶子存数据且有序链表,范围查询高效
- 最左前缀:联合索引 (a,b,c),条件必须从最左列开始
- 覆盖索引:查询列都在索引里,免回表
- 隔离级别:读未提交 → 读已提交 → 可重复读(MySQL 默认) → 串行化
Redis(面试必问三连)
- 穿透:查不存在的数据 → 布隆过滤器 / 缓存空值
- 击穿:热点 key 过期瞬间 → 互斥锁 / 逻辑过期
- 雪崩:大量 key 同时过期 → 过期时间加随机值 / 集群
JWT
无状态、验签不查库;缺点无法主动失效 → 短过期 + refresh token;必须 HTTPS。
SSE vs WebSocket
- SSE:单向(服务端→客户端)、HTTP 复用、自动重连;够用且简单
- WebSocket:双向全双工;需要双向/实时交互时用
- 项目:服务端推任务事件 → SSE 足够