Как избежать тайм-аута простоя при загрузке большого файла?

Рассмотрим нашу текущую архитектуру:

         +---------------+                             
         |    Clients    |                             
         |    (API)      |                             
         +-------+-------+                             
                 ∧                                     
                 ∨                                     
         +-------+-------+    +-----------------------+
         | Load Balancer |    |   Nginx               |
         | (AWS - ELB)   +<-->+   (Service Routing)   |
         +---------------+    +-----------------------+
                                          ∧            
                                          ∨            
                              +-----------------------+
                              |   Nginx               |
                              |   (Backend layer)     |
                              +-----------+-----------+
                                          ∧            
                                          ∨            
         -----------------    +-----------+-----------+
           File Storage       |       Gunicorn        |
           (AWS - S3)     <-->+       (Django)        |
         -----------------    +-----------------------+

Когда клиент, мобильный или веб-сервер пытается загружать большие файлы (более GB) на наши серверы, часто приходится сталкиваться с таймаутами в режиме ожидания. Либо из их клиентской библиотеки, например, в iOS, либо из нашего балансировщика нагрузки.

Когда файл фактически загружается клиентом, никаких тайм-аутов не происходит, потому что соединение не является "незанятым", байты передаются. Но я думаю, что когда файл был перенесен в базовый слой Nginx, и Django начинает загрузку файла на S3, соединение между клиентом и нашим сервером становится бездействующим до завершения загрузки.

Есть ли способ предотвратить это и на каком уровне я должен решить эту проблему?

Ответ 1

Вы можете создать обработчик загрузки для загрузки файла непосредственно на s3. Таким образом, вы не должны сталкиваться с таймаутом соединения.

https://docs.djangoproject.com/en/1.10/ref/files/uploads/#writing-custom-upload-handlers

Я сделал несколько тестов, и он отлично работает в моем случае.

Вам нужно запустить новый multipart_upload с помощью boto, например, и постепенно отправлять куски.

Не забудьте проверить размер блока. 5Mb является минимальным, если ваш файл содержит более 1 части. (Ограничение S3)

Я думаю, что это лучшая альтернатива django-queued-storage, если вы действительно хотите загрузить непосредственно на s3 и избежать таймаута соединения.

Возможно, вам также понадобится создать собственное поле для правильного управления файлом, а не отправить его второй раз.

Ниже приведен пример с S3BotoStorage.

S3_MINIMUM_PART_SIZE = 5242880


class S3FileUploadHandler(FileUploadHandler):
    chunk_size = setting('S3_FILE_UPLOAD_HANDLER_BUFFER_SIZE', S3_MINIMUM_PART_SIZE)

    def __init__(self, request=None):
        super(S3FileUploadHandler, self).__init__(request)
        self.file = None
        self.part_num = 1
        self.last_chunk = None
        self.multipart_upload = None

    def new_file(self, field_name, file_name, content_type, content_length, charset=None, content_type_extra=None):
        super(S3FileUploadHandler, self).new_file(field_name, file_name, content_type, content_length, charset, content_type_extra)
        self.file_name = "{}_{}".format(uuid.uuid4(), file_name)

        default_storage.bucket.new_key(self.file_name)

        self.multipart_upload = default_storage.bucket.initiate_multipart_upload(self.file_name)

    def receive_data_chunk(self, raw_data, start):
        buffer_size = sys.getsizeof(raw_data)

        if self.last_chunk:
            file_part = self.last_chunk

            if buffer_size < S3_MINIMUM_PART_SIZE:
                file_part += raw_data
                self.last_chunk = None
            else:
                self.last_chunk = raw_data

            self.upload_part(part=file_part)
        else:
            self.last_chunk = raw_data

    def upload_part(self, part):
        self.multipart_upload.upload_part_from_file(
            fp=StringIO(part),
            part_num=self.part_num,
            size=sys.getsizeof(part)
        )
        self.part_num += 1

    def file_complete(self, file_size):
        if self.last_chunk:
            self.upload_part(part=self.last_chunk)

        self.multipart_upload.complete_upload()
        self.file = default_storage.open(self.file_name)
        self.file.original_filename = self.original_filename

        return self.file

Ответ 2

Я столкнулся с той же проблемой и исправил ее, используя django-queued-storage поверх django-storages. Что такое хранилище в очереди django, так это то, что когда файл получен, он создает задачу celery, чтобы загрузить его в удаленное хранилище, такое как S3, и в среднем, если файл доступен кому-либо, и он еще не доступен на S3, он обслуживает его из локального файловая система. Таким образом, вам не нужно ждать, пока файл будет загружен на S3, чтобы отправить ответ клиенту.

В качестве приложения для Load Balancer вы можете использовать общую файловую систему, такую ​​как Amazon EFS, чтобы использовать вышеуказанный подход.

Ответ 3

Вы можете попытаться пропустить загрузку файла на свой сервер и загрузить его непосредственно на s3, а затем вернуть URL-адрес для своего приложения.

Есть приложение для этого: django-s3direct вы можете попробовать.