python-3.x - 无法将大文件上传到 GCP App Engine 中的 Python + Flask

标签 python-3.x google-app-engine flask google-cloud-platform google-cloud-storage

更新:(2020年5月18日)解决方案在本文末尾!

我正在尝试将大型 CSV 文件(30MB - 2GB)从浏览器上传到运行 Python 3.7 + Flask 的 GCP App Engine,然后将这些文件推送到 GCP 存储。这在使用大文件进行本地测试时效果很好,但如果文件大于大约 20MB,则 GCP 上会立即出错,并显示“413 - 您的客户端发出了太大的请求”。 此错误在上传时立即发生,甚至在到达我的自定义 Python 逻辑之前(我怀疑 App Engine 正在检查 Content-Length header )。经过大量 SO/博客研究后,我尝试了许多解决方案,但均无济于事。请注意,我正在使用基本/免费的 App Engine 设置以及运行 Gunicorn 服务器的 F1 实例。

首先,我尝试设置 app.config['MAX_CONTENT_LENGTH'] = 2147483648 但这并没有改变任何内容 ( SO post )。我的应用程序在到达我的 Python 代码之前仍然抛出错误:

# main.py
    app.config['MAX_CONTENT_LENGTH'] = 2147483648   # 2GB limit

    @app.route('/', methods=['POST', 'GET'])
    def upload():
        # COULDN'T GET THIS FAR WITH A LARGE UPLOAD!!!
        if flask.request.method == 'POST':

            uploaded_file = flask.request.files.get('file')

            storage_client = storage.Client()
            storage_bucket = storage_client.get_bucket('my_uploads')

            blob = storage_bucket.blob(uploaded_file.filename)
            blob.upload_from_string(uploaded_file.read())

<!-- index.html -->
    <form method="POST" action='/upload' enctype="multipart/form-data">
        <input type="file" name="file">
    </form>

经过进一步研究,我改用 Flask-Dropzone 进行分块上传,希望能够批量上传数据,然后将 CSV 文件附加/构建为存储 Blob:

# main.py
app = flask.Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 2147483648   # 2GB limit
dropzone = Dropzone(app)


@app.route('/', methods=['POST', 'GET'])
def upload():

    if flask.request.method == 'POST':

        uploaded_file = flask.request.files.get('file')

        storage_client = storage.Client()
        storage_bucket = storage_client.get_bucket('my_uploads')

        CHUNK_SIZE = 10485760  # 10MB
        blob = storage_bucket.blob(uploaded_file.filename, chunk_size=self.CHUNK_SIZE)

        # hoping for a create-if-not-exists then append thereafter
        blob.upload_from_string(uploaded_file.read())

JS/HTML 直接来 self 在网上找到的一些示例:

    <script>
       Dropzone.options.myDropzone = {
       timeout: 300000,
       chunking: true,
       chunkSize: 10485760 };
    </script>
    ....
    <form method="POST" action='/upload' class="dropzone dz-clickable" 
      id="dropper" enctype="multipart/form-data">
    </form>

上面确实分块上传(我可以看到对 POST/upload 的重复调用),但是,对 blob.upload_from_string(uploaded_file.read()) 的调用只是不断用最后上传的 block 替换 blob 内容,而不是追加。即使我删除 chunk_size=self.CHUNK_SIZE 参数,这也不起作用。

接下来我查看了写入 /tmp 然后写入存储,但文档说写入 /tmp 占用了我所拥有的少量内存,并且读取了其他地方的文件系统-only,所以这些都不起作用。

是否有附加 API 或经批准的方法来将大文件上传到 GCP App Engine 并推送/流式传输到存储?鉴于代码在我的本地服务器上运行(并且很高兴上传到 GCP 存储),我假设这是 App Engine 中需要解决的内置限制。


解决方案(2020 年 5 月 18 日) 我能够使用 Flask-Dropzone 让 JavaScript 将上传内容分割成许多 10MB 的 block ,并将这些 block 一次一个地发送到 Python 服务器。在 Python 方面,我们会不断附加到/tmp 中的文件以“构建”内容,直到所有 block 都进入。最后,在最后一个 block 上,我们将上传到 GCP Storage,然后删除/tmp 文件。

@app.route('/upload', methods=['POST'])
def upload():

    uploaded_file = flask.request.files.get('file')

    tmp_file_path = '/tmp/' + uploaded_file.filename
    with open(tmp_file_path, 'a') as f:
        f.write(uploaded_file.read().decode("UTF8"))

    chunk_index = int(flask.request.form.get('dzchunkindex')) if (flask.request.form.get('dzchunkindex') is not None)  else 0
    chunk_count = int(flask.request.form.get('dztotalchunkcount')) if (flask.request.form.get('dztotalchunkcount') is not None)  else 1

    if (chunk_index == (chunk_count - 1)):
        print('Saving file to storage')
        storage_bucket = storage_client.get_bucket('prairi_uploads')
        blob = storage_bucket.blob(uploaded_file.filename) #CHUNK??

        blob.upload_from_filename(tmp_file_path, client=storage_client)
        print('Saved to Storage')

        print('Deleting temp file')
        os.remove(tmp_file_path)
<!-- index.html -->
        <script>
          Dropzone.options.myDropzone = {
          ... // configs
          timeout: 300000,
          chunking: true,
          chunkSize: 1000000
        };
        </script>

请注意,/tmp 与 RAM 共享资源,因此您至少需要与上传文件大小一样多的 RAM,再加上 Python 本身的更多 RAM(我必须使用 F4 实例)。我想有一个更好的解决方案可以写入 block 存储而不是/tmp,但我还没有做到这一点。

最佳答案

答案是您无法在单个 HTTP 请求中上传或下载大于 32 MB 的文件。 Source

您需要重新设计服务以在多个 HTTP 请求中传输数据、使用预签名 URL 将数据直接传输到 Cloud Storage,或者选择不使用全局前端 (GFE) 的其他服务,例如 Compute Engine。这不包括 Cloud Functions、Cloud Run、App EngineFlexible 等服务。

如果您使用多个 HTTP 请求,则需要管理内存,因为所有临时文件都存储在内存中。这意味着当您接近 2 GB 的最大实例大小时,您将会遇到问题。

关于python-3.x - 无法将大文件上传到 GCP App Engine 中的 Python + Flask,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61858949/

相关文章:

python - python 文件的奇怪 IDE 行为

python - 字符串模板 4 和 Python

eclipse - 无法完成安装 Google Plugin for eclipse

python - 在函数内调用 return 时如何中断 Python Flask Controller 流程?

python-3.x - 如何向 Pandas 数据框列添加尾随零?

python - PyQt:为什么方法中的 QButtonGroup 需要 'self.' 前缀来发出按钮单击信号?

python - GAE : Routes vs standard

python - Google Cloud Datastore 'NoneType' 对象没有属性 'email'

python - 使用 Axios 响应 JS

javascript - 返回的 python 值在插入 javascript 数组后会破坏 html 表