FastAPI 的 Logo
由于速度快且易于使用,FastAPI 已经成为构建 Python API 的最流行的框架之一。但在构建高性能应用程序的过程中,你可能会问:你应该使用同步(sync)还是异步(async)代码的执行方式?
本文将通过实际性能测试来比较同步和异步的FastAPI实现方式,并深入分析测试结果,帮助您决定何时使用每种方式。
FastAPI中的同步和异步有什么区别?- 同步代码(Sync):在同步执行中,任务一次只处理一个。每个请求都必须等待前一个请求完成,当有多名用户或遇到慢 I/O 操作(如数据库查询或文件上传)时,可能会导致瓶颈。
- 异步代码(Async):异步执行能让多个请求同时进行处理。应用程序不会等待 I/O 操作(如数据库调用)完成,而是继续处理其他请求,在高并发环境下更为高效。
但是这两者在性能上到底有什么真正的差别呢?让我们一起来看看吧。
FastAPI中的同步与异步性能对比为了比较同步和异步实现,我创建了两个FastAPI应用版本。
- 同步版本:使用传统的阻塞数据库请求,使用
psycopg2
。 - 异步版本:使用
asyncpg
进行非阻塞的异步查询。
两个版本执行一个简单的任务:从 PostgreSQL 数据库查询用户信息。数据库中仅包含少量数据,以便测试同步与异步机制的效果。
技术栈- FastAPI 作为 API 框架的一部分。
- SQLAlchemy 作为 ORM。
- psycopg2 或 psycopg2-binary(同步模式)和 asyncpg(异步模式)作为用于数据库连接的库。
- PostgreSQL 作为数据库系统。
为了测试性能表现,我使用了 Apache 基准测试工具 ab 模拟了总共1000个请求,并保持100个并发连接。
同步版本的代码 💻在同步版本中,我们使用psycopg2驱动程序与SQLAlchemy一起执行阻塞式查询,表通过同步的SQLAlchemy引擎创建。
同步文件: main.py
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from .database import get_db, User
app = FastAPI()
@app.get("/users/{user_id}")
def 获取用户(user_id: int, db: Session = Depends(get_db)):
# 同步阻塞
user = db.query(User).filter(User.c.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="找不到用户")
return {"id": user.id, "name": user.name, "email": user.email}
同步文件:database.py
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "postgresql://user:password@localhost/db_name"
# 创建一个同步的SQLAlchemy引擎
engine = create_engine(DATABASE_URL, echo=True)
# 为同步查询创建一个session maker
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
# 定义元数据
metadata = MetaData()
# 定义User表
User = Table(
"users", metadata,
Column("id", Integer, primary_key=True),
Column("name", String),
Column("email", String),
)
# 在数据库中创建表结构
metadata.create_all(engine)
# 获取同步数据库会话的依赖项
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
💻 异步版本的代码
在 异步版本 下,我们使用 asyncpg 驱动与 SQLAlchemy,执行 非阻塞 的查询。但是,表的创建仍然是同步的,因为 SQLAlchemy 的 metadata.create_all()
方法不支持异步操作。
异步任务 main.py
从 contextlib 导入 asynccontextmanager
从 fastapi 导入 FastAPI, Depends, HTTPException
从 sqlalchemy.ext.asyncio 导入 AsyncSession
从 sqlalchemy 导入 select
从 .database 导入 get_async_db, User, initialize_database
@asynccontextmanager
async def lifespan(app: FastAPI):
# 初始化数据库
await initialize_database()
yield
app = FastAPI(lifespan=lifespan)
@app.get("/users/{user_id}")
async def get_user(user_id: int, db: AsyncSession = Depends(get_async_db)):
result = await db.execute(select(User).where(User.c.id == user_id))
user = result.fetchone()
如果 not user:
raise HTTPException(status_code=404, detail="用户未找到")
返回 {"id": user.id, "姓名": user.name, "电子邮件": user.email}
异步文件: database.py
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy import MetaData, Table, Column, Integer, String
DATABASE_URL = "postgresql+asyncpg://user:password@localhost/db_name"
# 异步引擎用于异步查询
engine = create_async_engine(
DATABASE_URL,
echo=True,
pool_size=10,
max_overflow=20,
)
# 异步会话用于异步查询
AsyncSessionLocal = sessionmaker(
bind=engine, class_=AsyncSession, expire_on_commit=False
)
# 定义元数据
metadata = MetaData()
# 定义用户表
users = Table(
"users",
metadata,
Column("id", Integer, primary_key=True),
Column("name", String),
Column("email", String),
)
# 创建所有表
async def init_db():
async with engine.begin() as conn:
await conn.run_sync(metadata.create_all)
# 依赖以获取异步数据库会话
async def get_async_db():
async with AsyncSessionLocal() as session:
yield session
# 启动:初始化数据库
async def initialize_database():
await init_db()
在这个版本中,请求是异步处理的,这意味着在等待输入输出(I/O)的过程中,可以处理多个请求。
基准指令:-
同步版本:
ab -n 1000 -c <http://127.0.0.1:8000/users/1>
- 异步版本:
ab -n 1000 -c <http://127.0.0.1:8001/users/1>
这是基准测试中性能指标的详细分析,
测试结果:同步 vs 异步(同步/异步)(Airtable 数据库)
性能分析 1. 请求次数:- 异步每秒可以处理 50.68个请求,而同步每秒仅处理 36.89个请求。
- 对比之下,异步处理的请求数量多了37%,因此在并发处理上更胜一筹。
- 异步版本的平均响应时间比同步版本低了27%(1973毫秒对比2710毫秒),说明异步版本在高负载情况下处理请求更高效。
- 两个版本显示出相似的最长请求时间(约4000毫秒),但异步版本的性能更稳定一致,这从响应时间的较小波动中可以看出。
图形对比
下面是一张图表,比较了Sync和Async版本在不同百分位上的表现,请求时间和最长请求耗时。
- 实线表示如下不同分位数的响应时间情况。
- 虚线表示如下同步(2710.648 ms)和异步(1973.057 ms)的平均响应时间情况。
- 点线突出显示如下同步(4167 ms)和异步(3851 ms)的最长请求时间情况。
- 你的应用需要处理高流量和大量并发用户。
- 该应用程序是I/O密集型,有大量数据库查询和API调用。
- 你需要尽量减少大量请求的响应时间。
- 该应用的并发性较低,或者主要执行的是CPU密集任务。
- 您希望代码库更简单,避免处理异步操作带来的复杂性。
- 您的应用不需要扩展来同时处理几百或几千个请求。
虽然异步在这些测试中表现更快,但仍然可以对其进行更多优化,进一步提升它的性能。
- 连接池:使用连接池来复用数据库连接,避免为每个请求创建新的连接。
- 使用异步库:确保所有I/O操作都异步处理以实现最大性能。
- 进行高并发测试:运行高并发的负载测试(如500+用户)以充分发挥异步处理的优势。
engine = create_async_engine(
DATABASE_URL,
pool_size=10,
max_overflow=20
)
让我们来试试吧!
你在 FastAPI 中试过实现 Sync 或 Async 吗?你得到的结果怎么样?分享一下你的经历,或者在你的应用程序上运行同样的基准测试。我很想看看异步在不同负载和应用场景下的表现!
所以,最后我们可以说如果你的应用需要处理大量并发用户,并且主要依赖I/O密集型任务,异步FastAPI 能提供更好的性能、可扩展性和响应速度。然而,对于更简单的情况,
[OOP]: 面向对象编程
[CRUD]: 增删查改
[JVM]: Java虚拟机
[SUT]: 被测系统