<p>werkzeug.formparser对于大型二进制文件上载确实很慢</p>

创建于 2016-03-03  ·  25评论  ·  资料来源: pallets/werkzeug

当我在Flask中执行任何大型二进制文件的multipart/form-data上传时,这些上传非常容易受CPU限制(Python占用100%CPU),而不是受任何合理的快速网络连接限制。

一点CPU分析显示,在这些上载期间,几乎所有CPU时间都花费在werkzeug.formparser.MultiPartParser.parse_parts() 。 方法parse_lines()产生很多小块的原因,有时甚至是单个字节

# we have something in the buffer from the last iteration.
# this is usually a newline delimiter.
if buf:
    yield _cont, buf
    buf = b''

因此, parse_parts()经历了许多小的迭代(对于100 MB的文件,超过200万次),处理单个“行”,始终仅将非常短的块甚至单个字节写入输出流。 这增加了很多开销,减慢了整个过程的速度,并使CPU的绑定速度非常快。

快速测试表明,通过首先在parse_lines()中的bytearray中收集数据,然后仅在self.buffer_size时才将数据重新生成为parse_parts() ,才可以轻松实现加速。超出了self.buffer_size 。 像这样:

buf = b''
collect = bytearray()
for line in iterator:
    if not line:
        self.fail('unexpected end of stream')

    if line[:2] == b'--':
        terminator = line.rstrip()
        if terminator in (next_part, last_part):
            # yield remaining collected data
            if collect:
                yield _cont, collect
            break

    if transfer_encoding is not None:
        if transfer_encoding == 'base64':
            transfer_encoding = 'base64_codec'
        try:
            line = codecs.decode(line, transfer_encoding)
        except Exception:
            self.fail('could not decode transfer encoded chunk')

    # we have something in the buffer from the last iteration.
    # this is usually a newline delimiter.
    if buf:
        collect += buf
        buf = b''

    # If the line ends with windows CRLF we write everything except
    # the last two bytes.  In all other cases however we write
    # everything except the last byte.  If it was a newline, that's
    # fine, otherwise it does not matter because we will write it
    # the next iteration.  this ensures we do not write the
    # final newline into the stream.  That way we do not have to
    # truncate the stream.  However we do have to make sure that
    # if something else than a newline is in there we write it
    # out.
    if line[-2:] == b'\r\n':
        buf = b'\r\n'
        cutoff = -2
    else:
        buf = line[-1:]
        cutoff = -1

    collect += line[:cutoff]

    if len(collect) >= self.buffer_size:
        yield _cont, collect
        collect.clear()

仅此一项更改就比我机器上的本地主机将我的34 MB测试文件的上传时间从4200毫秒减少到大约1100毫秒,这几乎使性能提高了4倍。 所有测试都是在Windows(64位Python 3.4)上完成的,我不确定在Linux上是否同样有问题。

它仍然主要受CPU限制,因此我确信还有更多的优化潜力。 我想我会在发现更多时间时进行调查。

最有用的评论

我想提一下在接收到流时对流进行大块解析。 @siddhantgoel为我们编写了这个很棒的小解析器。 这对我来说很棒。 https://github.com/siddhantgoel/streaming-form-data

所有25条评论

我也有同样的问题,当我上传一个iso文件(200m)时,对request.form的第一次调用将花费7s

有2件事对于进一步优化似乎很有趣-试用cython,并尝试解释内容站点标头以进行更智能的mime消息解析

(如果您知道子消息的内容长度,则无需扫描行)

简要说明一下,如果直接在请求正文中流式传输文件(即,没有application/multipart-formdata ),则将完全绕过表单解析器并直接从request.stream读取文件。

使用jQuery-File-Upload的分块上载方法时,分段上传的上载速度较慢时,我也遇到同样的问题。 当使用小块数据块(〜10MB)时,传输速度在0到12MB / s之间跳跃,而网络和服务器完全可以超过50MB / s。 速度减慢是由CPU绑定的多部分解析引起的,该解析与实际上载大约需要相同的时间。 遗憾的是,使用流上传来绕过多部分解析并不是一个真正的选择,因为我必须支持无法在后台进行流传输的iOS设备。

@sekrause提供的

@carbn :通过将最后一行更改为collect = bytearray()我能够使补丁在Python 2.7中工作。 这只是创建一个新的字节数组,而不是清除现有的字节数组。

@cuibonobo :这是我更改的第一件事,但仍然有另一个错误。 我目前无法检查工作补丁,但是IIRC的收益必须从yield _cont, collect更改yield _cont, str(collect) 。 这样就可以测试代码,并且该补丁使多部分处理速度提高了约30%。 这是一个不错的加速,但是性能仍然很差。

进一步的调查显示, werkzeug.wsgi.make_line_iter已经是一个瓶颈,无法真正优化parse_lines() 。 查看以下Python 3测试脚本:

import io
import time
from werkzeug.wsgi import make_line_iter

filename = 'test.bin' # Large binary file
lines = 0

# load a large binary file into memory
with open(filename, 'rb') as f:
    data = f.read()
    stream = io.BytesIO(data)
    filesize = len(data) / 2**20 # MB

