MAATRIX / Блог / Сколько одновременных загрузок файлов выдержит сервер: считаем по буферам

Сколько одновременных загрузок файлов выдержит сервер: считаем по буферам

MAATRIX

Сервис принимает файлы от пользователей — аватарки, документы, видео, бэкапы — и при обычной нагрузке всё работает ровно. Потом кто-то запускает массовую загрузку: сто пользователей одновременно заливают отчёты в конце месяца, или клиент разом синхронизирует папку с фотографиями — и сервер начинает захлёбываться, хотя CPU почти свободен, а на диске вроде бы есть место. Причина обычно не в вычислительной мощности, а в буферах: каждая одновременная загрузка резервирует память на приём тела запроса, а иногда ещё и место на диске под временный файл, и этот бюджет заканчивается раньше, чем кажется. Ниже — как посчитать, сколько параллельных загрузок реально выдержит конкретный сервер, и что настроить, чтобы не упираться в это раньше времени.

Три места, где расходуется память при одновременной загрузке

Когда клиент отправляет файл через HTTP POST с multipart/form-data, тело запроса проходит минимум через два независимых буфера, прежде чем окажется на диске в виде готового файла:

  • Буфер веб-сервера (nginx). Директива client_body_buffer_size задаёт, сколько тела запроса nginx готов держать в памяти, прежде чем начнёт сбрасывать остаток на диск во временный файл. По умолчанию это небольшое значение (обычно 8–16 КБ в зависимости от платформы) — рассчитано на формы и API-запросы, а не на файлы.
  • Буфер приложения. Если за nginx стоит бэкенд (Node.js, Python, PHP-FPM, Go), он сам получает тело запроса и парсит multipart/form-data — и здесь есть два принципиально разных сценария: часть библиотек читает данные потоком и сразу пишет на диск или в объектное хранилище, а часть сначала собирает файл целиком в памяти (в Buffer, bytes, byte[]) и только потом отдаёт обработчику. Второй подход простой в коде, но именно он превращает 50 одновременных загрузок по 200 МБ в мгновенный расход 10 ГБ RAM.
  • Временный файл на диске. Когда тело запроса превышает буфер nginx, остаток пишется в client_body_temp_path — это снимает нагрузку с памяти, но переносит её на диск: нужно и свободное место, и запас по IOPS, иначе очередь записи временных файлов сама станет узким местом.

Ключевой момент: эти три расхода не взаимозаменяемы. Маленький буфер в nginx не спасёт, если приложение всё равно соберёт файл в памяти целиком после получения. А потоковая обработка в приложении не поможет, если nginx уже сложил весь файл в оперативную память до того, как передал его дальше.

Потоковая обработка против буферизации всего файла в памяти

Это развилка, которая определяет, зависит ли потребление памяти от размера файла или только от числа одновременных загрузок:

  • Буферизация целиком (buffering). Файл полностью читается в память (или во временный файл, который потом читается целиком) прежде, чем начинается его обработка — сохранение, валидация, отправка в S3. Пиковое потребление памяти на одну загрузку примерно равно размеру файла. При N одновременных загрузках по X МБ пиковый расход — N × X МБ, и он растёт линейно с обоими параметрами одновременно. Это самый частый источник внезапного OOM: пока файлы маленькие, всё работает, а стоит одному пользователю залить файл побольше — процесс падает.
  • Потоковая обработка (streaming). Данные читаются кусками (chunks) фиксированного размера — условно, десятки или сотни килобайт — и сразу пишутся в целевое место (диск, объектное хранилище, стрим дальше по конвейеру), не накапливаясь в памяти целиком. Пиковое потребление памяти на одну загрузку не зависит от размера файла — оно определяется только размером буфера чтения. При N одновременных загрузках расход памяти — N × размер_буфера, что на порядки меньше и предсказуемо не зависит от того, льёт ли пользователь файл на 5 МБ или на 5 ГБ.

Практическая проверка режима: залейте один файл заметного размера (сотни мегабайт) и посмотрите на RSS процесса через ps -o pid,rss,cmd -C node (или имя вашего рантайма) — если память растёт пропорционально размеру файла, у вас буферизация; если RSS почти не меняется — потоковая обработка уже работает.

В Node.js потоковый путь — библиотеки вроде busboy, которые эмитят события по мере разбора частей запроса и позволяют сразу пайпить данные в fs.createWriteStream, не дожидаясь конца запроса. В Python-фреймворках — чтение файла циклом чанками вместо одного вызова, читающего тело целиком. В Go у multipart.Reader поток по умолчанию, но его легко свести на нет, вызвав io.ReadAll(part) вместо построчного io.Copy(dst, part). Смысл один и тот же независимо от языка: узкое место — не конкретная библиотека, а то, вызывает ли ваш код что-то вроде «прочитать всё и вернуть один объект» вместо «читать и обрабатывать по частям».

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Как посчитать реальный предел сервера по одновременным загрузкам

