Вчера автор побывал на прекрасной конференции Backend Talks 360 от компании Yandex и не в силах спокойно спать, не ответив конструктивной критикой на доклад Ильи Абрамова о работе Яндекс.Диска, автор принялся писать очередной «В интернете кто‑то не прав». Что из этого вышло — добро пожаловать под кат.

Почему загрузка больших файлов одним запросов для облачных хранилищ — это тупик

Одним из основных пунктов докладчика было балансирование нагрузки между «кладунами». Но в итоге доклад перерос в балансирование нагрузки между «балансерунами» и «кладунами».

Схематичное обобщение доклада
Схематичное обобщение доклада

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

  1. Пользователь из балансеруна получает ссылку на кладуна;

  2. На указанный кладун загружается файл (целиком).

Из‑за разности в скоростях подключений всех пользователей гарантированно получаем при любых попытках в такой архитектуре что‑либо балансировать перекосы по нагрузке на кладунов. Гарантированно будут перегруженные, и так же гарантированно будут недогруженные. Причем способ выбора не имеет значения.

Докладчик предложил решение в ходе которого удалось улучшить распределение за счет довольно больших и трудоемких решений, требующих сами по себе инфраструктуру, которой, как говорил докладчик итак не хватает.

Не будем указывать на то, что в случае ошибок сети, все загружаемые гигабайты можно будет выкидывать и заливать заново, поэтому попробуем исправить изначальную ошибку и покажем, что есть решение, при котором не нужны балансеруны вовсе, но при условии, что у всех кладунов есть общее хранилище (например nfs, главное, что бы у всех кладунов в группе было оно общим, а так же общая БД). Так как внешний балансировщик все равно есть, будем опираться именно на него.

Загрузка файлов блоками

Уже давно придумано решение, когда файл заливается блоками и после загрузки проверяется на целостность, так делает Google.drive, а так же некоторые другие облачные хранилища.

Работает блочная загрузка только при условии, что эти блоки всегда выровнены по степени двойки (на самом деле по размеру буфера, который пишется на диск операционной системой), в противном случае будет гарантированное повреждение при передаче (например один пишет пару байт в конце буфера, обнуляя все что писали до него из‑за неправильного смещения и гонок).

Архитектура решения для такого варианта тривиальная

Пользователи могут отправлять запросы на любой макладун, так как размер тела запроса не может превышать размер блока + небольшой оверхед на заголовки и остальную обвязку http протокола, нагрузка будет ложиться достаточно ровно, что бы не перегружать никого.
Пользователи могут отправлять запросы на любой макладун, так как размер тела запроса не может превышать размер блока + небольшой оверхед на заголовки и остальную обвязку http протокола, нагрузка будет ложиться достаточно ровно, что бы не перегружать никого.

Все API при этом разделяется на 2 класса: все что меньше блока (маленькие файлы) и все что больше блока (большие файлы). Размер блока можно выбрать любой степенью двойки, например 4 194 304 (4 мегабайта).

При такой организации мелкие файлы пользователи будут спокойно загружать и не создавать лишней нагрузки на систему (иначе пришлось бы делать 3 запроса). Таких файлов большинство и поэтому нужна отдельная ручка для таких запросов.

В случае больших файлов (а иногда требуется загружать сотни гигабайт), жизненно необходимо гарантировать, что у пользователя всегда будет возможность продолжить загрузку. Поэтому для этого формируется отдельное API. Смысл в том, что в начале отправляется запрос на подготовку загрузки большого файла, в этом запросе сообщается размер загружаемого файла, контрольная сумма для сверки и другие необходимые и полезные пользователю свойства, например флаг «перед открытием удалить». Далее в один или несколько потоков загружаются блоки, а по завершении загрузки всех блоков — специальный запрос на завершение.

Общий алгоритм тогда такой (лишь один из вариантов):

  1. Получить идентификатор для загрузки большого файла;

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

  3. Дождаться завершения загрузки всех блоков;

  4. По окончании загрузки — сообщить сервису об окончании, и начале процессе удаления загруженного валидации файла (проверки контрольной суммы, можно еще антивирусом погонять). Контрольную сумму можно сообщать на данном этапе, а не создания загрузки.

Такое API — простое для реализации любым разработчиком, да и Яндекс достаточно богатая компания, что бы предоставлять клиенты на различных языках программирования для интеграции своим пользователям.

Простой пример

