手记

FastAPI性能大比拼:同步 vs 异步,哪种更好?

FastAPI 的 Logo

由于速度快且易于使用,FastAPI 已经成为构建 Python API 的最流行的框架之一。但在构建高性能应用程序的过程中,你可能会问:你应该使用同步(sync)还是异步(async)代码的执行方式?

本文将通过实际性能测试来比较同步异步的FastAPI实现方式,并深入分析测试结果,帮助您决定何时使用每种方式。

FastAPI中的同步和异步有什么区别?
  • 同步代码(Sync):在同步执行中,任务一次只处理一个。每个请求都必须等待前一个请求完成,当有多名用户或遇到慢 I/O 操作(如数据库查询或文件上传)时,可能会导致瓶颈。
  • 异步代码(Async):异步执行能让多个请求同时进行处理。应用程序不会等待 I/O 操作(如数据库调用)完成,而是继续处理其他请求,在高并发环境下更为高效。

但是这两者在性能上到底有什么真正的差别呢?让我们一起来看看吧。

FastAPI中的同步与异步性能对比

为了比较同步异步实现,我创建了两个FastAPI应用版本。

  1. 同步版本:使用传统的阻塞数据库请求,使用 psycopg2
  2. 异步版本:使用 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)的过程中,可以处理多个请求。

基准指令:
  1. 同步版本:ab -n 1000 -c <http://127.0.0.1:8000/users/1>

  2. 异步版本:ab -n 1000 -c <http://127.0.0.1:8001/users/1>
测试基准结果:同步 vs 异步:谁更胜一筹?

这是基准测试中性能指标的详细分析,

测试结果:同步 vs 异步(同步/异步)(Airtable 数据库)

性能分析
1. 请求次数:
  • 异步每秒可以处理 50.68个请求,而同步每秒仅处理 36.89个请求
  • 对比之下,异步处理的请求数量多了37%,因此在并发处理上更胜一筹。
2. 每次请求的平均响应时间(平均值):
  • 异步版本的平均响应时间比同步版本低了27%(1973毫秒对比2710毫秒),说明异步版本在高负载情况下处理请求更高效。
3. 最长的请求耗时:
  • 两个版本显示出相似的最长请求时间(约4000毫秒),但异步版本的性能稳定一致,这从响应时间的较小波动中可以看出。
图形对比:

图形对比

下面是一张图表,比较了SyncAsync版本在不同百分位上的表现,请求时间和最长请求耗时。

  • 实线表示如下不同分位数的响应时间情况。
  • 虚线表示如下同步(2710.648 ms)和异步(1973.057 ms)的平均响应时间情况。
  • 点线突出显示如下同步(4167 ms)和异步(3851 ms)的最长请求时间情况。
当何时在FastAPI中使用同步和异步?
在以下情况下使用异步:
  • 你的应用需要处理高流量和大量并发用户
  • 该应用程序是I/O密集型,有大量数据库查询和API调用。
  • 你需要尽量减少大量请求的响应时间。
以下情况可使用同步:
  • 该应用的并发性较低,或者主要执行的是CPU密集任务
  • 您希望代码库更简单,避免处理异步操作带来的复杂性。
  • 您的应用不需要扩展来同时处理几百或几千个请求。
异步性能优化

虽然异步在这些测试中表现更快,但仍然可以对其进行更多优化,进一步提升它的性能。

  1. 连接池:使用连接池来复用数据库连接,避免为每个请求创建新的连接。
  2. 使用异步库:确保所有I/O操作都异步处理以实现最大性能。
  3. 进行高并发测试:运行高并发的负载测试(如500+用户)以充分发挥异步处理的优势。
    engine = create_async_engine(  
        DATABASE_URL,  
        pool_size=10,  
        max_overflow=20  
    )
让我们来试试吧!

你在 FastAPI 中试过实现 SyncAsync 吗?你得到的结果怎么样?分享一下你的经历,或者在你的应用程序上运行同样的基准测试。我很想看看异步在不同负载和应用场景下的表现!

所以,最后我们可以说

如果你的应用需要处理大量并发用户,并且主要依赖I/O密集型任务,异步FastAPI 能提供更好的性能、可扩展性和响应速度。然而,对于更简单的情况,

[OOP]: 面向对象编程
[CRUD]: 增删查改
[JVM]: Java虚拟机
[SUT]: 被测系统

0人推荐
随时随地看视频
慕课网APP