架构 Flask 与 FastAPI

Architecture Flask vs FastAPI

我一直在研究 Flask 和 FastAPI 以了解它如何充当服务器。
我想知道的主要事情之一是 Flask 和 FastAPI 如何处理来自多个客户端的多个请求。
特别是当代码存在效率问题时(数据库查询时间长)。

所以,我试着编写了一个简单的代码来理解这个问题。
代码很简单,客户端访问路由时,应用休眠10秒才returns出结果。
它看起来像这样:

FastAPI

import uvicorn
from fastapi import FastAPI
from time import sleep
app = FastAPI()

@app.get('/')
async def root():
    print('Sleeping for 10')
    sleep(10)
    print('Awake')
    return {'message': 'hello'}

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000)

烧瓶

from flask import Flask
from flask_restful import Resource, Api
from time import sleep

app = Flask(__name__)
api = Api(app)

class Root(Resource):
    def get(self):
        print('Sleeping for 10')
        sleep(10)
        print('Awake')
        return {'message': 'hello'}

api.add_resource(Root, '/')

if __name__ == "__main__":
    app.run()

应用程序启动后,我尝试通过 2 个不同的 chrome 客户端同时访问它们。 以下是结果:

FastAPI

烧瓶

如您所见,对于 FastAPI,代码首先等待 10 秒,然后再处理下一个请求。而对于 Flask,代码会在 10 秒睡眠仍在发生时处理下一个请求。

尽管进行了一些谷歌搜索,但关于这个主题并没有真正直接的答案。
如果有人对此有任何意见,请在评论中提出。

非常感谢您的意见。非常感谢大家的宝贵时间。

编辑 关于此的更新,我正在探索更多并发现了流程管理器的概念。例如,我们可以使用进程管理器 (gunicorn) 运行 uvicorn。通过添加更多工人,我能够实现类似 Flask 的功能。然而,仍在测试其极限。 https://www.uvicorn.org/deployment/

感谢所有留下评论的人!欣赏一下。

我认为您在异步框架 FastAPI 中阻塞了一个事件队列,而在 Flask 中请求可能 运行 每个都在新线程中。将所有 CPU 绑定任务移动到单独的进程中,或者在您的 FastAPI 示例中仅在事件循环中休眠(不要在此处使用 time.sleep)。在 FastAPI 中 运行 IO 异步绑定任务

这看起来有点有趣,所以我 运行 用 ApacheBench:

做了一些小测试

烧瓶

from flask import Flask
from flask_restful import Resource, Api


app = Flask(__name__)
api = Api(app)


class Root(Resource):
    def get(self):
        return {"message": "hello"}


api.add_resource(Root, "/")

FastAPI

from fastapi import FastAPI


app = FastAPI(debug=False)


@app.get("/")
async def root():
    return {"message": "hello"}

我运行对FastAPI进行了2次测试,差别很大:

  1. gunicorn -w 4 -k uvicorn.workers.UvicornWorker fast_api:app
  2. uvicorn fast_api:app --reload

所以这是 5000 个并发 500 请求的基准测试结果:

FastAPI 与 Uvicorn Workers

