Waitress和GUnicorn大数据输入比Flask开发服务器慢很多
Waitress and GUnicorn large data input is much slower than Flask development server
问题描述
我正在尝试创建一个 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)
[已编辑:这个问题的原始版本在上面对 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 都在传输更多数据的情况下在速度上受到了显着影响。
问题
此时,我有几个问题:
- Waitress & GUnicorn 运行 对提交的数据进行某种检查需要时间吗?如果是这样,有没有办法禁用它们?
- Waitress / GUnicorn 比 Flask 开发服务器更好的重要原因是什么,或者我可以将它用于我的用例吗?如前所述:
- 我不关心安全问题;这些只能从本地主机看到,我在我的另一个进程中生成进入它们的数据
- 我积极想要同时使用一个 process/thread 运行ning,这是 Flask 开发服务器的唯一可能性,我为其他人强制执行。这是因为我的真实应用程序将 运行 在 GPU 上运行,如果我有很多进程/线程,我会很快耗尽内存
- 我知道在任何时间点都只有少量连接到此服务器(可能是 4 个,肯定不会超过 8 个),因此扩展也不是问题。
- ...但是这会在生产中,所以我需要一些可靠和稳定的东西。
这很有趣。也许这会解释这个问题。
通过使用 time.time() 我发现网络应用程序中的 request.data
花费了不同的时间。使用 gunicorn 时,这花费了超过 95% 的时间,即 0.35 秒。使用 Flask Web 应用程序时,这大约需要 0.001 秒。
我走进了它的包裹。我发现大部分时间花在 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秒。
我步入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 秒。
我试着把上面的代码改成self.buf.write(self.reader.read(size))
。这使得它花费了 0.07 秒。
我把上面的代码拆分成了
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()
这样快多了。
问题描述
我正在尝试创建一个 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)
[已编辑:这个问题的原始版本在上面对 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 都在传输更多数据的情况下在速度上受到了显着影响。
问题
此时,我有几个问题:
- Waitress & GUnicorn 运行 对提交的数据进行某种检查需要时间吗?如果是这样,有没有办法禁用它们?
- Waitress / GUnicorn 比 Flask 开发服务器更好的重要原因是什么,或者我可以将它用于我的用例吗?如前所述:
- 我不关心安全问题;这些只能从本地主机看到,我在我的另一个进程中生成进入它们的数据
- 我积极想要同时使用一个 process/thread 运行ning,这是 Flask 开发服务器的唯一可能性,我为其他人强制执行。这是因为我的真实应用程序将 运行 在 GPU 上运行,如果我有很多进程/线程,我会很快耗尽内存
- 我知道在任何时间点都只有少量连接到此服务器(可能是 4 个,肯定不会超过 8 个),因此扩展也不是问题。
- ...但是这会在生产中,所以我需要一些可靠和稳定的东西。
这很有趣。也许这会解释这个问题。
通过使用 time.time() 我发现网络应用程序中的
request.data
花费了不同的时间。使用 gunicorn 时,这花费了超过 95% 的时间,即 0.35 秒。使用 Flask Web 应用程序时,这大约需要 0.001 秒。我走进了它的包裹。我发现大部分时间花在
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秒。我步入
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 秒。
我试着把上面的代码改成
self.buf.write(self.reader.read(size))
。这使得它花费了 0.07 秒。我把上面的代码拆分成了
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()
这样快多了。