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

Waitress and GUnicorn large data input is much slower than Flask development server

问题描述

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

我用 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)

[已编辑:这个问题的原始版本在上面对 app.run 的调用中缺少参数 threaded=False, processes=1,因此行为与下面的 GUnicorn 和 Waitress 不同,它们是被迫单身thread/process;我现在添加了它,并重新测试,结果没有改变,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()

waitress_server.py

from waitress import serve
import basic_flask_app
serve(basic_flask_app.app, host='127.0.0.1', port=80, threads=1)

测试结果

在使用以下三个命令之一启动模型后,我 运行 测试 运行ning 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

使用这三个选项,并切换 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)

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

问题

此时,我有几个问题:

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

  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()

    使用烧瓶开发服务器时。这个self.stream就是werkzeug.wsgi.LimitedStream。这条线耗时约0.001秒。

    使用 gunicorn 时。这个self.stream就是gunicorn.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)
    

    这花费了超过 0.3 秒。

  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()
    

    我发现一线成本为 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()

这样快多了。