Concurrency Level:      500
Time taken for tests:   0.577 seconds
Complete requests:      5000
Failed requests:        0
Total transferred:      720000 bytes
HTML transferred:       95000 bytes
Requests per second:    8665.48 [#/sec] (mean)
Time per request:       57.700 [ms] (mean)
Time per request:       0.115 [ms] (mean, across all concurrent requests)
Transfer rate:          1218.58 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    6   4.5      6      30
Processing:     6   49  21.7     45     126
Waiting:        1   42  19.0     39     124
Total:         12   56  21.8     53     127

Percentage of the requests served within a certain time (ms)
  50%     53
  66%     64
  75%     69
  80%     73
  90%     81
  95%     98
  98%    112
  99%    116
 100%    127 (longest request)

FastAPI - 纯 Uvicorn

Concurrency Level:      500
Time taken for tests:   1.562 seconds
Complete requests:      5000
Failed requests:        0
Total transferred:      720000 bytes
HTML transferred:       95000 bytes
Requests per second:    3200.62 [#/sec] (mean)
Time per request:       156.220 [ms] (mean)
Time per request:       0.312 [ms] (mean, across all concurrent requests)
Transfer rate:          450.09 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    8   4.8      7      24
Processing:    26  144  13.1    143     195
Waiting:        2  132  13.1    130     181
Total:         26  152  12.6    150     203

Percentage of the requests served within a certain time (ms)
  50%    150
  66%    155
  75%    158
  80%    160
  90%    166
  95%    171
  98%    195
  99%    199
 100%    203 (longest request)

对于 Flask:

Concurrency Level:      500
Time taken for tests:   27.827 seconds
Complete requests:      5000
Failed requests:        0
Total transferred:      830000 bytes
HTML transferred:       105000 bytes
Requests per second:    179.68 [#/sec] (mean)
Time per request:       2782.653 [ms] (mean)
Time per request:       5.565 [ms] (mean, across all concurrent requests)
Transfer rate:          29.13 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0   87 293.2      0    3047
Processing:    14 1140 4131.5    136   26794
Waiting:        1 1140 4131.5    135   26794
Total:         14 1227 4359.9    136   27819

Percentage of the requests served within a certain time (ms)
  50%    136
  66%    148
  75%    179
  80%    198
  90%    295
  95%   7839
  98%  14518
  99%  27765
 100%  27819 (longest request)

总结果

Flask测试时间:27.827 秒

FastAPI - Uvicorn测试时间:1.562 秒

FastAPI - Uvicorn Workers测试时间:0.577 秒


使用 Uvicorn Workers FastAPI 比 Flask 快近 48x,这是非常可以理解的。 ASGI vs WSGI,所以我 运行 有 1 个并发:

FastAPI - UvicornWorkers测试时间:1.615 秒

FastAPI - 纯 Uvicorn测试时间:2.681 秒

Flask测试时间:5.541 秒

我 运行 进行更多测试以使用生产服务器测试 Flask。

5000请求1000并发

带女服务员的烧瓶

Server Software:        waitress
Server Hostname:        127.0.0.1
Server Port:            8000

Document Path:          /
Document Length:        21 bytes

Concurrency Level:      1000
Time taken for tests:   3.403 seconds
Complete requests:      5000
Failed requests:        0
Total transferred:      830000 bytes
HTML transferred:       105000 bytes
Requests per second:    1469.47 [#/sec] (mean)
Time per request:       680.516 [ms] (mean)
Time per request:       0.681 [ms] (mean, across all concurrent requests)
Transfer rate:          238.22 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    4   8.6      0      30
Processing:    31  607 156.3    659     754
Waiting:        1  607 156.3    658     753
Total:         31  611 148.4    660     754

Percentage of the requests served within a certain time (ms)
  50%    660
  66%    678
  75%    685
  80%    691
  90%    702
  95%    728
  98%    743
  99%    750
 100%    754 (longest request)

Gunicorn 与 Uvicorn 工人

Server Software:        uvicorn
Server Hostname:        127.0.0.1
Server Port:            8000

Document Path:          /
Document Length:        19 bytes

Concurrency Level:      1000
Time taken for tests:   0.634 seconds
Complete requests:      5000
Failed requests:        0
Total transferred:      720000 bytes
HTML transferred:       95000 bytes
Requests per second:    7891.28 [#/sec] (mean)
Time per request:       126.722 [ms] (mean)
Time per request:       0.127 [ms] (mean, across all concurrent requests)
Transfer rate:          1109.71 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0   28  13.8     30      62
Processing:    18   89  35.6     86     203
Waiting:        1   75  33.3     70     171
Total:         20  118  34.4    116     243

Percentage of the requests served within a certain time (ms)
  50%    116
  66%    126
  75%    133
  80%    137
  90%    161
  95%    189
  98%    217
  99%    230
 100%    243 (longest request)

纯独角兽,不过这次是4个工人uvicorn fastapi:app --workers 4

Server Software:        uvicorn
Server Hostname:        127.0.0.1
Server Port:            8000

Document Path:          /
Document Length:        19 bytes

Concurrency Level:      1000
Time taken for tests:   1.147 seconds
Complete requests:      5000
Failed requests:        0
Total transferred:      720000 bytes
HTML transferred:       95000 bytes
Requests per second:    4359.68 [#/sec] (mean)
Time per request:       229.375 [ms] (mean)
Time per request:       0.229 [ms] (mean, across all concurrent requests)
Transfer rate:          613.08 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0   20  16.3     17      70
Processing:    17  190  96.8    171     501
Waiting:        3  173  93.0    151     448
Total:         51  210  96.4    184     533

Percentage of the requests served within a certain time (ms)
  50%    184
  66%    209
  75%    241
  80%    260
  90%    324
  95%    476
  98%    504
  99%    514
 100%    533 (longest request)

您正在 async 端点中使用 time.sleep() 函数。 time.sleep() 是阻塞的,不应该在异步代码中使用。您应该使用的可能是 asyncio.sleep() 函数:

import asyncio
import uvicorn
from fastapi import FastAPI
app = FastAPI()

@app.get('/')
async def root():
    print('Sleeping for 10')
    await asyncio.sleep(10)
    print('Awake')
    return {'message': 'hello'}

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000)

这样,每个请求大约需要 10 秒才能完成,但您将能够同时处理多个请求。

一般来说,异步框架为标准库中的所有阻塞函数(睡眠函数、IO 函数等)提供替代品。您应该在编写异步代码和(可选)await 时使用这些替换。

某些非阻塞框架和库(例如 gevent)不提供替代品。他们取而代之的是标准库中的猴子补丁函数,使它们成为非阻塞的。据我所知,对于较新的异步框架和库来说情况并非如此,因为它们旨在允许开发人员使用异步等待语法。

为什么代码很慢

阻塞操作将停止您的事件循环运行任务。当您调用 sleep() 函数时,所有任务(请求)都在等待它完成,从而扼杀了异步代码执行的所有好处。

要理解为什么这段代码比较错误,我们应该更好地理解异步代码在Python中是如何工作的,并且对GIL有一些了解。并发和异步代码在 FastAPI 的 docs 中有很好的解释。

@Asotos 已经描述了为什么你的代码很慢,是的,你应该使用协程进行 I/O 操作,因为它们会阻止事件循环执行(sleep() 是一个阻塞操作)。合理地建议使用异步函数,这样事件循环就不会被阻塞,但目前,并不是所有的库都有异步版本。

没有 async 函数和 asyncio.sleep

的优化

如果您不能使用库的异步版本,您可以简单地将路由函数定义为简单的 def 函数,而不是 async def

如果路由函数定义为同步(def),FastAPI会在外部线程池中智能调用该函数,不会阻塞带事件循环的主线程,你的benchmarks会是不使用 await asyncio.sleep() 会好很多。在 this 部分有很好的解释。

解决方案

from time import sleep

import uvicorn
from fastapi import FastAPI


app = FastAPI()

@app.get('/')
def root():
    print('Sleeping for 10')
    sleep(10)
    print('Awake')
    return {'message': 'hello'}

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000)

顺便说一句,如果线程池中的操作 运行 由于 GIL 而受到 CPU 限制(例如繁重的计算),您将不会获得很多好处。 CPU 绑定任务必须 运行 在单独的进程中。