start = time.perf_counter()
for _ in make_line_iter(stream):
    lines += 1
stop = time.perf_counter()
delta = stop - start

print('File size: %.2f MB' % filesize)
print('Time: %.1f seconds' % delta)
print('Read speed: %.2f MB/s' % (filesize / delta))
print('Number of lines yielded by make_line_iter: %d' % lines)

对于带有Python 3.5的923 MB视频文件,输出在我的笔记本电脑上如下所示:

File size: 926.89 MB
Time: 20.6 seconds
Read speed: 44.97 MB/s
Number of lines yielded by make_line_iter: 7562905

因此,即使您在上面应用了我的优化并对其进行了进一步优化,直到完美为止,对于大型二进制文件上传,您仍然会被限制为〜45 MB / s,这仅仅是因为make_line_iter无法给您足够快的数据,将为检查边界的923 MB数据执行750万次迭代。

我猜唯一的好优化是将parse_lines()完全替换

我想提一下在接收到流时对流进行大块解析。 @siddhantgoel为我们编写了这个很棒的小解析器。 这对我来说很棒。 https://github.com/siddhantgoel/streaming-form-data

我猜唯一的好选择是完全替换parse_lines()

为此+1。

我正在编写一个桥接器,将用户的上传直接流式传输到S3,而没有任何中间临时文件,可能会有背压,并且我发现werkzeugflask情况令人沮丧。 您不能在两个管道之间直接移动数据。

@lambdaq我同意这是一个必须解决的问题。 如果这对您很重要,我很乐意查看更改行为的补丁。

@lambdaq请注意,如果您只是直接在请求正文中流式传输数据并使用application/octet-stream则表单解析器根本不会启动,您可以使用request.stream (即,没有临时文件等) 。

我们遇到的唯一问题是werkzeug表单解析器正在急切地根据允许的最大内容长度检查内容长度,然后才知道它是否应实际解析请求正文

这样可以防止您在常规表单数据上设置最大内容长度,但也允许上传非常大的文件。

我们通过对检查功能

请注意,如果您只是直接在请求正文中流式传输数据并使用application / octet-stream,则表单解析器根本不会启动,您可以使用request.stream(即没有临时文件等)。

不幸的是没有。 这只是具有多部分内容的正常形式上载。

我很乐意查看更改行为的补丁。

我尝试使用生成器的send()破解werkzeug.wsgi.make_line_iterparse_lines() send() ,所以我们可以发出信号_iter_basic_lines()发出整个块而不是行。 事实并非如此简单。

基本上,兔子整体以'itertools.chain' object has no attribute 'send' ....😂开始

我不知道使用C(或Cython等)编写的本机加速程序可以提高多少代码。 我认为更有效地处理半大文件(几百MB,但不如许多GB中的大文件)非常重要,而不必更改应用程序使用它们的方式(即直接流式传输而不是缓冲)-对于许多应用程序来说过度杀伤力并不是绝对必要的(实际上,即使对于目前有些慢的性能来说,对他们来说也可以),但是让事情变得更快总是一件好事!

另一个可能的解决方案是将multipart解析作业卸载到nginx

https://www.nginx.com/resources/wiki/modules/upload/

两个仓库都死了。

所以没有已知的解决方案吗?

有一个解决方法👆

在uwsgi下,我们使用它内置的chunked_read()函数并在传入时自行分析流。它在99%的时间内都有效,但是它有一个我尚未跟踪的错误。 请参阅我的较早评论,以获取开箱即用的流式表单解析器。 在python2下,它很慢,所以我们自己滚动了它,而且很快。 :)

从上面引用:

我同意这是一个必须解决的问题。 如果这对您很重要,我很乐意查看更改行为的补丁。

我现在真的没有时间从事此工作。 如果这是您要花费的时间,考虑提供补丁。 欢迎捐款。

@sdizazzo

但它有一个我尚未追踪的错误

您在谈论流式数据吗? 如果是这样,我很想知道错误是什么。

我们的问题是,缓慢的表单处理阻止并发请求处理,这导致nomad认为进程已挂起并杀死了该进程。

我的解决方法是在werkzeug/formparser.py:MutlipartParser.parse_lines()添加sleep(0) werkzeug/formparser.py:MutlipartParser.parse_lines()

            for i, line in enumerate(iterator):
                if not line:
                    self.fail('unexpected end of stream')

                # give other greenlets a chance to run every 100 lines
                if i % 100 == 0:
                    time.sleep(0)

如果要应用此修补程序,请搜索unexpected end of stream

我想提一下在接收到流时对流进行大块解析。 @siddhantgoel为我们编写了这个很棒的小解析器。 这对我来说很棒。 https://github.com/siddhantgoel/streaming-form-data

借调。
这将文件上传到我的Flask应用的速度提高了10倍以上

@siddhantgoel
非常感谢您对流式表单数据的修复。 我终于可以在不占用内存的情况下,以良好的速度上传技嘉大小的文件!

请参阅#1788,其中讨论了将解析器重写为sans-io。 根据这里的反馈,我认为这也可以解决此问题。

此页面是否有帮助?
0 / 5 - 0 等级