Waitress和GUnicorn大数据输入比Flask开发服务器慢很多

Mar*_*aci 11 python wsgi flask gunicorn waitress

问题描述

我正在尝试创建一个 Flask 应用程序,它应该:

  • 仅在本地主机上可见,因此不会降低网络速度
  • 获取相当多的数据(30MB 作为一个大型 numpy 数组)作为输入,并输出相对较小的数据量(大约 1MB)。

我做了一个快速测试并使用 Flask 开发服务器运行它,它按预期工作。由于被红色文字吓到,WARNING: This is a development server. Do not use it in a production deployment.我尝试将其放在 WSGI 服务器后面,但 Waitress 和 GUnicorn 的结果都慢得多。测试(针对具有人工输入、微小输出和完全可复制代码的玩具问题)如下。

运行测试的代码

我把这三个文件放在一个文件夹中:

basic_flask_app.py(这里应该对它获取的数据做很少的事情;我拥有的真正代码是一个在 GPU 上运行得相当快的深度学习模型,但创建这个示例是为了使问题更加极端)

import numpy as np

from flask import Flask, request
from do_request import IS_SMALL_DATA, WIDTH, HEIGHT

app = Flask(__name__)


@app.route('/predict', methods=['POST'])
def predict():
    numpy_bytes = np.frombuffer(request.data, np.float32)
    if IS_SMALL_DATA:
        numpy_image = np.zeros((HEIGHT, WIDTH)) + numpy_bytes
    else:
        numpy_image = numpy_bytes.reshape(HEIGHT, WIDTH)
    result = numpy_image.mean(axis=1).std(axis=0)
    return result.tobytes()


if __name__ == '__main__':
    app.run(host='localhost', port=80, threaded=False, processes=1)
Run Code Online (Sandbox Code Playgroud)

[编辑:这个问题的原始版本缺少threaded=False, processes=1上面调用中的参数app.run,因此行为与下面的 GUnicorn 和 Waitress 不同,它们被迫单线程/进程;我现在添加了它,并重新测试,结果没有改变,Flask 服务器在此更改后仍然很快 - 如果有的话,更快]

do_request.py

import requests
import numpy as np
from tqdm import trange

WIDTH = 2500
HEIGHT = 3000
IS_SMALL_DATA = False


def main(url='http://127.0.0.1:80/predict'):
    n = WIDTH * HEIGHT
    if IS_SMALL_DATA:
        np_image = np.zeros(1, dtype=np.float32)
    else:
        np_image = np.arange(n).astype(np.float32) / np.float32(n)
    results = []
    for _ in trange(50):
        results.append(requests.post(url, data=np_image.tobytes()))


if __name__ == '__main__':
    main()
Run Code Online (Sandbox Code Playgroud)

女服务员服务器.py

from waitress import serve
import basic_flask_app
serve(basic_flask_app.app, host='127.0.0.1', port=80, threads=1)
Run Code Online (Sandbox Code Playgroud)

检测结果

python do_requests.py我使用以下三个命令之一启动模型后运行了测试:

python basic_flask_app.py
python waitress_server.py 
gunicorn -w 1 basic_flask_app:app -b 127.0.0.1:80
Run Code Online (Sandbox Code Playgroud)

通过这三个选项,并切换IS_SMALL_DATA标志(如果为 True,则仅传输 4 个字节的数据;如果为 False,则传输 30MB),我得到以下计时:

50 requests              Flask               Waitress             GUnicorn
30MB input, 4B output:   00:01 (28.6 it/s)   00:11 (4.42 it/s)    00:11 (4.26 it/s)
4B input, 4B output:     00:01 (25.2 it/s)   00:02 (23.6 it/s)    00:01 (26.4 it/s)
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,Flask 开发服务器速度非常快,与传输的数据量无关(“小”数据甚至有点慢,可能是因为它浪费了在 50 次迭代中每次分配内存的时间),而 Waitress 和随着传输数据的增加,GUnicorn 的速度受到显着影响。

问题

此时,我有几个问题:

  • Waitress 和 GUnicorn 是否会对提交的数据进行某种需要时间的检查?如果是这样,有办法禁用它们吗?
  • Waitress / GUnicorn 比 Flask 开发服务器更好有一个重要原因吗?或者我可以将它用于我的用例吗?如上所述:
    • 我不关心安全;这些仅从本地主机可见,并且我生成在我的另一个进程中进入它们的数据
    • 我积极希望一个进程/线程同时运行,这是 Flask 开发服务器的唯一可能性,并且我对其他服务器强制执行。这是因为我的真实应用程序将在 GPU 上运行,如果我有很多进程/线程,我很快就会耗尽内存
    • 我知道在任何时间点,到该服务器的连接数都会很少(可能是 4 个,当然不会超过 8 个),因此扩展也不是问题。
    • ...但这将在生产中,所以我需要一些可靠和稳定的东西。

Xu *_*shi 9

这很有趣。也许这可以解释这个问题。

  1. 通过使用 time.time() 我发现request.data网络应用程序花费了不同的时间。当使用 Gunicorn 时,这会花费超过 95% 的时间,即 0.35 秒。当使用 Flask Web 应用程序时,这花费了大约 0.001 秒。

  2. 我走进它的包裹。我发现大部分时间werkzeug/wrappers/base_request.py 456 line花在

    rv = self.stream.read()

    使用 Flask 开发服务器时。这self.streamwerkzeug.wsgi.LimitedStream。这条线花费了大约0.001s。

    使用枪角兽时。这self.streamgunicorn.http.body.Body。这将花费超过0.3秒。

  3. 我步入gunicorn/http/body.py。第 214-218 行

     while size > self.buf.tell():
         data = self.reader.read(1024)
         if not data:
             break
         self.buf.write(data)
    
    Run Code Online (Sandbox Code Playgroud)

    这花费了超过0.3s。

  4. 我尝试将上面的代码更改为self.buf.write(self.reader.read(size)). 这使得它花费了 0.07 秒。

  5. 我将上面的代码分成

     now = time.time()
     buffer = self.reader.read(size)
     print(time.time() - now)
     now = time.time()
    
    Run Code Online (Sandbox Code Playgroud)

    我发现第一行成本为 0.053。第二行成本 0.017。

我想我已经找到原因了。

首先,gunicorn 使用 io.BytesIO 将原始字节包装到他的特殊对象中。

其次,gunicorn 使用 while 循环读取字节,这会花费更多时间。

我猜这些代码的目的是支持高并发。

对于你的情况,我认为你可以直接使用 gevent 。

from gevent.pywsgi import WSGIServer
from basic_flask_app import app

http_server = WSGIServer(('', 80), app)
http_server.serve_forever()
Run Code Online (Sandbox Code Playgroud)

这要快得多。