<p>werkzeug.formparser sangat lambat dengan unggahan biner yang besar</p>

Dibuat pada 3 Mar 2016  ·  25Komentar  ·  Sumber: pallets/werkzeug

Ketika saya melakukan upload multipart/form-data dari file biner besar manapun di Flask, upload tersebut sangat mudah terikat dengan CPU (dengan Python mengkonsumsi 100% CPU) dan bukan I / O terikat pada koneksi jaringan yang cukup cepat.

Sedikit dari profil CPU mengungkapkan bahwa hampir semua waktu CPU selama pengunggahan ini dihabiskan dalam werkzeug.formparser.MultiPartParser.parse_parts() . Alasan mengapa metode parse_lines() menghasilkan _a lot_ dari potongan yang sangat kecil, terkadang bahkan hanya satu byte :

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

Jadi parse_parts() melewati banyak iterasi kecil (lebih dari 2 juta untuk file 100 MB) memproses satu "baris", selalu menulis hanya potongan yang sangat pendek atau bahkan byte tunggal ke dalam arus keluaran. Ini menambahkan banyak overhead yang memperlambat seluruh proses tersebut dan membuatnya terikat dengan CPU dengan sangat cepat.

Tes cepat menunjukkan bahwa percepatan sangat mudah dilakukan dengan terlebih dahulu mengumpulkan data di bytearray di parse_lines() dan hanya menghasilkan data itu kembali ke parse_parts() saat self.buffer_size terlampaui. Sesuatu seperti ini:

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

Perubahan ini sendiri mengurangi waktu unggah untuk file uji 34 MB saya dari 4200 md menjadi sekitar 1100 md melalui localhost di mesin saya, itu hampir peningkatan kinerja 4X lipat. Semua pengujian dilakukan pada Windows (64-bit Python 3.4), saya tidak yakin apakah ini menjadi masalah di Linux.

Sebagian besar masih terikat CPU, jadi saya yakin ada potensi lebih untuk pengoptimalan. Saya pikir saya akan memeriksanya ketika saya menemukan lebih banyak waktu.

bug

Komentar yang paling membantu

Saya ingin menyebutkan melakukan parsing di aliran dalam potongan saat diterima. @siddhantgoel menulis parser kecil yang hebat ini untuk kami. Ini bekerja dengan baik untuk saya. https://github.com/siddhantgoel/streaming-form-data

Semua 25 komentar

Saya juga memiliki masalah yang sama, ketika saya mengunggah file iso (200m), panggilan pertama ke request.form akan memakan waktu 7 detik

2 hal tampaknya menarik untuk pengoptimalan lebih lanjut - bereksperimen dengan cython, dan bereksperimen dengan menafsirkan judul situs konten untuk penguraian pesan pantomim yang lebih cerdas

(tidak perlu memindai baris jika Anda mengetahui panjang-konten sub-pesan)

Sebagai catatan singkat, jika Anda melakukan streaming file langsung di badan permintaan (yaitu, tidak ada application/multipart-formdata ), Anda akan melewati pengurai formulir dan membaca file langsung dari request.stream .

Saya memiliki masalah yang sama dengan kecepatan unggah lambat dengan unggahan multi bagian saat menggunakan metode unggahan terpotong jQuery-File-Upload. Saat menggunakan potongan kecil (~ 10MB), kecepatan transfer melonjak antara 0 dan 12MB / dtk sementara jaringan dan server sepenuhnya mampu mencapai kecepatan lebih dari 50MB / dtk. Perlambatan ini disebabkan oleh penguraian multibagian yang terikat cpu yang memakan waktu sekitar waktu yang sama dengan unggahan sebenarnya. Sayangnya, menggunakan pengunggahan streaming untuk melewati penguraian multi bagian sebenarnya bukan merupakan pilihan karena saya harus mendukung perangkat iOS yang tidak dapat melakukan streaming di latar belakang.

Tambalan yang disediakan oleh

@carbn : Saya bisa mendapatkan patch untuk bekerja dengan Python 2.7 dengan mengubah baris terakhir menjadi collect = bytearray() . Ini hanya membuat bytearray baru alih-alih menghapus yang sudah ada.

