FastAPI性能對(duì)比:同步vs異步
FastAPI已成為構(gòu)建Python API的最流行框架之一,因其速度和易用性而廣受歡迎。但在構(gòu)建高性能應(yīng)用程序時(shí),有一個(gè)重要問(wèn)題需要回答:應(yīng)該使用同步(sync)還是異步(async)代碼執(zhí)行?
在本文中,我們將通過(guò)現(xiàn)實(shí)世界的性能測(cè)試,對(duì)同步和異步的FastAPI實(shí)現(xiàn)進(jìn)行基準(zhǔn)測(cè)試,并深入分析相關(guān)數(shù)據(jù),以幫助開發(fā)者決定何時(shí)使用這兩種方法。
一、同步與異步在FastAPI中的區(qū)別是什么?
- 同步代碼(sync):在同步執(zhí)行中,任務(wù)一個(gè)接一個(gè)地處理。每個(gè)請(qǐng)求都需要等待前一個(gè)請(qǐng)求完成,這在用戶數(shù)量較多或存在慢I/O操作(如數(shù)據(jù)庫(kù)查詢或文件上傳)時(shí),可能會(huì)導(dǎo)致瓶頸。
- 異步代碼(async):異步執(zhí)行支持多個(gè)請(qǐng)求并發(fā)處理。應(yīng)用程序無(wú)需等待I/O操作(如數(shù)據(jù)庫(kù)調(diào)用)完成,而是可以繼續(xù)處理其他請(qǐng)求,從而在高并發(fā)環(huán)境中更有效率。
那么,這兩者在性能上的真實(shí)差異是什么呢?讓我們來(lái)看看。
二、設(shè)置:在FastAPI中基準(zhǔn)測(cè)試同步與異步
為了比較同步和異步實(shí)現(xiàn),在這里創(chuàng)建了兩個(gè)版本的FastAPI應(yīng)用程序。
- 同步版本:使用傳統(tǒng)的阻塞數(shù)據(jù)庫(kù)查詢,采用psycopg2。
- 異步版本:使用非阻塞的異步查詢,采用asyncpg。
這兩個(gè)版本都執(zhí)行一個(gè)簡(jiǎn)單的任務(wù):從PostgreSQL數(shù)據(jù)庫(kù)查詢用戶。數(shù)據(jù)庫(kù)中包含極少的數(shù)據(jù),以便隔離同步/異步機(jī)制的影響。
技術(shù)棧:
- 使用FastAPI作為API框架。
- 使用SQLAlchemy作為ORM。
- 使用psycopg2或psycopg2-binary(同步)和asyncpg(異步)進(jìn)行數(shù)據(jù)庫(kù)連接。
- 使用PostgreSQL作為數(shù)據(jù)庫(kù)。
為了測(cè)試性能,使用**Apache Benchmark(ab)**模擬了1000個(gè)請(qǐng)求,具有100個(gè)并發(fā)連接。
2.1 同步版本代碼 ??
在同步版本中,在這里使用psycopg2驅(qū)動(dòng)與SQLAlchemy,后者可執(zhí)行阻塞查詢。表格是使用同步的SQLAlchemy引擎創(chuàng)建的。
同步: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 get_user(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="User not found")
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"
# 創(chuàng)建同步的SQLAlchemy引擎
engine = create_engine(DATABASE_URL, echo=True)
# 創(chuàng)建用于同步查詢的會(huì)話生成器
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
# 定義元數(shù)據(jù)
metadata = MetaData()
# 定義User表
User = Table(
"users", metadata,
Column("id", Integer, primary_key=True),
Column("name", String),
Column("email", String),
)
# 在數(shù)據(jù)庫(kù)中創(chuàng)建表
metadata.create_all(engine)
# 獲取同步數(shù)據(jù)庫(kù)會(huì)話的依賴
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()2.2 異步版本代碼 ??
在異步版本中,在這里使用asyncpg驅(qū)動(dòng)與SQLAlchemy,進(jìn)行非阻塞查詢。然而,表的創(chuàng)建仍然是同步進(jìn)行的,因?yàn)镾QLAlchemy的metadata.create_all()不支持異步。
異步:main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from .database import get_async_db, User, initialize_database
@asynccontextmanager
async def lifespan(app: FastAPI):
# 啟動(dòng):初始化數(shù)據(jù)庫(kù)
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()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return {"id": user.id, "name": user.name, "email": 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,
)
# 用于異步查詢的異步會(huì)話
AsyncSessionLocal = sessionmaker(
bind=engine, class_=AsyncSession, expire_on_commit=False
)
# 定義元數(shù)據(jù)
metadata = MetaData()
# 定義User表
users = Table(
"users",
metadata,
Column("id", Integer, primary_key=True),
Column("name", String),
Column("email", String),
)
# 創(chuàng)建所有表
async def init_db():
async with engine.begin() as conn:
await conn.run_sync(metadata.create_all)
# 獲取異步數(shù)據(jù)庫(kù)會(huì)話的依賴
async def get_async_db():
async with AsyncSessionLocal() as session:
yield session
# 啟動(dòng):初始化數(shù)據(jù)庫(kù)
async def initialize_database():
await init_db()在這個(gè)版本中,請(qǐng)求是以異步方式處理的,支持多個(gè)請(qǐng)求在等待I/O時(shí)并發(fā)處理。
三、基準(zhǔn)測(cè)試命令
- 同步版本:ab -n 1000 -c 100 http://127.0.0.1:8000/users/1
- 異步版本:ab -n 1000 -c 100 http://127.0.0.1:8001/users/1
基準(zhǔn)測(cè)試結(jié)果:同步vs異步
以下是基準(zhǔn)測(cè)試的性能指標(biāo)分析。
基準(zhǔn)測(cè)試結(jié)果:同步vs異步(Airtable)
性能明細(xì)
- 每秒請(qǐng)求數(shù):
異步版本每秒可處理50.68個(gè)請(qǐng)求,而同步版本每秒只能處理36.89個(gè)請(qǐng)求。
在相同時(shí)間內(nèi),異步處理的請(qǐng)求數(shù)比同步版本多37%,因此在并發(fā)性方面,異步顯然勝出。
- 每個(gè)請(qǐng)求的響應(yīng)時(shí)間(平均值):
異步版本的平均響應(yīng)時(shí)間低于同步版本27%(1973毫秒vs2710毫秒),這表明在高負(fù)載情況下,異步處理請(qǐng)求的效率更高。
最長(zhǎng)請(qǐng)求時(shí)間:
兩個(gè)版本的最長(zhǎng)請(qǐng)求時(shí)間相似(約4000毫秒),但異步版本的表現(xiàn)更穩(wěn)定,這體現(xiàn)在響應(yīng)時(shí)間的波動(dòng)較小。
圖表比較
圖表比較
以上是同步和異步版本在不同百分位數(shù)下的比較圖,包括平均和最長(zhǎng)請(qǐng)求時(shí)間。
- 實(shí)線表示不同百分位數(shù)下的響應(yīng)時(shí)間。
- 虛線表示同步(2710.648毫秒)和異步(1973.057毫秒)的平均響應(yīng)時(shí)間。
- 點(diǎn)線突出顯示同步(4167毫秒)和異步(3851毫秒)的最長(zhǎng)請(qǐng)求時(shí)間。
四、何時(shí)在FastAPI中使用同步與異步?
使用異步的情況:
- 應(yīng)用程序需要處理高流量和大量并發(fā)用戶。
- 應(yīng)用程序是與I/O綁定的,需要進(jìn)行大量數(shù)據(jù)庫(kù)查詢或API調(diào)用。
- 需要為大量請(qǐng)求最小化響應(yīng)時(shí)間。
使用同步的情況:
- 應(yīng)用程序的并發(fā)量較小,或主要執(zhí)行CPU密集型任務(wù)。
- 希望保持代碼庫(kù)的簡(jiǎn)單性,避免異步處理的復(fù)雜性。
- 不希望應(yīng)用程序擴(kuò)展到同時(shí)處理數(shù)百或數(shù)千個(gè)請(qǐng)求。
五、優(yōu)化異步性能
雖然異步在這些測(cè)試中速度更快,但仍有一些方法可以進(jìn)一步優(yōu)化。
- 連接池:使用連接池重用數(shù)據(jù)庫(kù)連接,避免為每個(gè)請(qǐng)求創(chuàng)建一個(gè)新連接。
- 使用異步庫(kù):確保所有I/O綁定的任務(wù)(例如文件讀/寫、外部API調(diào)用)都以異步方式處理,以獲得最佳性能。
- 測(cè)試更高的并發(fā)性:進(jìn)行更高并發(fā)量的負(fù)載測(cè)試(例如,500+用戶),以充分發(fā)揮異步的優(yōu)勢(shì)。
engine = create_async_engine(
DATABASE_URL,
pool_size=10,
max_overflow=20
)六、結(jié)論
如果你的應(yīng)用程序需要處理大量并發(fā)用戶,并嚴(yán)重依賴于I/O綁定任務(wù),那么異步FastAPI可以提供更好的性能、可擴(kuò)展性和響應(yīng)能力。不過(guò),對(duì)于更簡(jiǎn)單的用例,可以選擇同步實(shí)現(xiàn)。























