Передача больших файлов медленная по HTTP, но быстрая с помощью SCP

Вопрос или проблема

Мне нужно передать изображения объемом 432 МБ между двумя компьютерами с Linux, которые подключены к одной быстрой сети. scp передает каждое изображение менее чем за 1 секунду. Код ниже, использующий HTTP, занимает от 3 до 8 секунд на изображение.

На клиентских и серверных компьютерах нет другой загрузки, кроме этого процесса передачи. Использование ЦП и диска на обоих компьютерах низкое. HTTP-сервер не обрабатывает других запросов. Передача файлов по HTTP и scp проходит успешно. Ошибок нет, и я не получал сообщений о тайм-ауте.

Я понимаю, что HTTP имеет больший оверхед, чем scp, но я не ожидал, что это увеличит время передачи более чем в 3 раза.

Я потратил более часа безуспешно ища причины этой разницы в производительности. Чат-боты не помогают.

Можете ли вы сообщить, ожидаемо ли это и есть ли способы сделать передачу по HTTP более близкой к производительности scp.

Код, использованный для процесса HTTP:

client.py

#!/usr/bin/python3
import argparse
import requests
import sys, os

# Предопределенные переменные
SERVER = 'ua02861p.abbvienet.com'
PORT = 8443


def upload_image(image_path, url, verify_ssl):
    with open(image_path, 'rb') as img:
        files = {'file': img}
        response = requests.post(url, files=files, verify=verify_ssl)
    try:
        response.raise_for_status()
    except requests.exceptions.HTTPError as e:
        print(f"HTTP ошибка: {e}")
        return response.json()

    return response.json()

def main():
    parser = argparse.ArgumentParser(description="Загрузка изображений на сервер FastAPI.")
    parser.add_argument('-s', '--server', type=str, default=SERVER, help=f'Адрес сервера (имя хоста или IP) [по умолчанию: {SERVER}]')
    parser.add_argument('-p', '--port', type=int, default=PORT, help=f'Номер порта [по умолчанию: {PORT}]')
    parser.add_argument('--use-http', action='store_true', default=False, help='Использовать HTTPS вместо HTTP [по умолчанию: True]')
    parser.add_argument('--verify-ssl', action='store_true', default=False, help='Включить проверку SSL-сертификата [по умолчанию: False]')
    parser.add_argument('filenames', type=str, nargs="+", help='Один или несколько путей к файлам изображений')

    args = parser.parse_args()

    protocol = "http" if args.use_http else "https"
    server = args.server
    port = args.port
    filenames = args.filenames
    verify_ssl = args.verify_ssl

    url = f"{protocol}://{server}:{port}/upload-image/"

    for image_path in filenames:
        print(f"Загрузка {image_path} на {url} ", end = '')
        ''' перенаправление stderr, чтобы убрать раздражающее предупреждение о проверке ssl '''
        stderr = sys.stderr 
        sys.stderr = open(os.devnull,'w')
        response = upload_image(image_path, url, verify_ssl)
        sys.stderr = stderr
        print(f"({response['info']})")

if __name__ == "__main__":
    main()

server.py

#!/usr/bin/python3
from fastapi import FastAPI, File, UploadFile, Request, HTTPException
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from pathlib import Path
import uvicorn
import requests
import os
import sys

# По умолчанию
# SERVER = 'ua02861p.abbvienet.com' #
SERVER = '0.0.0.0' # Слушать все интерфейсы
PORT = 8443

def fpath(file_path):
    if os.path.exists(file_path):
        #print(f"Файл '{file_path}' существует.")
        return file_path
    else:
        # Добавить директорию скрипта к пути файла
        script_directory= os.path.dirname(sys.argv[0])
        #print (f'script_directory = {type (script_directory)}')
    
        modified_file_path = os.path.join(script_directory, file_path)
        #print(f"Измененный путь файла '{modified_file_path}' существует. {type (modified_file_path)}")
        if os.path.exists(modified_file_path):
            return modified_file_path
        else:
            raise FileNotFoundError(f"{file_path} не существует.")

app = FastAPI()

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=422,
        content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
    )

@app.post("/upload-image/")
async def upload_image(file: UploadFile = File(...)):
    file_location = f"uploaded_images/{file.filename}"
    Path("uploaded_images").mkdir(parents=True, exist_ok=True)
    with open(file_location, "wb") as buffer:
        buffer.write(await file.read())
    return {"info": "файл успешно загружен", "filename": file.filename}

def get_module_name():
    return os.path.splitext(os.path.basename(sys.argv[0]))[0]