@ Cuibonobo : Itu hal pertama yang saya ubah tetapi masih memiliki kesalahan lain. Saya tidak dapat memeriksa tambalan yang berfungsi saat ini, tetapi IIRC hasil harus diubah dari yield _cont, collect menjadi yield _cont, str(collect) . Ini memungkinkan kode untuk diuji dan tambalan menghasilkan sekitar 30% peningkatan kecepatan pemrosesan multi-bagian. Ini percepatan yang bagus, tetapi kinerjanya masih sangat buruk.

Penyelidikan lebih lanjut menunjukkan bahwa werkzeug.wsgi.make_line_iter sudah terlalu banyak menjadi hambatan untuk benar-benar dapat mengoptimalkan parse_lines() . Lihat skrip pengujian Python 3 ini:

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)

Untuk file video 923 MB dengan Python 3.5, outputnya terlihat seperti ini di laptop saya:

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

Jadi, bahkan jika Anda menerapkan pengoptimalan saya di atas dan mengoptimalkannya lebih jauh hingga sempurna, Anda masih akan dibatasi hingga ~ 45 MB / s untuk unggahan biner besar hanya karena make_line_iter tidak dapat memberikan data dengan cukup cepat dan Anda ' akan melakukan 7,5 juta iterasi untuk 923 MB data dalam loop Anda yang memeriksa batasnya.

Saya kira satu-satunya pengoptimalan yang hebat adalah mengganti parse_lines() sepenuhnya dengan sesuatu yang lain. Solusi yang mungkin terlintas dalam pikiran adalah membaca sebagian besar aliran ke dalam memori, lalu gunakan string.find () (atau bytes.find () dengan Python 3) untuk memeriksa apakah batasnya ada dalam potongan. Di Python find () adalah algoritma pencarian string yang sangat dioptimalkan yang ditulis dalam C, jadi itu akan memberi Anda beberapa kinerja. Anda hanya perlu menangani kasus di mana batas mungkin tepat di antara dua bagian.

Saya ingin menyebutkan melakukan parsing di aliran dalam potongan saat diterima. @siddhantgoel menulis parser kecil yang hebat ini untuk kami. Ini bekerja dengan baik untuk saya. https://github.com/siddhantgoel/streaming-form-data

Saya kira satu-satunya pengoptimalan yang bagus adalah mengganti parse_lines () sepenuhnya

1 untuk ini.

Saya menulis jembatan untuk mengalirkan unggahan pengguna langsung ke S3 tanpa file temporer apa pun, mungkin dengan backpressure, dan saya menemukan situasi werkzeug dan flask membuat frustrasi. Anda tidak dapat memindahkan data secara langsung di antara dua pipa.

@lambdaq Saya setuju ini adalah masalah yang perlu diperbaiki. Jika ini penting bagi Anda, dengan senang hati saya akan meninjau tambalan yang mengubah perilaku.

@lambdaq Perhatikan bahwa jika Anda hanya mengalirkan data secara langsung di badan permintaan dan menggunakan application/octet-stream maka parser formulir tidak bekerja sama sekali dan Anda dapat menggunakan request.stream (yaitu tidak ada file temporer dll) .

Satu-satunya masalah yang kami hadapi adalah parser formulir werkzeug dengan bersemangat memeriksa panjang konten terhadap panjang konten maksimal yang diizinkan sebelum mengetahui apakah itu benar-benar harus mengurai badan permintaan .

Ini mencegah Anda menyetel panjang konten maksimal pada data formulir normal, tetapi juga mengizinkan unggahan file yang sangat besar.

Kami memperbaikinya dengan sedikit mengurutkan fungsi centang . Tidak yakin apakah masuk akal untuk menyediakan upstream ini karena beberapa aplikasi mungkin bergantung pada perilaku yang ada.

Perhatikan bahwa jika Anda hanya mengalirkan data secara langsung di badan permintaan dan menggunakan application / octet-stream maka parser formulir tidak bekerja sama sekali dan Anda dapat menggunakan request.stream (misalnya, tidak ada file temporer, dll).

