问题背景大概是这样的:项目是fastapi,有一个接口有个耗时操作,我想把这个耗时操作扔到后台,所以使用了BackgroundTasks。但是并没有得到预期的结果,接口依然耗时很长,且期间使用其他接口会堵塞。

问题代码大概是这样的:

@router.get('/dialog-info')
def get_dialog_info(request: Request, background_tasks: BackgroundTasks, key: str):

    ret = {
        'dialog_info': {}
    }

    # xxx一段逻辑略过
    
    # 使用BackgroundTasks在后台 添加操作
    background_tasks.add_task(add_history,key)

    return JSONResponse(get_resp_body(ret))


async def add_history(key: str):
    #这里有一段sqlalchemy的数据库操作,我这简单写了下示意
       with db_session_scope('xxx', 'w') as session:
        session.execute(xxxxx).fetchall() 

问题的原因是:session.execute(xxxxx).fetchall() 这是一个同步查询,阻塞了当前的请求,即使我是在后台任务中执行它。它也会等待数据库操作完成,导致请求无法立即响应。

再往深处看看原因:

先提一个问题:fastapi在同时接收两个请求时,会在两个线程中执行任务吗?

  • 在 FastAPI 中,是否使用多个线程来处理多个请求,取决于你如何配置 FastAPI 及其环境运行,但通常情况下,是不会开两个线程的。
  • FastAPI 是基于 asyncio 的,默认情况下是 单线程异步。这意味着当 FastAPI 接收到多个请求时,它通常 不会为每个请求启动独立的线程。相反,FastAPI 会使用一个 事件循环(event loop),并且通过 asyncawait 关键字来非阻塞地处理多个请求。
  • 在这种情况下,当 FastAPI 接收到多个请求时,它会依次调度每个请求的处理函数,但如果一个请求的处理需要等待 I/O 操作(例如数据库查询、外部 API 请求等),它会在等待期间释放控制权,允许事件循环继续处理其他请求。这使得 FastAPI 在处理 I/O 密集型任务时非常高效。
  • 但是,由于事件循环是异步的,只要请求本身是非阻塞的(例如,使用异步 I/O 操作),FastAPI 就能够同时处理多个请求。

在了解了fastapi的机制后,回过头看这个问题:

  • SQLAlchemy 本身并不直接支持异步操作,它的主要设计是同步的(SQLAlchemy 2.x之后可以,但要安装需要的依赖包)
  • add_history这个函数里,即使不是MySQL的耗时操作,是es的耗时操作,或者干脆直接用time.sleep,只要是同步阻塞的,那么就会有这个问题。

GPT给的几个解决方法:

  1. 启动多个进程:gunicorn -w 4,简单粗暴的解决
  2. 将阻塞的数据库查询移到后台线程,避免阻塞主线程,示例如下
import asyncio

async def add_history(user: User, ad_key: str):
    # 使用 asyncio.to_thread 执行阻塞的 SQL 查询
    await asyncio.to_thread(run_slow_query)

def run_slow_query():
    with db_session_scope('xxx', 'w') as session:
        session.execute(xxx).fetchall()

  1. 使用支持异步的数据库,如 databases 等(我也没具体实践,后面再试试)
Logo

加入社区!打开量化的大门,首批课程上线啦!

更多推荐