def start_uvicorn():
    import argparse

    parser = argparse.ArgumentParser(description="Запуск сервера FastAPI с uvicorn.")
    parser.add_argument('--host', type=str, default=SERVER, help='Хост сервера [по умолчанию: {SERVER}')
    parser.add_argument('--port', type=int, default=PORT, help='Порт сервера [по умолчанию: {PORT}]')
    parser.add_argument('--log-level', type=str, default="debug", help='Уровень логирования [по умолчанию: debug]')
    parser.add_argument('--ssl-keyfile', type=str, default="key.pem", help='Путь к файлу ключа SSL [по умолчанию: key.pem]')
    parser.add_argument('--ssl-certfile', type=str, default="cert.pem", help='Путь к файлу сертификата SSL [по умолчанию: cert.pem]')
    args = parser.parse_args()

    keyfile=fpath(args.ssl_keyfile)
    certfile=fpath(args.ssl_certfile)
    # Печать отладочной информации о файлах SSL
    if keyfile:
        print(f"Использование файла ключа SSL: {keyfile}")
        if not os.path.exists(keyfile):
            raise FileNotFoundError(f"Файл ключа SSL не найден: {keyfile}")
    else:
        print("Файл ключа SSL не предоставлен")

    if certfile:
        print(f"Использование файла сертификата SSL: {certfile}")
        if not os.path.exists(certfile):
            raise FileNotFoundError(f"Файл сертификата SSL не найден: {certfile}")
    else:
        print("Файл сертификата SSL не предоставлен")

    module_name = get_module_name()
  
    uvicorn.run(
        f"{module_name}:app",
        host=args.host,
        port=args.port,
        log_level=args.log_level,
        ssl_keyfile=keyfile,
        ssl_certfile=certfile
    )

if __name__ == "__main__":
    start_uvicorn()

Ответ или решение

Передача больших файлов может демонстрировать различные показатели производительности в зависимости от использования протоколов. В вашем случае, передача файлов через HTTP происходит медленнее, чем через SCP, что может объясняться несколькими факторами.

Причины различий в производительности:

  1. Проверка целостности данных: Протокол SCP часто использует механизм проверки целостности данных, который более оптимизирован по сравнению с HTTP. При передаче HTTP сервер может выполнять дополнительные проверки, что увеличивает задержку.

  2. Структура протокола: HTTP работает на основе запросов и ответов, что подразумевает большее количество накладных расходов, таких как установка соединения, обработка заголовков и аутентификация. SCP, наоборот, использует SSH для создания единого канала передачи, что позволяет передавать данные более эффективно.

  3. Параметры сервера: Ваша реализация сервера FastAPI, возможно, не оптимизирована для отправки больших файлов. Обратите внимание на настройки uvicorn, который вы используете для запуска FastAPI.

  4. Размеры буферов и потока: SCP может использовать более крупные буферы для передачи данных, что может значительно увеличить скорость передачи. При использовании HTTP для передачи файлов, данные могут обрабатываться в меньших порциях, что приводит к большему количеству операций ввода-вывода.

  5. Настройки сети: Проверьте, есть ли какие-либо ограничения или настройки в вашей сетевой инфраструктуре, которые могут повлиять на скорость передачи по протоколу HTTP.

Рекомендации для повышения производительности HTTP:

  1. Используйте Streaming: Вместо того чтобы загружать файл целиком в память перед отправкой, используйте потоковую передачу. FastAPI поддерживает потоковую передачу, что может помочь сократить время передачи.

    Измените upload_image в server.py следующим образом:

    @app.post("/upload-image/")
    async def upload_image(file: UploadFile = File(...)):
       file_location = f"uploaded_images/{file.filename}"
       Path("uploaded_images").mkdir(parents=True, exist_ok=True)
       with open(file_location, "wb") as buffer:
           while content := await file.read(1024 * 1024):  # Чтение файла по частям
               buffer.write(content)
       return {"info": "file uploaded successfully", "filename": file.filename}
  2. Настройка uvicorn: Если вы используете SSL, убедитесь, что это не создает дополнительной задержки. Также рассмотрите возможность использования более оптимальных значений для --workers, чтобы использовать возможности многопоточности и многопроцессности (зависит от вашей архитектуры серверов).

  3. Изменения в клиенте: Как и в серверной части, попробуйте реализовать потоковую отправку на клиенте для более эффективной передачи данных.

  4. Настройки HTTP-сервера: Если вы используете дополнительные модули или прокси-серверы (например, Nginx или Apache), убедитесь, что их конфигурация не ограничивает скорость передачи.

Заключение

Ваша наблюдаемая разница в скорости передачи данных через HTTP и SCP вполне объяснима. Учитывая рекомендации, вы сможете значительно повысить производительность передачи через HTTP и приблизиться к скорости, которую демонстрирует SCP. Если все предложенные шаги не приведут к ожидаемым результатам, возможно, стоит продолжить использовать SCP для задач, связанных с передачей больших файлов, до тех пор, пока не будет найдено более оптимальное решение.

Оцените материал
Добавить комментарий

Капча загружается...