Дабы не быть голословным, и развеять все сомнения у тех, кто может воскликнуть, что это не будет работать, автор подготовил простое приложение. Написано оно так же на простом языке программирования Rust.

Код доступен по ссылке.

Сервис реализует простейшее апи:

  1. POST /api/upload/small‑file/{user_id}/{display_file_name:.*} — Загрузка маленького файла на диск, display_file_name — это специальный параметр, который берет весь остаток пути.
    В запросе ожидается заголовок x‑sha-512.

  2. POST /api/upload/large‑file/new — создать файл для загрузки, он будет считаться незавершенным, пока не будет завершен процесс загрузки. В запросе передается тело, в котором указывается: идентификатор пользователя, отображаемое имя файла, размер и контрольная сумма.

  3. PUT /api/upload/large‑file/{file_id}/chunk/{offset} — пишет блок по смещению, проверяя контрольную сумму полученного блока перед записью; Вернет 400, если контрольная сумма неверна.
    В запросе ожидается заголовок x‑sha-512 — контрольная сумма блока.
    Данный запрос можно направлять на любого макладуна, так как запись осуществляется в непересекающиеся блоки.

  4. POST /api/upload/large‑file/{file_id}/done — уведомление о завершении загрузки, синхронно (лучше это делать фоновой задачей) проверяет целостность файла, вернет 200, только если контрольные суммы совпали.

В чем главное преимущество данного API? Оно дружелюбно к повторам, если по каким‑то причинам сервер не ответил, то клиент может отправить блок заново и не прерывать загрузку.

Как использовать написанное приложение

Для запуска нам потребуется PostgreSQL:

podman run --rm --name test-db \
  -e POSTGRES_PASSWORD=xxXX12341234 \
  -e POSTGRES_USER=user \
  -p 5432:5432 \
  postgres:17

После этого из корня проекта запускаем миграцию БД:

sqlx migrate run

Теперь можно запустить парочку инстансов:

cargo run --bin makladun -- -c config/makladun_local.yaml

И в другом терминале

cargo run --bin makladun -- -c config/makladun_local2.yaml

Если оба сервиса успешно запущены и будут писать в папку /tmp/makladun/1, убедитесь, что она создана. Вы можете смонтировать сетевой диск (smb, nfs), и пробовать загружать туда большие файлы (не рекомендую больше 100мб, так как почему‑то расчет SHA-512 занимает много времени, консольная утилита делает быстрее на порядок), маленькие файлы работать перестанут, потому что перемещения между дисками нет, а править это на копирование нет желания.

Пример для загрузки файла:

cargo run --bin disk-io-client -- -u 2 -d "makladun" \
  -f /tmp/makladun/makladun \
  -p 4 \
  -b http://localhost:8080 -b http://localhost:8081

Что сделает эта команда: от пользователя 2, создаст файл makladun в «облачном хранилище в корне», возьмет файл из /tmp/makladun/makladun и будет заливать его в 4 потока. Отправка запросов осуществляется на сервисы http://localhost:8080 и http://localhost:8081 случайно. Если поставите балансировщик, то рекомендую использовать round‑robin стратегию балансировки, так как она даст наилучший результат. Ссылку тогда достаточно будет оставить одну.

Конфигурация устроена следующим образом:

http:
  port: 8080
storages:
  - key: "1"
    path: "/tmp/makladun/1" # Можно указать множество хранилищ, в том числе сетевые диски
                            # главное, что бы эта папка существовала. Выбирается случайно.
  # - key: "2"
  #   path: "/tmp/makladun/2"
chunkSize: 4096 # Размер блока в килобайтах, все что меньше - считается маленьким файлом
db:
  host: "localhost"
  port: 5432
  database: "postgres"
  schema: "public"
  user: "user"
  password: "xxXX12341234"
  additionalOptions:
    search_path: "public"

Вместо заключения

Илья Абрамов с командой проделали огромную работу, и на самом деле сложную. Но иногда решение лежит куда как ближе и проще. Не всегда переусложнение имеет смысл.

Возможно, что эта статья позволит Яндекс.Диску стать немного лучше и производительнее.;)

