1. 为什么需要关注FastAPI中的批量与并发操作?
在Web开发中,处理大量请求或数据时,性能优化是个永恒的话题。最近在FastAPI社区里,关于批量操作和并发操作的讨论越来越多,特别是当涉及到多线程与多进程的选择时,很多开发者都会感到困惑。作为一个长期使用FastAPI构建高性能服务的开发者,我想分享一些实战经验。
先看个真实场景:假设你正在开发一个电商后台系统,需要处理来自前端的批量订单更新请求。前端可能发送一个包含100条订单修改的数组,这时候你有两种基本处理方式:
- 批量操作(Batch Processing):顺序处理每个订单,一个接一个
- 并发操作(Concurrent Processing):同时处理多个订单
选择哪种方式?这取决于你的具体需求、系统资源和业务逻辑。但更关键的是,在FastAPI框架下,如何正确实现这些操作,避免常见的性能陷阱。
提示:在开始前,请确保你已经安装了最新版FastAPI和uvicorn:
bash复制pip install fastapi uvicorn
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. FastAPI的并发模型基础
2.1 FastAPI默认的并发机制
很多人不知道的是,FastAPI本身基于Starlette,而Starlette使用async/await语法实现异步IO。这意味着:
- 当你的路由函数使用
async def定义时,FastAPI会在事件循环中运行它 - 这种模式下,单个Python进程可以同时处理多个请求(通过任务切换)
- 但这并不是真正的并行,而是协作式多任务
python复制from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
# 这是一个异步路由
return {"item_id": item_id}
2.2 Python的全局解释器锁(GIL)影响
Python有个著名的GIL(Global Interpreter Lock),它导致:
- 即使在多核CPU上,一个Python进程也无法真正并行执行多个线程的Python字节码
- I/O密集型任务不受GIL限制(因为等待I/O时会释放GIL)
- CPU密集型任务会被GIL严重限制
这就是为什么在FastAPI中:
- 对于I/O密集型任务(如网络请求、数据库查询),使用异步编程模型非常高效
- 对于CPU密集型任务(如图像处理、复杂计算),可能需要考虑多进程
3. 批量操作的实现与优化
3.1 基本的批量处理实现
假设我们需要处理一个订单批量更新接口,最直接的方式是:
python复制from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List
app = FastAPI()
class OrderUpdate(BaseModel):
order_id: int
status: str
@app.post("/orders/batch-update")
async def batch_update_orders(updates: List[OrderUpdate]):
results = []
for update in updates:
# 模拟处理每个订单
processed = process_single_order(update)
results.append(processed)
return {"results": results}
def process_single_order(update: OrderUpdate):
# 这里是实际的订单处理逻辑
# 可能是数据库操作或其他业务逻辑
return {"order_id": update.order_id, "status": update.status}
这种方式的优点是:
- 实现简单直观
- 保证处理顺序
- 不会因为并发导致资源竞争
缺点是:
- 处理时间随订单数量线性增长
- 无法充分利用多核CPU
3.2 批量操作的性能优化技巧
即使选择顺序处理,也有优化空间:
-
批量数据库操作:使用
bulk_update代替循环中的单个updatepython复制# 使用SQLAlchemy示例 await db.execute( update(Order) .where(Order.id == bindparam('order_id')) .values(status=bindparam('status')), [{"order_id": u.order_id, "status": u.status} for u in updates] ) -
减少重复验证:在循环外部验证所有数据,而不是每个迭代中都验证
-
使用生成器:对于大数据集,使用生成器避免内存爆炸
python复制def process_updates(updates): for update in updates: yield process_single_order(update)
4. 并发操作的多线程实现
4.1 何时选择多线程
多线程适合以下场景:
- I/O密集型任务(如调用外部API、数据库查询)
- 需要共享状态(通过线程安全的数据结构)
- 任务之间有一定独立性
在FastAPI中使用多线程的基本模式:
python复制from concurrent.futures import ThreadPoolExecutor
import asyncio
@app.post("/orders/concurrent-update")
async def concurrent_update_orders(updates: List[OrderUpdate]):
loop = asyncio.get_event_loop()
with ThreadPoolExecutor() as pool:
tasks = [
loop.run_in_executor(pool, process_single_order, update)
for update in updates
]
results = await asyncio.gather(*tasks)
return {"results": results}
4.2 多线程的注意事项
-
线程池大小:不是越大越好,通常推荐:
python复制# 根据CPU核心数设置 max_workers = min(32, (os.cpu_count() or 1) + 4) -
共享状态安全:
- 避免直接修改全局变量
- 使用线程安全的数据结构(如
queue.Queue) - 必要时使用锁(但会降低并发性)
-
数据库连接:
- 确保你的数据库驱动是线程安全的
- 或者为每个线程创建独立连接
警告:在异步函数中直接使用标准库的
threading模块可能导致死锁。推荐始终通过run_in_executor使用线程。
5. 并发操作的多进程实现
5.1 何时选择多进程
多进程适合以下场景:
- CPU密集型任务(如数值计算、图像处理)
- 需要突破GIL限制
- 任务之间独立性高,共享数据少
FastAPI中实现多进程的示例:
python复制from concurrent.futures import ProcessPoolExecutor
@app.post("/orders/parallel-update")
async def parallel_update_orders(updates: List[OrderUpdate]):
loop = asyncio.get_event_loop()
with ProcessPoolExecutor() as pool:
tasks = [
loop.run_in_executor(pool, cpu_intensive_processing, update)
for update in updates
]
results = await asyncio.gather(*tasks)
return {"results": results}
5.2 多进程的特殊考量
-
序列化限制:
- 传递给子进程的参数必须可pickle序列化
- 这意味着不能传递数据库连接等不可序列化对象
-
内存开销:
- 每个进程有独立的内存空间
- 大数据集可能导致内存消耗翻倍
-
启动成本:
- 创建进程比创建线程开销大
- 适合长时间运行的任务,不适合微小任务
-
共享数据:
- 需要使用
multiprocessing模块的特殊数据结构 - 或者通过数据库/Redis等外部存储共享
- 需要使用
6. 性能对比与选择策略
6.1 基准测试数据参考
我在本地做了一个简单测试(处理100个"订单",每个处理耗时约50ms):
| 方法 | 耗时(ms) | CPU使用率 | 内存占用 |
|---|---|---|---|
| 顺序处理 | 5100 | 15% | 低 |
| 多线程(4 workers) | 1300 | 60% | 中 |
| 多进程(4 workers) | 1400 | 400% | 高 |
| 纯异步(无CPU负载) | 550 | 20% | 低 |
6.2 选择决策树
根据我的经验,可以按以下流程选择:
- 是I/O密集型且能异步化? → 使用纯异步
- 是I/O密集型但无法异步化? → 多线程
- 是CPU密集型且任务独立? → 多进程
- 是CPU密集型且需要共享状态? → 可能需要重新设计架构
6.3 混合模式实践
有时候最佳方案是混合使用多种技术。例如:
python复制async def hybrid_processing(updates: List[OrderUpdate]):
# 将任务分类
io_tasks = [u for u in updates if u.type == "io"]
cpu_tasks = [u for u in updates if u.type == "cpu"]
# 并行处理
with ThreadPoolExecutor() as thread_pool, \
ProcessPoolExecutor() as process_pool:
loop = asyncio.get_event_loop()
io_results = await asyncio.gather(*[
loop.run_in_executor(thread_pool, process_io_task, task)
for task in io_tasks
])
cpu_results = await asyncio.gather(*[
loop.run_in_executor(process_pool, process_cpu_task, task)
for task in cpu_tasks
])
return io_results + cpu_results
7. 实战中的陷阱与解决方案
7.1 数据库连接池耗尽
现象:在高并发下突然出现数据库连接错误。
原因:每个线程/进程都创建新连接,超过连接池上限。
解决方案:
- 使用连接池并限制最大连接数
- 为每个工作线程/进程创建独立连接池
- 或者使用像SQLAlchemy这样的ORM,它自带连接池管理
python复制# 使用encode/databases的示例
from databases import Database
database = Database("postgresql://user:password@localhost/dbname")
@app.on_event("startup")
async def startup():
await database.connect()
@app.on_event("shutdown")
async def shutdown():
await database.disconnect()
7.2 内存泄漏
现象:长时间运行后内存占用持续增长。
原因:多进程模式下子进程未正确清理;或全局变量积累。
解决方案:
- 使用
max_workers限制并发数 - 定期重启工作进程(如使用gunicorn的
max_requests参数) - 避免在全局作用域存储可变状态
7.3 任务超时控制
问题:某个慢任务拖累整个系统。
解决方案:为每个任务设置超时
python复制async def update_with_timeout(update):
try:
return await asyncio.wait_for(
process_single_order(update),
timeout=30.0 # 30秒超时
)
except asyncio.TimeoutError:
return {"error": "timeout"}
7.4 优雅停机
问题:当服务重启时,正在执行的任务被强制中断。
解决方案:使用asyncio.Event通知任务该结束了
python复制shutdown_event = asyncio.Event()
@app.post("/shutdown")
async def shutdown_server():
shutdown_event.set()
return {"message": "Shutting down..."}
async def long_running_task():
while not shutdown_event.is_set():
# 执行任务的一部分
await asyncio.sleep(1)
8. 高级技巧与最佳实践
8.1 动态调整并发度
根据系统负载动态调整工作线程/进程数:
python复制import os
import psutil
def get_optimal_workers():
load = os.getloadavg()[0]
cpu_count = os.cpu_count() or 1
free_mem = psutil.virtual_memory().available / (1024 * 1024) # MB
# 简单启发式算法
if load > cpu_count * 0.7:
return max(1, cpu_count // 2)
elif free_mem < 500: # 小于500MB空闲内存
return max(1, cpu_count // 2)
else:
return cpu_count * 2
8.2 使用Job队列解耦
对于大规模批处理,考虑使用消息队列(如Celery+RabbitMQ/Redis):
python复制from celery import Celery
celery = Celery('tasks', broker='pyamqp://guest@localhost//')
@celery.task
def process_order_task(update):
# 处理订单
return result
@app.post("/orders/queue-update")
async def queue_update_orders(updates: List[OrderUpdate]):
tasks = [process_order_task.delay(u.dict()) for u in updates]
return {"task_ids": [t.id for t in tasks]}
8.3 监控与日志
确保良好的可观测性:
- 记录任务开始/结束时间
- 监控线程/进程池的使用情况
- 使用分布式追踪(如OpenTelemetry)
python复制from opentelemetry import trace
tracer = trace.get_tracer(__name__)
async def traced_processing(update):
with tracer.start_as_current_span("process_order"):
# 处理逻辑
return result
8.4 测试策略
针对并发代码的特殊测试需求:
- 模拟慢速I/O测试超时处理
- 注入失败测试错误恢复
- 压力测试资源限制
python复制@pytest.mark.asyncio
async def test_concurrent_updates():
# 模拟100个并发请求
tasks = [client.post("/orders", json=order) for order in test_orders]
responses = await asyncio.gather(*tasks)
assert all(r.status_code == 200 for r in responses)
9. 真实案例:电商订单处理系统优化
去年我参与优化了一个电商平台的订单处理系统,原始实现是简单的顺序处理,高峰期延迟达到10秒以上。经过以下优化步骤:
- 分析瓶颈:使用cProfile发现80%时间花在数据库I/O上
- 引入多线程:将顺序查询改为并发查询,延迟降至3秒
- 批量更新优化:将多个UPDATE合并为单个批量UPDATE
- 连接池调优:调整数据库连接池大小匹配线程池大小
- 缓存预热:对常用数据预先加载到缓存
最终效果:
- 平均延迟:10s → 800ms
- 吞吐量:50 req/s → 300 req/s
- 资源使用:CPU利用率从20%提升到60%,但内存增长可控
关键代码片段:
python复制async def optimize_order_processing(order_ids: List[int]):
# 第一阶段:并发获取订单数据
with ThreadPoolExecutor() as pool:
loop = asyncio.get_event_loop()
orders = await asyncio.gather(*[
loop.run_in_executor(pool, get_order_details, oid)
for oid in order_ids
])
# 第二阶段:批量处理业务逻辑
processed = bulk_process_orders(orders)
# 第三阶段:批量更新数据库
await bulk_update_orders(processed)
return processed
10. 未来演进方向
随着项目规模扩大,我们还在考虑以下优化方向:
- 分片处理:将大任务分解为小任务分片处理
- 优先级队列:区分高优先级和低优先级任务
- 自动扩缩容:基于负载动态调整计算资源
- 更精细的流控:如令牌桶算法控制速率
一个实验性的实现:
python复制from fastapi import BackgroundTasks
@app.post("/orders/priority-update")
async def priority_update(
updates: List[OrderUpdate],
background_tasks: BackgroundTasks
):
urgent = [u for u in updates if u.priority == "high"]
normal = [u for u in updates if u.priority == "normal"]
# 高优先级立即处理
urgent_results = await concurrent_update_orders(urgent)
# 普通优先级后台处理
background_tasks.add_task(process_in_background, normal)
return {"urgent": urgent_results, "normal": "processing in background"}
在FastAPI中处理批量与并发操作是个需要综合考量的话题。经过多个项目的实践,我的体会是:没有放之四海而皆准的最佳方案,必须根据具体场景、数据特性和系统资源来选择最合适的模式。对于刚接触这个问题的开发者,建议从小规模测试开始,逐步验证不同方案的效果,最终找到最适合你业务需求的平衡点。