Расчёт похож на любой другой бюджет памяти на сервере — берём доступный ресурс, вычитаем резерв, делим на расход на единицу нагрузки:

  1. Доступная память под загрузки. Из общей RAM вычтите резерв под ОС, page cache, другие сервисы (база, кэш) и уже занятую базовую память вашего процесса в простое. Общий подход к тому, сколько памяти закладывать с запасом на сервере, разобран в статье сколько оперативной памяти закладывать с запасом.
  2. Расход памяти на одну загрузку. Измерьте у себя, а не берите чужую цифру: для потоковой обработки это client_body_buffer_size (или аналогичный буфер приложения) плюс небольшой оверхед на соединение; для буферизации целиком — ожидаемый максимальный размер файла, который разрешён вашим client_max_body_size / лимитом приложения.
  3. Предел по памяти. Доступная_память / Расход_на_загрузку — это верхняя граница по одновременным загрузкам, которую даёт именно оперативная память.
  4. Предел по диску (если временные файлы пишутся на диск). Свободное место, делённое на средний ожидаемый размер файла, даёт второй потолок — по месту. Отдельно нужно прикинуть, вытянет ли диск запись N временных файлов одновременно по IOPS и пропускной способности — это не то же самое, что просто «есть свободное место»; методика такой прикидки разобрана в статье предел IOPS: как отличить упор в диск.
  5. Предел по числу соединений nginx. Отдельно от памяти и диска есть третий, независимый потолок — сколько одновременных HTTP-соединений вообще способен принять nginx на этой машине по worker_connections, дескрипторам и своей памяти; расчёт этого предела — тема отдельной статьи предел одного nginx worker: connections, дескрипторы и реальная граница.
  6. Итоговый предел — минимум из трёх. Одновременных загрузок сервер выдержит не больше, чем позволяет самый узкий из трёх потолков: память, диск, соединения.
ПотолокФормулаЧто ограничивает
По памятиRAM_доступная / расход_на_загрузкубуферы nginx + приложения
По диску (место)Свободное_место / средний_размер_файлаёмкость диска под temp-файлы
По диску (скорость)зависит от IOPS/пропускной способности диска и размера чанка записифизическая скорость записи
По соединениямworker_processes × worker_connections, ограниченный fd и памятью nginxконфигурация и лимиты ОС
Реальный пределmin(память, диск, соединения)самый узкий из перечисленных

Цифры в таблице — метод расчёта, а не готовые числа: расход памяти на загрузку, скорость диска и профиль файлов у каждого сервера свои, и подставлять стоит только измеренное на своей машине, а не усреднённые оценки из интернета.

Настройка nginx под большие объёмы одновременных загрузок

Если nginx стоит перед приложением как реверс-прокси, от его настройки зависит, попадёт ли нагрузка на память приложения вообще или осядет раньше:

http {
    # общий лимит на размер тела запроса — без него огромный
    # файл просто оборвёт соединение с 413 Request Entity Too Large
    client_max_body_size 500m;

    server {
        location /upload/ {
            # сколько тела держим в памяти, прежде чем уйти на диск
            client_body_buffer_size 128k;

            # куда и как складывать временные файлы, если тело
            # запроса больше client_body_buffer_size
            client_body_temp_path /var/cache/nginx/client_temp 1 2;

            # не передавать запрос бэкенду, пока не получено целиком —
            # для больших файлов лучше выключить и проксировать потоково
            proxy_request_buffering off;

            # таймауты приёма тела: медленный клиент не должен
            # держать буфер открытым бесконечно
            client_body_timeout 60s;

            proxy_pass http://backend;
        }
    }
}

Несколько практических нюансов этой настройки:

  • client_body_buffer_size — это компромисс, а не «чем больше, тем лучше». Слишком маленький буфер означает, что почти каждая загрузка уходит во временный файл на диске (лишняя запись и чтение), а слишком большой — что при N одновременных загрузках nginx резервирует N × размер_буфера памяти даже для мелких файлов, которые в него не помещаются. Разумная отправная точка — чуть выше типичного размера файла для вашего сценария (аватарки — единицы КБ хватит с запасом, документы и видео — заметно больше), а не максимальное значение «на всякий случай».
  • proxy_request_buffering off заставляет nginx передавать тело запроса бэкенду потоково, по мере получения, а не дожидаться его целиком. Это снижает пиковую память на стороне nginx при больших файлах, но требует, чтобы бэкенд тоже умел принимать поток — иначе выигрыш теряется на следующем шаге конвейера.
  • client_body_temp_path стоит явно вынести на диск с запасом места и, желательно, отдельно от системного раздела — если временные файлы загрузок забьют тот же раздел, где живут логи или база, отказ каскадом заденет не только загрузки.
  • Проверить, что реально применилось, можно тем же способом, что и для любых директив nginx — nginx -T | grep -E "client_body|client_max" покажет действующие значения по всем блокам конфигурации, а не только то, что вы правили последним.