Комментарии (18)


  1. Roman2dot0
    17.05.2026 10:15

    но при условии, что у всех кладунов есть общее хранилище (например nfs, главное, что бы у всех кладунов в группе было оно общим, а так же общая БД).

    И вот это условие убивает на корню геораспределённые системы.

    И из статьи не понятно, но думаю, что "кладун" - это не один сервер с одним диском на который заливается объект целиком, а распределённое хранилище, которое при заливке "нарезает" объёкт, управляет репликацией и т.д.


    1. lastrix Автор
      17.05.2026 10:15

      Репликация между локациями - это уже отдельный процесс, Илья рассказывал именно о том, как залить файл в облако. То что происходит дальше - это вообще отдельная песня =)


    1. lastrix Автор
      17.05.2026 10:15

      Раз уж зашла речь про геораспределенные системы.

      Тут вы абсолютно не правы. Решение такое же будет.

      В случае, если у вас есть сервера в Москве, Владивостоке и Пекине, то по хорошему ваш "disk.cloud.ru" будет указывать на разные IP адреса (во всяком случае все нормальные геораспределенные системы так проектируются), не пытаются сливать всех в один балансировщик в Москве. Т.е. у вас будут разные балансировщики в разных частях планеты, и каждый работать будет со своими "кладунами". Репликация - это отдельный процесс и он не всегда должен осуществляться, есть вообще такие законы как приземление информации и вы не можете перезаливать файлы пользователя куда-нибудь в Чили.

      Так что минус все же ваш.


      1. Roman2dot0
        17.05.2026 10:15

        Гео - это не только города в разных частях планеты, но и разные ДЦ в одном регионе.
        Например, в Московской области, где между ДЦ будет RTT 3-5 мс. Каждый ДЦ - свой кладун.
        Мы же не хотим побить пользовательские данные? Тогда нам не подходят асинхронные репликации в бд и общем сторадже -> получаем неприемлимые тормоза на сколько-нибудь значимых объёмах записи.
        Конечно, можно написать, что "у вас будут разные балансировщики", т.е. балансировщик на уровне Мск, который отправит на нужный (ближайший или менее загруженный) кладун в регионе, то тогда чем эта схема отличается от схемы Яндекса?)


        1. lastrix Автор
          17.05.2026 10:15

          Раз уж вы так настойчиво желаете выйти за границы ДЦ.

          В случае множества ДЦ в одном регионе придется в любом случае каким-то образом выбирать конкретный для конкретного пользователя, а еще лучше и для конкретного запроса.

          Заливать файл частью в один ДЦ, а другую во второй - идея плохая, так как объединять файл размером 100 ГБ+ ничего хорошего не даст. И тоже самое касается маршрутизации, если ДЦ1 перекидывает в ДЦ2 тем или иным образом запросы.

          И повторюсь, даже в приведенном вами случае (кладун=ДЦ) все равно получается, что изложенное в статье имеет место быть. В докладе Ильи была информация о том, что канал кладуна - 4 гбит/с. До ДЦ явно не может быть 4 гбит/с, из чего было предположение о том, что работа в рамках одного ДЦ и статья писалась с этим пунктом. В этом случае общий сетевой диск и общая БД - это разумное решение.

          Кладун всегда знает свою нагрузку, тем более что Илья и ко сделали потрясающую работу по сбору такой статистики (и они могут ее хранить и анализировать локально в ДЦ). Если по каким-то причинам кладун не может обработать запрос клиента - всегда можно отправить http-код на переотправку запроса (если речь про загрузку маленького файла или подготовку загрузки большого) и балансировщик на уровне мск переведет из-за round-robin запрос на другой ДЦ. В случае с балансерунами будет все тот же RTT 3-5 мс, что бы собрать статистику со всех ДЦ в одном месте. Решение с балансерунами плохо масштабируется, не говоря о том, что балансерун может выдать ссылку на кладуна, которого уже нет в живых.

          Нет смысла перекладывать работу кладуна куда-то еще - это переусложнение и увеличение нагрузки на инфраструктуру.


  1. V1tol
    17.05.2026 10:15

    почему-то расчет SHA-512 занимает много времени, консольная утилита делает быстрее на порядок

    Глянул код, есть две рекомендации - использовать BufReader для уменьшения сисколлов, да и размер я бы увеличил - 4кб буфер уж сильно часто будет дёргать чтение. Если верно нашёл, у гнушного sha512sum размер буфера по-умолчанию 32кб.


    1. lastrix Автор
      17.05.2026 10:15

      Полагаю, что вы правы =)
      Спасибо за совет.


  1. anonymous
    17.05.2026 10:15


    1. lastrix Автор
      17.05.2026 10:15

      Для молотка - везде гвозди.


  1. anonymous
    17.05.2026 10:15


  1. Politura
    17.05.2026 10:15

    А зачем выдумывать какие-то странные слова? Балансерун, кладун, макладун.

    Так-то multipart upload/download придумано давно и используется повсеместно.


    1. lastrix Автор
      17.05.2026 10:15

      Это термины докладчика, а не мои =)


  1. AndrewTishkin
    17.05.2026 10:15

    По поводу странных слов.

    Вышел к народу Яндекс-воин, достал свой меч-клаждунец...
    ...и обалансрался народ от неожиданности.

    Возможно "старших" по диску приберегли на июньский YoungCon, поэтому не стали дёргать на данное мероприятие, а отдали "малышне". Ну либо просто боевое крещение - а расскажи-ка публике о том, с чем работаешь. Пропусти через себя.

    И вот такой пропущенный фарш и получили.

    Ну поиск не знает упоминаний об Илье Абрамове, кроме как засвет такого тёзки в тренировках по алгоритмам (от Яндекса) сего года, да и аватарка докладчика намекает на что-то студенческое, поэтому если первый блин - простительно, но в будущем конечно Илье стоит избегать сленга, а стараться описать архитектуру общепринятыми словами. Вот тогда и должно прийти понимание, что речь не о велосипеде, а о каких-то стандартных для индустрии вещах.


    1. lastrix Автор
      17.05.2026 10:15

      Все были студентами. И все учились. Как уже писал в конце - работу они проделали большую и тяжелую. Не всякий "выпускник" такое же проделает.


  1. AndrewTishkin
    17.05.2026 10:15

    По поводу эффективности.

    Если "кладуны" - это старая история, то кажется это сходится с моим свежием ощущением рукожопного рукожопства создателей клиента Яндекс.Диска для Windows. Типа - сами не поняли, как работать с тем, что наворотили.

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

    А дальше начался треш. Лютый и длительный жор процессорного времени, хотя подсчёт хеша альтернативными утилитами проходил за десятки секунд. Когда наконец клиент проперделся - у папок вместо зелёных галочек появились и синие знаки вопроса (типа загрузка не завершена) и даже красные крестики (ошибка). Перезапуск и синхронизации, и клиента ничего не даёт. При этом проверка показала, что всё благополучно загружено и чудит именно официальная чудо-программулина. Помог радикальный способ - удаление скрытой системной папки .sync с кэшированными данными о файлах. Видимо кэш перестроился, хоть это опять и заняло ужас сколько времени, и на какое-то время помогло. А потом снова неконсистентность.

    В итоге терпение иссякло, в нейросетку было засунуто официальное API Диска и неофициальное SDK для Python, в итоге родилась пара скриптов - на аплоад и на сверку.

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

    Яндекс какое-то время назад выпустил четвёртую версию, которая вообще ещё не умеет синхронизировать локальную папку Диска с удалённой, только скачивать файлы в реалтайме по запросу, при их открывании. То ли это из-за недавно озвученной тарифной политики (вы нам нагрузку создаёте - мы прогу оставим только для пользователей с подпиской 36), то ли просто-напросто из-за того, что третья версия клиента рукожопая, а когда начали четвёртую мутить, то даже затруднились всё это рукожопство переписать.


    1. AndrewTishkin
      17.05.2026 10:15

      У нас вообще с обработкой множества файлов умеют в крупных российских компаниях работать?) А то второй случай под нервный смех заставил сомневаться в таких навыках...

      Второй шок я испытал ночью, когда после посещения картинной галереи захотел залить в альбом VK восемь сотен фоток. Ладно, тут речь про Мэйл.ру, у них кармически свойство превращать даже золото в отходы жизнедеятельности.

      Раньше сотнями через мобильное приложение загружал, в несколько заходов (выбиралось по 100 файлов за раз), сейчас лимита не было, но файлы при загрузке перемешались. Сначала загрузилось N с конца, потом пошло всё остальное с начала.

      Хорошо, добрался до компа, стал грузить в браузере. Та же история! Чуваки, вы жжёте напалмом что ли? У вас что за сортировка используется, которая стандартные имена вида IMG202605... внезапно миксует по какому-то разделителю или что за чанки-приколы такие? Ещё и в процессе отобразилось меньше файлов, чем я перетащил в аплоад (без каких-либо ошибок).

      Чем дело кончилось - да тем же самым, утопающий спасает сам себя. Очередной скрипт, который через неофициальное апи (ибо для использования официального надо пописать в баночку, верифицироваться через Госуслуги, а потом ещё переварить разношёрстную противоречивую документацию) грузит последовательно по одному файлу, благодаря чему порядок следования фотографий сохраняется.

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

      PS: вот буквально свежак, как мэйлсрушечка занимается разработкой

      заявление старшего вице-президента VK по медиастратегии и развитию сервисов Степана Ковальчука. «Я поставил команде [VK Видео] такой KPI: сделать принципиально новые оригинальные фичи, которые потом скопирует YouTube»

      Команда сосать - из пальца. Таких товарищей надо привязывать к стулу и принудительно заставлять читать классику - а-ля "Психбольница в руках пациентов" и т.д. Потому что сперва надо думать о пользователе, а потом о том, как на нём зарабатывать.


  1. SeTeryoshkin
    17.05.2026 10:15

    Загрузка файла параллельными блоками глобально не решит проблему. Да, с помощью такой параллельной загрузки суммарную скорость загрузки пользователя будет “размазана” по нескольким узлам. Однако с точки зрения системы проблема хоть и сгладится, но никуда не уйдёт: всё ещё будет большое количество загрузок, у которых скорость различается, просто вместо 1 загрузки большого файла будет N загрузок “файлов” (блоков) поменьше (с пропорционально меньшей скоростью).


    1. lastrix Автор
      17.05.2026 10:15

      Решит. Ситуация следующая. У вас есть N клиентов с случайным размером файла и скоростью подключения. Каждый клиент загрузит Ki количество блоков.

      Если все они будут делать это одним потоком в одну сессию http-подключения, то в зависимости от самых разных ситуаций получаем следующие эффекты:

      1. Скорость загрузки может плавать (в один момент она 1 гбит/с, а через минуту упала до 1 мбит/с). Для интернета это норма. Сам клиент может начать использовать свой канал для других целей и т.д.;

      2. В случае потери подключения загрузку придется начинать заново. Если на каждом ki блоке вероятность обрыва соединения Pi, и даже если она очень мала - на больших файлах получаем гарантированный разрыв соединения. Какова вероятность успешной загрузки, если требуется залить файл размером 1 ТБ, а вероятность обрыва соединения на каждый 1 мбайт равна 10^-4? Не говоря о том, что делать размер тела http терабайтом ни один админ в здравом уме не станет;

      3. В частности это означет, что даже если мы изначально правильно разложили клиентов по кладунам, в ходе загрузки клиенты будут периодически перегружать их или недогружать;

      4. Так как некий балансерун решает за кладуна, сможет он или нет обеспечить пользователю загрузить файл, то всегда будем получать расхождение и проблемы, вообще в коллективах людей такое называется микромеджментом. Оно никогда до добра не доводило;

      5. Разумеется, можно доработать загрузку так, что бы файл загружался с места последней передачи, но в этом случае могут быть проблемы с целостностью данных, нужно будет где-то хранить выровненное количество записанных байт и отдавать его пользователю, сделать так, что бы пользователь мог продолжить закачку файла - реально, но пункты выше это все равно не отменит. Так же есть большие сомнения, что периодически не будут всплывать артефакты и порча данных при передаче.

      Теперь берем вариант при отправке каждого блока по отдельности (отдельным http-подключением):

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

      2. Нет ограничений на размер отправляемого файла, кроме стоимости подписки клиента. Ему нужно залить файл на 5 ТБ? Если заплатил за эту услугу, почему не предоставлять?

      3. Если правильно подобрать размер блока, то скорость подключения клиента перестает играть роль, за счет round-robin все будут сглажены и хвосты нормального распределения нагрузки на кладуны будут меньше, почти в шпильку превратится колокол;

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

      5. Кладуны можно в ходе загрузки файла спокойно обновлять, перезагружать или выполнять другие задачи по техническому обеспечению, такой подход не ставит инженеров в неудобное положение и не заставляет их откладывать задачи на потом (или вы хотите потом в тех поддержке гневные тикеты о том, что некий пользователь 10 часов файл грузил, а вы ему весь прогресс отменили своими "обновлениями"?).

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

      И размер блока гарантированно сказывается на колоколе нормального распределения нагрузки на кладуны.

      Доклад Ильи был именно о том, что бы нагрузка была равномерной, и моя статья была про это же.