Sayangnya tidak. Ini hanya unggahan formulir biasa dengan multi bagian.

Saya akan dengan senang hati meninjau tambalan yang mengubah perilaku.

Saya mencoba meretas werkzeug.wsgi.make_line_iter atau parse_lines() menggunakan generator send() , jadi kami dapat memberi sinyal _iter_basic_lines() untuk memancarkan seluruh potongan alih-alih garis. Ternyata tidak semudah itu.

Pada dasarnya, keseluruhan kelinci dimulai dengan 'itertools.chain' object has no attribute 'send' .... 😂

Saya bertanya-tanya seberapa banyak kode ini dapat dipercepat menggunakan speedup asli yang ditulis dalam C (atau Cython dll.). Menurut saya, menangani file semi-besar (beberapa 100 MB, tetapi tidak sebesar banyak GB) secara lebih efisien adalah penting tanpa harus mengubah cara aplikasi menggunakannya (yaitu mengalirkannya secara langsung alih-alih buffering) - untuk banyak aplikasi ini akan menjadi berlebihan dan tidak mutlak diperlukan (sebenarnya, bahkan kinerja yang agak lambat saat ini mungkin OK untuk mereka) tetapi membuat segalanya lebih cepat selalu menyenangkan!

Solusi lain yang mungkin adalah memindahkan pekerjaan parsing multipart ke nginx

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

Kedua repo terlihat mati.

jadi apakah tidak ada solusi yang diketahui untuk ini?

Ada solusinya 👆

Di bawah uwsgi, kami menggunakan fungsi bawaan chunked_read() dan mengurai aliran kami sendiri saat masuk. Ini berfungsi 99% dari waktu, tetapi memiliki bug yang belum saya lacak. Lihat komentar saya sebelumnya untuk parser formulir streaming yang siap pakai. Di bawah python2 itu lambat, jadi kami menggulung sendiri dan cepat. :)

Mengutip dari atas:

Saya setuju itu masalah yang perlu diperbaiki. Jika ini penting bagi Anda, dengan senang hati saya akan meninjau tambalan yang mengubah perilaku.

Saya tidak benar-benar punya waktu untuk mengerjakan ini sekarang. Jika ini adalah sesuatu yang Anda habiskan waktu, mohon pertimbangkan untuk berkontribusi tambalan. Kontribusi sangat kami harapkan.

@tokopedia

tetapi ada bug yang belum saya lacak

apakah Anda berbicara tentang streaming-form-data ? jika demikian, saya ingin tahu apa bugnya.

Masalah kami adalah bahwa pemrosesan formulir yang lambat mencegah penanganan permintaan serentak yang menyebabkan nomad mengira proses tersebut macet dan mematikannya.

Perbaikan saya adalah menambahkan sleep(0) di 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)

cari unexpected end of stream jika Anda ingin menerapkan tambalan ini.

Saya ingin menyebutkan melakukan parsing di aliran dalam potongan saat diterima. @siddhantgoel menulis parser kecil yang hebat ini untuk kami. Ini bekerja dengan baik untuk saya. https://github.com/siddhantgoel/streaming-form-data

diperbantukan.
ini mempercepat unggahan file ke aplikasi Flask saya lebih dari faktor 10

@tokopedia
Terima kasih banyak atas perbaikan Anda dengan streaming-form-data. Saya akhirnya dapat mengunggah file berukuran gigabyte dengan kecepatan yang baik dan tanpa memori yang penuh!

Lihat # 1788 yang membahas tentang penulisan ulang parser menjadi sans-io. Berdasarkan umpan balik di sini, saya pikir itu akan mengatasi masalah ini juga.

Apakah halaman ini membantu?
0 / 5 - 0 peringkat

Masalah terkait

mhelmetag picture mhelmetag  ·  8Komentar

Nessphoro picture Nessphoro  ·  6Komentar

alexgurrola picture alexgurrola  ·  5Komentar

golf-player picture golf-player  ·  10Komentar

davidism picture davidism  ·  9Komentar