【问题标题】:FastAPI runs api-calls in serial instead of parallel fashionFastAPI 以串行方式而不是并行方式运行 api 调用
【发布时间】:2022-06-18 13:47:55
【问题描述】:

我有以下代码:

import time
from fastapi import FastAPI, Request
    
app = FastAPI()
    
@app.get("/ping")
async def ping(request: Request):
        print("Hello")
        time.sleep(5)
        print("bye")
        return {"ping": "pong!"}

如果我在本地主机上运行我的代码 - 例如,http://localhost:8501/ping - 在同一浏览器窗口的不同选项卡中,我会得到:

Hello
bye
Hello
bye

代替:

Hello
Hello
bye
bye

我已经阅读了有关使用 httpx 的信息,但我仍然无法实现真正​​的并行化。有什么问题?

【问题讨论】:

    标签: python asynchronous python-asyncio fastapi concurrent-processing


    【解决方案1】:

    根据FastAPI's documentation

    当你用普通的def声明一个路径操作函数时 async def,它在外部线程池中运行,然后 等待,而不是直接调用(因为它会阻塞 服务器)。

    因此,def(同步)路由在与线程池不同的线程中运行,或者换句话说,服务器同时处理请求,而async def 路由在主线程上运行(单)线程,即服务器按顺序处理请求 - 只要在此类路由中没有await 调用I/O-bound 操作,例如等待 来自客户端的要通过网络发送的数据、要读取的磁盘中文件的内容、要完成的数据库操作等 - 看看here。带有async and await is many times summarised as using coroutines 的异步代码。 协程是协作的(或cooperatively multitasked):“在任何给定时间,具有协程的程序只运行它的一个协程,并且这个正在运行的协程只有在它明确请求暂停时才会暂停其执行"(有关协程的更多信息,请参阅 herehere)。但是,这不适用于CPU-bound 操作,例如here 描述的操作(例如,音频或图像处理、机器学习)。 CPU-bound 操作,即使在 async def 函数中声明并使用 await 调用,也会阻塞主线程。这也意味着 async def 路由中的阻塞操作(例如 time.sleep())将阻塞整个服务器(如您的情况)。

    因此,如果您的函数不会进行任何async 调用,您可以改为使用def 声明它,如下所示:

    @app.get("/ping")
    def ping(request: Request):
        #print(request.client)
        print("Hello")
        time.sleep(5)
        print("bye")
        return "pong"
    

    否则,如果你要调用async 函数,你必须要await,你应该使用async def。为了证明这一点,下面使用来自asyncio 库的asyncio.sleep() 函数。 herehere 也给出了类似的例子。

    import asyncio
     
    @app.get("/ping")
    async def ping(request: Request):
        print("Hello")
        await asyncio.sleep(5)
        print("bye")
        return "pong"
    

    如果两个请求大约同时到达,上述两个函数都将打印预期的输出 - 如您的问题中所述。

    Hello
    Hello
    bye
    bye
    

    注意:当您第二次(第三次等)调用端点时,请记住从与浏览器主会话隔离的选项卡中执行此操作;否则,请求将显示为来自同一个客户端(您可以检查使用 print(request.client) - 如果两个选项卡在同一窗口中打开,port 数字将显示相同),因此,请求将按顺序处理。您可以重新加载相同的选项卡(正在运行),或者在隐身窗口中打开一个新选项卡,或者使用其他浏览器/客户端发送请求。

    异步/等待和昂贵的 CPU 密集型操作(长计算任务)

    如果您需要使用async def(因为您可能需要await 用于路由内的协程),但也有一些可能阻塞服务器并且不允许其他请求的同步长计算任务通过,例如:

    @app.post("/ping")
    async def ping(file: UploadFile = File(...)):
        print("Hello")
        try:
            contents = await file.read()
            res = some_long_computation_task(contents)  # this blocks other requests
        finally:
            await file.close()
        print("bye")
        return "pong"
    

    然后:

    1. 使用更多workers(例如uvicorn main:app --workers 4)。 注意:每个工人"has its own things, variables and memory"。这意味着global 变量/对象等不会在进程/工作者之间共享。在这种情况下,您应该考虑使用数据库存储或键值存储(缓存),如herehere 所述。此外,“如果您在代码中消耗大量内存,每个进程将消耗等量的内存”

    2. 使用来自concurrency 模块(源代码herehere)的FastAPI(Starlette's)run_in_threadpool() - 正如@tiangolo 建议的here - “将在单独的线程中运行函数以确保主线程(运行协程的地方)不会被阻塞”(参见here)。正如@tiangolo here 所描述的,“run_in_threadpool 是一个可等待函数,第一个参数是一个普通函数,下一个参数直接传递给该函数。它支持序列参数和关键字参数”。

      from fastapi.concurrency import run_in_threadpool
      res = await run_in_threadpool(some_long_computation_task, contents)
      
    3. 或者,使用asynciorun_in_executor

      loop = asyncio.get_running_loop()
      res = await loop.run_in_executor(None, lambda: some_long_computation_task(contents))
      
    4. 您还应该检查是否可以将路由定义更改为def。例如,如果您的端点中唯一需要等待的方法是读取文件内容的方法(正如您在下面的 cmets 部分中提到的),FastAPI 可以为您读取文件的bytes(但是,这应该适用于小文件,因为整个内容将存储在内存中,请参阅here),或者您甚至可以直接调用SpooledTemporaryFile对象的read()方法,这样您就不必等待@ 987654388@ 方法 - 由于您现在可以使用 def 声明您的路由,因此每个请求都将在单独的线程中运行。

      @app.post("/ping")
      def ping(file: UploadFile = File(...)):
          print("Hello")
          try:
              contents = file.file.read()
              res = some_long_computation_task(contents)
          finally:
              file.file.close()
          print("bye")
          return "pong"
      
    5. 查看this answer,以及文档here,了解更多建议的解决方案。

    【讨论】:

    • 事实上,这是一个试验,以检查为什么另一个呼叫正在串行运行。另一个函数调用“UploadFile”并执行“await file.read()”并串行运行。此外,这是在亚马逊服务器产品内部运行的,在来自亚马逊的 api 网关之后,因此所有请求都来自同一个 IP,因为用户连接到亚马逊,亚马逊服务器调用我的 api。问题是对文件的操作很长,如果我在最后序列化了这个,我会因为亚马逊的限制而超时。我想我将不得不去你提供的最后一个链接!
    • 加载文件(图像)后,我对图像进行了一些硬处理,并将图像上传到 AWS 服务器(有 S3 处理程序)。但是,代码中没有任何其他显式等待。
    • 加载我拥有的图像:def myfunc(image: bytes = File(...)): Image.open(BytesIO(image)).convert('RGB'),但是现在失败。之前是: async def myfunc(image: UploadFile = File(...)): Image.open(BytesIO(await image.read())).convert('RGB') 没有 async 和 wait 怎么办?
    【解决方案2】:

    问:
    " ...有什么问题?"

    答:
    FastAPI 文档明确指出框架使用进程内任务(继承自 Starlette)。

    就其本身而言,这意味着所有此类任务都在竞争(不时地)接收 Python 解释器 GIL 锁 - 有效地成为一个 MUTEX-terrorising 全局解释器锁,实际上是 re-[SERIAL]-ises任何和所有数量的 Python Interpreter 进程内线程
    one-and-only-one-WORKS-while-all-others-stay-waiting 的形式工作...

    在细粒度范围内,您会看到结果——如果为第二个(从第二个 FireFox 选项卡手动启动)产生另一个处理程序到达的 http-request 实际上比睡眠花费的时间长,GIL 的结果- lock interleaved ~ 100 [ms] time-quanta round-robin (all-wait-one-can-work ~ 100 [ms] 在每一轮 GIL-lock release-acquire-roulette 发生之前) Python Interpreter 内部工作没有显示更多细节,您可以使用来自here 的更多详细信息(取决于 O/S 类型或版本)来查看更多 in-thread LoD,就像正在执行的异步修饰代码中这样:

    import time
    import threading
    from   fastapi import FastAPI, Request
    
    TEMPLATE = "INF[{0:_>20d}]: t_id( {1: >20d} ):: {2:}"
    
    print( TEMPLATE.format( time.perf_counter_ns(),
                            threading.get_ident(),
                           "Python Interpreter __main__ was started ..."
                            )
    ...
    @app.get("/ping")
    async def ping( request: Request ):
            """                                __doc__
            [DOC-ME]
            ping( Request ):  a mock-up AS-IS function to yield
                              a CLI/GUI self-evidence of the order-of-execution
            RETURNS:          a JSON-alike decorated dict
    
            [TEST-ME]         ...
            """
            print( TEMPLATE.format( time.perf_counter_ns(),
                                    threading.get_ident(),
                                   "Hello..."
                                    )
            #------------------------------------------------- actual blocking work
            time.sleep( 5 )
            #------------------------------------------------- actual blocking work
            print( TEMPLATE.format( time.perf_counter_ns(),
                                    threading.get_ident(),
                                   "...bye"
                                    )
            return { "ping": "pong!" }
    

    最后但并非最不重要的一点是,不要犹豫,阅读更多关于所有other sharks 基于线程的代码可能会遭受...甚至导致...幕后...

    广告备忘录

    混合了 GIL 锁、基于线程的池、异步装饰器、阻塞和事件处理——肯定会混合不确定性和 HWY2HELL ;o)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-11-15
      • 1970-01-01
      • 2020-04-01
      • 1970-01-01
      • 2021-02-18
      • 1970-01-01
      • 1970-01-01
      • 2015-07-14
      相关资源
      最近更新 更多