Настройка приложения: где чаще всего теряют память

Даже идеально настроенный nginx не спасёт, если бэкенд сам буферизует файл целиком. Практические шаги для приложения:

  • Ограничьте максимальный размер файла и число одновременных загрузок на уровне приложения отдельно от nginx. client_max_body_size в nginx — это защита периметра, но приложению всё равно стоит знать свой собственный лимит и отклонять запрос до начала обработки, если превышен разумный размер для конкретного эндпоинта (аватарка и бэкап — разные лимиты, и валидировать их стоит раздельно).
  • Проверьте библиотеку парсинга multipart на потоковость явно, а не по названию — многие библиотеки предоставляют оба режима API, и лёгкость сваливания в буферизацию целиком часто маскируется удобным синтаксисом вроде «получить файл одним вызовом».
  • Пишите временный файл сразу на диск или сразу в целевое хранилище, не через промежуточный Buffer в памяти процесса. Если конечная точка — объектное хранилище (S3-совместимое), используйте multipart upload API самого хранилища, которое тоже льёт данные потоково, а не собирает файл на сервере целиком перед повторной отправкой дальше.
  • Ставьте лимит на число одновременных обработчиков загрузки, если фреймворк это позволяет (очередь, семафор, пул воркеров) — это дешевле, чем полагаться только на память: лучше вернуть клиенту вежливый 429 Too Many Requests на 101-ю одновременную загрузку, чем уронить процесс OOM-киллером и потерять все 100 уже шедших. Фактическое число одновременных загрузок и пиковую память под нагрузкой стоит логировать отдельно — без этой метрики предел сервера так и останется теоретическим числом из расчёта.

Диск под временные файлы: место и IOPS, а не только ёмкость

Когда файлы не помещаются в буфер памяти и уходят во временные файлы на диске, легко посчитать только «хватит ли места» и забыть про скорость. Оба параметра важны отдельно:

  • Место. N_одновременных_загрузок × средний_размер_файла — пиковый расход дискового пространства под временные файлы в момент максимальной нагрузки, и для больших файлов (бэкапы, видео) он может ощутимо превысить обычный ежедневный расход диска. Мониторинг места и причины его внезапного исчерпания — в статье закончилось место на диске VPS.
  • Скорость записи и IOPS. Даже при достаточном месте одновременная запись десятков временных файлов создаёт конкурентную нагрузку на диск, и на медленном или перегруженном диске очередь записи сама становится узким местом — загрузки выстраиваются в очередь на I/O, а не на память или CPU.
  • Очистка временных файлов. Если загрузка обрывается (клиент закрыл вкладку, пропала связь), временный файл должен гарантированно удаляться — самим сервером/фреймворком или периодической задачей очистки каталога temp по возрасту файла. Накопление «осиротевших» файлов — частая скрытая причина того, что диск заполняется быстрее ожидаемого при том же трафике.
  • Отдельный раздел или каталог с квотой под временные файлы загрузок снижает риск того, что всплеск загрузок заденет остальные сервисы на той же машине через общий диск.

Проверка того, во что реально упирается сервер под нагрузкой — в память, диск или CPU — делается тем же способом, что и для любой другой утилизации ресурсов: free -m и vmstat покажут давление на память и своп, iostat -x 1 — загрузку диска и очередь запросов к нему, а узкое место стоит определять по совпадению этих метрик во времени с моментом всплеска загрузок.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Что произойдёт, если превысить client_max_body_size в nginx?

Клиент получит 413 Request Entity Too Large до того, как тело запроса начнёт обрабатываться приложением — это защитный лимит, и его стоит держать чуть выше реального максимального размера файла для конкретного эндпоинта, а не ставить огромное значение «про запас», иначе он перестаёт защищать от аномально больших запросов.

Потоковая загрузка полностью снимает вопрос о пределе одновременных загрузок?

Нет, только снижает расход памяти на файл. Предел всё равно есть — по числу соединений, по диску под временные файлы и по CPU на разбор заголовков и TLS. Потоковая обработка убирает зависимость памяти от размера файла, но не от числа одновременных клиентов.

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

Технически можно для маленьких файлов при небольшом числе одновременных загрузок, но это делает память единственным ограничителем и убирает запас прочности — при всплеске загрузок процесс упрётся в OOM резко и без промежуточных симптомов. Надёжнее оставить путь через диск или прямой стрим в объектное хранилище.

Как понять, что сервер уже близок к пределу по одновременным загрузкам, до того как начнутся ошибки?

Следите одновременно за тремя метриками под нагрузкой: RSS процесса приложения (рост, коррелирующий с числом активных загрузок), свободное место и IOPS на разделе с временными файлами, и Active connections из stub_status nginx относительно расчётного потолка по worker_connections. Момент, когда любая из трёх метрик стабильно приближается к своему потолку при обычном, не аномальном трафике, — сигнал пересчитать бюджет и поднять соответствующий лимит заранее, а не после первого падения.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →