
Представим некого разработчика, который по выходным делает собственный пет-проект. Сначала всё живёт на одной виртуальной машине. Проект небольшой, поэтому вся инфраструктура находится там же. API, база данных, обработка запросов и фоновые задачи. На этом этапе всё просто, а нагрузки почти нет.
После завершения разработки MVP версии, этот разработчик решил показать проект своим друзьям и знакомым.
Проект многим понравился, да настолько, что пользователи начали неконтролируемо расти, только за счёт рекомендаций других пользователей.
Инфраструктура та же. Одни пользователи загружают файлы, другие активно работают с сайтом, третьи ждут ответа, чтобы получить результат.
Сервер начинает отвечать медленнее. В логах появляются тайм-ауты, очередь растёт и сервер просто захлёбывается от наплыва задач, которые не может обработать.
Первая мысль, это улучшить код. В основном помогает, но не спасает от слабого сервера, на котором запущен проект.
Логично подумать: «Просто арендую сервер помощнее».
Опять же, это может помочь, но не всегда. Запросы распределяются честно, а работа нет. Один запрос отвечает за десятки миллисекунд, другой несколько минут.
В этой статье разберём, какие подходы используют крупные компании, и как я решил эту задачу в своём проекте.
❯ Когда одного сервера становится мало

Нагрузка бывает разной
Представим сайт с публикациями, комментариями и загрузкой файлов. Запрос на открытие страницы обычно короткий. Backend сходил в кэш или базу данных и вернул ответ.
А генерация отчёта, изменение размера изображения или конвертация большого файла могут занимать секунды и минуты.
Есть и третий класс работы, операции ввода-вывода и вызовы внешних API.
Они могут упираться не в процессор, а в скорость сети, диск или чужие лимиты.
Пока запросов мало, один процесс может принять обращение, сходить в базу, прочитать файл и вернуть результат.
С ростом потока возникают очереди ожидания. Часть из них видна разработчику. Например очередь фоновых заданий. Другая часть скрыта внутри операционной системы, пула соединений к базе, диска или внешнего API.
Поэтому фраза «сервер тормозит» бесполезна без уточнения.
Сначала нужно найти ресурс, который не успевает обслуживать работу:
CPU или память;
база данных и её пул соединений;
диск или сетевой канал;
внешний сервис с медленными ответами или лимитами.
Масштабировать систему можно по-разному

Есть 2 подхода по увеличению производительности:
1. |
Горизонтальное масштабирование — это добавить ещё одну машину или процесс той же роли. После этого появляются новые вопросы: кто выберет сервер, где хранить состояние, как синхронизировать данные и что произойдёт при падении одного узла. |
2. |
Вертикальное масштабирование — это взять более мощную машину. Добавить CPU, память, быстрый диск или GPU. Приложение почти не меняется, поэтому для небольшого проекта это хороший первый шаг. Ограничения тоже понятны: у машины есть предел, а при её отказе приложение остановится целиком. |
Оба подхода бесполезны против общего узкого ресурса. Если все серверы приложения обращаются к одной перегруженной базе, новая машина только увеличит давление на неё.
❯ Что насчёт асинхронности?
Асинхронность часто называют лекарством от медленного сервера, что в большинстве случаев действительно так и есть. Но точнее будет сказать, что это способ не тратить поток исполнения на бесполезное ожидание.
Примеры выше, я приводил на основе того что код проекта является синхронным.
В синхронной модели обработчик ждёт завершения операции.
Если он читает медленный диск или ждёт внешний HTTP-ответ, конкретный поток занят ожиданием. В асинхронной модели приложение может отдать управление event loop циклу событий, который переключается между операциями, пока одна из них ждёт сеть или диск, и принять другой запрос.
Например, API принимает запрос на построение отчёта, сохраняет описание работы и сразу возвращает идентификатор. Отдельный процесс забирает фоновые задачи из очереди.
Пользователь не держит HTTP-соединение открытым несколько минут, а API продолжает отвечать на короткие запросы.
Асинхронность не ускоряет вычисление само по себе. Если внутри event loop запустить длительный расчёт на CPU, этот поток всё равно будет занят вычислениями. То же касается и GPU. Асинхронная оболочка не создаёт второй GPU и не увеличивает объём его памяти. Для такой работы нужен отдельный процесс или worker, а число одновременно запущенных задач приходится ограничивать.

❯ YouTube: как Google экономит магистральные каналы
Большая часть нагрузки видеосервиса — это не вычисления, а передача файлов. Если каждый просмотр обслуживать из одного центрального дата-центра, популярный ролик придётся снова и снова отправлять по магистральной сети.
Когда вы открываете видео на YouTube, файл не обязательно летит через полмира из центрального дата-центра Google. Значительная часть контента хранится на серверах, которые физически находятся внутри сети вашего провайдера или рядом с ней. Это и есть Google Global Cache (GGC).
Google Global Cache
Суть этой технологии простая: если сервер с видео находится внутри сети провайдера, то трафик остаётся локальным и не нагружает внешние магистральные каналы. Так же когда сервер находится за границей, провайдер вынужден тянуть тот же объём данных через платные транзитные линии. GGC помогает решить эту проблему.

Два режима кэширования:
Реактивный |
Превентивный |
Контент попадает в кеш после нескольких запросов от пользователей. Например, после 5-10 просмотров одного ролика в сети провайдера он закрепляется на локальном сервере, и следующие зрители получают его оттуда. |
Самые популярные видео заранее загружаются на кеш-серверы, независимо от того, сколько раз их уже смотрели в конкретной сети. Это позволяет отдавать видео без задержек. |
Что именно он кэширует?
GGC обслуживает не только YouTube, но и другие статические сервисы.
Google карты, Chrome, картинки из поиска и т.д.
Для YouTube в первую очередь кэшируются сами видеопотоки и превью.
Типичная оценка Google: 70–90% кэшируемого трафика можно отдать с узла, hit rate зависит от того, что смотрят абоненты.
Если GGC отключить, запросы на видео пойдут напрямую к удалённым серверам Google. Это увеличит задержки, ухудшит качество воспроизведения и повысит нагрузку на магистральные каналы.
Внутри YouTube нет «одной программы». Это набор микросервисов: рекомендации, поиск, метаданные роликов, комментарии, реклама и т.д. Когда вы открываете главную страницу, один сервис делает десятки внутренних RPC-вызовов к другим, чтобы собрать всё воедино. Каждый из этих сервисов запущен в сотнях реплик, поэтому перед каждым вызовом нужно решить, на какую реплику отправить запрос.
Для этого компания Google разработала Prequal (Probing to Reduce Queuing and Latency). Её цель не «уравнять загрузку CPU», а минимизировать реальную задержку запросов и избежать очередей, особенно в хвосте распределения.
Prequal

Вместо того, чтобы балансировать по средней загрузке процессора, Prequal выбирает реплику по двум сигналам.
RIF (Requests In Flight) |
Оценка задержки |
сколько запросов прямо сейчас обрабатывается на реплике. Это мгновенный показатель, который хорошо предсказывает будущую нагрузку и потребление памяти |
медианная латентность недавно завершенных запросов при похожем уровне RIF. |
Реплика выбирается по определенным правилам.
Сначала, клиент оценивает распределение RIF по всем репликам и помечает зонды:
«горячие» (hot) |
RIF выше выбранного квантиля (например, выше 80–90% реплик) |
«холодные» (cold) |
остальные. |
Если в пуле есть хотя бы один «холодный» зонд, выбирается холодный с наименьшей задержкой. Если же все зонды «горячие», то выбирается тот зонд, где RIF имеет меньшее значение.
Эти правила отражают приоритеты. Главное, не допустить, чтобы реплика ушла в перегруз по памяти и числу активных запросов. А второе, это минимизировать задержку.
Зонды Prequal отправляются асинхронно. Текущий запрос использует данные от зондов, запущенных предыдущими запросами, чтобы не добавлять задержку на критический путь. Каждый зонд переиспользуется несколько раз, но не бесконечно. Старые записи удаляются по возрасту, по лимиту переиспользования и через периодическую чистку «худших» зондов, чтобы пул не смещался в сторону перегруженных реплик.
Почему отказались от WRR

Почему prequal лучше старой реализации балансировки по CPU (WRR) ?
До внедрения Prequal, YouTube (и многие другие сервисы Google) долгое время использовался балансировку по CPU на основе
Weighted Round Robin (WRR).
Её базовый принцип: каждая реплика получает «вес» пропорционально своей средней утилизации процессора, и запросы распределяются по репликам циклически с учётом этих весов, чтобы в среднем нагрузка по CPU была примерно одинаковой.
WRR хорошо выравнивал среднюю загрузку CPU, но плохо реагировал на короткие всплески и «несчастливые» реплики. Это приводило к росту очередей, увеличению хвостовых задержек и периодическим таймаутам на чувствительных к латентности сервисах.
Даже при «нормальной» средней утилизации CPU отдельные реплики регулярно уходили в перегруз из-за конкуренции с другими процессами на той же машине. Балансировка по CPU не успевала это фиксировать и продолжала направлять запросы на уже перегруженные узлы.
Prequal сместил фокус с «средней утилизации CPU» на реальную задержку и число активных запросов. Это позволило быстрее обходить проблемные реплики, сократить хвостовые задержки на 40–50% и практически устранить ошибки из-за дисбаланса нагрузки, что и стало причиной перехода.

❯ Яндекс Диск: как избежать двойной нагрузки на сеть
Обычный балансировщик принимает запрос пользователя и пересылает его на один из серверов. Для небольшого ответа это нормально. С большими файлами возникает лишняя работа. Балансировщик сначала принимает весь файл, а затем передаёт те же данные серверу хранения. Сеть используется дважды.
При загрузке файлов на Яндекс Диск трафик идёт через промежуточное звено, которое не хранит данные, но вынуждено пропускать через себя гигабайты. Это создаёт узкое место. Балансировщик тратит ресурсы на передачу чужих данных, а сеть нагружается вдвое сильнее, чем нужно.
Разделение Control- и Data-запросов
В Яндекс Диске этот путь разделили. Сначала клиент отправляет маленький управляющий запрос и спрашивает, куда загружать файл. В ответ получает ссылку на конкретный сервер и передаёт данные сразу туда, минуя промежуточный балансировщик.
Компонент, который выбирает сервер, в Яндексе назвали Балансерун. Сервер-приёмник получил имя Кладун. Балансерун знает список доступных Кладунов, следит за их состоянием и возвращает пользователю ссылку на один из них.
Основная суть:
Control-запросы |
Data-запросы |
лёгкие HTTP-запросы для получения адреса загрузки. Они проходят через стандартный балансировщик, так как почти не создают сетевой нагрузки. |
сами файлы, которые идут напрямую от клиента к выбранному Кладуну, минуя балансировщик. |
Это снимает двойную нагрузку на сеть. Данные больше не проходят через промежуточное звено.
Почему не подходят Random и Round Robin
Оба алгоритма распределяют запросы, но не учитывают реальную скорость загрузки:
Random |
Round Robin |
случайный выбор узла. При высокой дисперсии скоростей пользователей (от 10 Мбит/с до 1 Гбит/с) некоторые узлы получают несколько «тяжёлых» клиентов подряд и перегружаются, пока другие простаивают. |
чередование узлов. Даёт равное число запросов, но не равный трафик. Один узел может получить трёх быстрых клиентов, другой, трёх медленных. |
В обоих случаях нагрузка на сеть распределяется неравномерно, возникают «хвосты» перегруженных узлов.
Решение проблемы: HOBA
Для решения этой проблемы каждый Балансерун получает собственную группу серверов. Выдав ссылку пользователю, он сразу увеличивает расчётную нагрузку выбранного Кладуна на ожидаемую скорость загрузки. Ждать следующего отчёта от сервера не нужно. Периодические общие показатели лишь исправляют накопившуюся ошибку прогноза. В Яндексе этот алгоритм назвали HOBA (Hierarchical Optimized Balancing Algorithm).
Как это работает:
Локальные пулы. Каждый Балансерун получает непересекающийся пул Кладунов. При назначении загрузки он мгновенно обновляет виртуальную нагрузку выбранного узла на ожидаемую скорость клиента, не дожидаясь отчётов. Это даёт актуальную «картину мира» в ту же миллисекунду.
Учёт скорости. Балансировщик хранит данные о скорости пользователя (текущей, средней по истории или медианной по системе) и оперирует прогнозируемой нагрузкой на канал, а не числом запросов.
Глобальная статистика. Периодические отчёты от Кладунов (раз в K секунд) теперь используются не для мгновенных решений, а для коррекции накопленной погрешности локальных расчётов. Это позволило увеличить интервал K и разгрузить шину управления.
Rendezvous Hashing: стабильное распределение пулов
Остаётся понять, какой Балансерун отвечает за какие серверы. Для каждого Кладуна все балансировщики независимо вычисляют одного и того же владельца. Если Балансерун исчезает, переназначаются только его серверы. Этот метод называется Rendezvous Hashing.
Принцип работы:
1. Для каждой пары «Кладун + Балансерун» вычисляется весовая функция.2. Кладун назначается в пул того балансировщика, у которого вес максимален.3. Если один Балансерун падает, пересчитываются веса только для его Кладунов — остальные пары сохраняют свои веса и остаются в прежних пулах.
Это минимизирует миграции и сохраняет локальную статистику на живых балансировщиках.
Ограничение размера пулов
Чтобы случайность не отдала одному Балансеруну слишком много Кладунов, размер каждой группы дополнительно ограничивается:
Вычисляется «идеальный» размер пула:
uploaders_num / balancers_num.При назначении Кладуна проверяется, не достиг ли пул балансировщика лимита. Если да, выбирается следующий по весу балансировщик.
Дополнительно можно ограничивать пул снизу, чтобы разница между наиболее и наименее заполненными пулами не превышала единицы.
После внедрения HOBA распределение загрузки сети стало более равномерным. Стандартное отклонение снизилось на 22%, количество перегруженных «хвостов» уменьшилось, а утилизация сети выровнялась. Алгоритм особенно эффективен при высокой дисперсии скоростей пользователей.

❯ Timeweb Cloud: не запустить всё сразу
Иногда физический сервер нужно освободить для ремонта или замены. Запущенные на нём виртуальные машины переносят на другие серверы. Если начать десятки таких миграций одновременно, они займут весь сетевой канал и помешают не только друг другу, но и работающим клиентским машинам.
Внутренний сервис Timeweb Cloud под названием vapi-server сначала измеряет доступную скорость между исходным и целевым сервером. Затем он решает, сколько виртуальных машин можно переносить одновременно, и делит между ними канал. Новая миграция начинается, когда освобождается место.
Принцип |
1. Если запустить все миграции сразу, они «забьют» сеть, и каждая будет идти медленно, мешая остальным. |
2. Если ограничить число одновременных переносов и разделить канал, миграции пройдут быстрее и не повлияют на работу клиентских машин. |
Решение: агенты на гипервизорах
При создании виртуальных машин используется другой подход. Физический сервер, на котором запускаются виртуальные машины, называется гипервизором. Раньше команды для всех гипервизоров проходили через центральный API и образовывали длинную общую цепочку.
Теперь на каждом гипервизоре работает небольшой служебный процесс или агент. Центральный API сообщает, что нужно сделать, а агент выполняет команду на своей машине и возвращает результат. Несвязанные операции могут идти параллельно.
Как это работает:
Центральный API |
Агент на гипервизоре |
принимает запросы от пользователей и распределяет задачи между гипервизорами. |
получает команду, выполняет её локально (создание ВМ, старт, остановка) и возвращает результат. |
Это убирает узкое место в виде единой очереди и позволяет выполнять независимые операции параллельно. Похожую логику можно использовать не только внутри облачной платформы, но и при построении собственной инфраструктуры.
Как взять этот принцип и применить у себя
Похожий подход можно использовать и при настройке пользовательской инфраструктуры.
Например, проект можно разместить на нескольких облачных серверах Timeweb Cloud, а входящий трафик распределить между ними с помощью балансировщика нагрузки.
Вместо одной машины, которая принимает все запросы, нагрузка разделяется между несколькими серверами. Если проект растёт, к схеме можно добавить новый сервер и направить на него часть трафика. Балансировщик также проверяет доступность подключённых машин и исключает из распределения те, которые перестали отвечать.
Такой сценарий позволяет постепенно расширять инфраструктуру. Начать с двух серверов, а затем добавлять новые ресурсы по мере роста нагрузки. Серверы создаются через панель Timeweb Cloud.
Здесь используется тот же общий принцип, что и во внутренней инфраструктуре Timeweb Cloud. Не отправлять все операции или запросы одному исполнителю, а распределять нагрузку между несколькими доступными ресурсами.
Что дала новая архитектура
В июльском дайджесте Timeweb Cloud указывает результат этой перестройки. Что от нажатия кнопки до готовой виртуальной машины проходит около 30 секунд.
Такой подход работает не только внутри платформы. Когда вы разворачиваете проект на облачных серверах Timeweb Cloud, аналогичные принципы применяются и к вашей инфраструктуре. балансировщик нагрузки распределяет трафик между несколькими VPS, а новые виртуальные машины создаются за секунды через API или панель управления.
В одном случае компания распределяет команды между локальными исполнителями. В другом ограничивает число одновременных переносов реальной скоростью сети. Это внутренняя инфраструктура Timeweb Cloud, а не описание балансировщика, который предлагается пользователям.
Для гипотетического разработчика из начала статьи как раз этот слой и нужен. Две-три машины, балансировщик между ними, без собственного мини-Диска. Продукт здесь инструмент в истории, не заголовок.
❯ .sound: пример из жизни

|
Сначала такая обработка выполнялась рядом с основным приложением. В итоге веб-запросы и распознавание музыки конкурировали за процессор, память и диск. Масштабировать весь Backend ради одной тяжёлой операции было бессмысленно.

Я вынес вычисления в отдельный репозиторий.
При его запуске создаётся отдельный worker. Он сам подключается к Backend и просит следующую задачу.
Такой порядок называют pull-моделью. Не сервер ищет свободного исполнителя, а исполнитель приходит за работой.
Благодаря этому, на домашнем компьютере или сервере с видеокартой не нужно открывать входящий порт. Да и нагрузка значительно снижается, ведь распределена между разными серверами.
Разные задачи, разные способы обработки
В «.sound» разные типы задач не складываются в одну общую очередь.
Для каждой группы используется отдельный способ хранения и обработки. Обычные фоновые операции выполняются рядом с сайтом, а ресурсоёмкие вычисления и распознавание аудио обрабатываются отдельно.
Это позволяет задавать для них разные ограничения, правила запуска и повторной обработки.
На архитектурной схеме единая очередь выглядела бы аккуратнее. Но в коде разделение оказалось понятнее. Короткие фоновые задачи, удалённые вычисления и длительная обработка аудио по-разному восстанавливаются после сбоев.
Как исполнители делят задачи между собой
При подключении исполнитель указывает профиль:
cpu_light (обычный процессор) или gpu_full (видеокарта) и какие типы задач умеет брать. Распознавание песен (ASR) обычно ждёт машину с видеокартой.
Сервер (Backend) выбирает работу по типу, приоритету, профилю и времени, когда можно пробовать снова. Если несколько исполнителей тянут одну задачу сразу, PG на короткое время блокирует эту запись. Остальные не ждут, а берут следующую.
Забрав задачу, исполнитель держит её только ограниченное время.
Раз в минуту отдельный фоновый процесс находит просроченные задачи и возвращает их в очередь с паузой перед новой попыткой.
Это не гарантия, что задача физически запустится ровно один раз. После сбоя её могут выполнить снова. Поэтому повторная постановка не должна создавать копии, а повторное сохранение результата, портить данные.
Как Backend понимает, что исполнитель жив

Каждые 15 секунд исполнитель шлёт запрос «я жив» на сервер, и обновляет время последней связи.
И в том же ответе может сказать: не бери новые задачи или отмени текущую задачу.
Почему аудио и остальные задачи выполняются по-разному?

Сложные GPU задачи работают строго по одой задаче. Пока текущая не закончена, следующую не берут.
Остальные вычисления можно делать параллельно. Есть общий потолок и отдельные лимиты по типам.
Что даёт отдельный исполнитель
Тяжёлые задачи больше не живут в том же процессе, который отвечает пользователям. База и выдача музыки отделены от вычислений.
Исполнителя можно перенести на другую машину. Можно добавить несколько исполнителей, чтобы разгрузить очередь. Можно сменить процессор на видеокарту или поставить машины с другим набором задач. Снаружи проект от этого не меняется.
Если исполнитель падает, задание не зависает в статусе «обрабатывается». Когда отведённое время кончается, фоновый процесс возвращает его в очередь. Перегруженную машину можно поставить на паузу: сервер говорит об этом в ответе на очередной пульс.
Про «в десять раз быстрее» писать не буду. Задачи стали обрабатываться быстрее, но траты на аренду тоже выросли. Воркеры живут на отдельных серверах.
❯ Общие принципы
Не чеклист на все случаи, а то, что выявил разбирая примеры выше.
1. |
Балансируй конкретный ресурс, а не «запросы вообще». |
2. |
Разделяй короткие HTTP-запросы и тяжёлую работу. |
3. |
Метрика важнее названия алгоритма. |
❯ Заключение
Балансировать нужно не абстрактные запросы, а ресурс, который заканчивается.
Масштабирование не начинается с покупки самого мощного сервера или подключения случайного балансировщика. Сначала нужно понять, что именно не справляется: CPU, GPU, сеть, диск, база данных или очередь задач.
У крупных сервисов разные решения, но принцип один. Тяжёлую работу отделяют от коротких запросов, данные стараются не гонять через лишние узлы, а нагрузку измеряют метрикой, которая связана с реальной проблемой.
Для небольшого проекта не нужно копировать инфраструктуру YouTube или Яндекс Диска. На первых этапах достаточно вынести долгие операции в отдельные воркеры, ограничить число параллельных задач и при необходимости добавить несколько серверов за балансировщиком. Инфраструктура должна расти вместе с продуктом, а не опережать его.
Ссылки
GitHub: https://github.com/network-user
Источники:
YouTube и Google
1. Load Is Not What You Should Balance: Introducing Prequal, NSDI '24.
2. Google Global Cache: overview.
3. Google Global Cache: Cache Fill.
4. Google Global Cache: Content Served.
Яндекс Диск
1. Как Яндекс Диск выдерживает сотни гигабит входящего трафика.
Timeweb Cloud
1. Июльский дайджест: сервер за 30 секунд, новые модели и токены на агента.
2. Transfer 2.0, или Как я перестал бояться и полюбил миграции облачных серверов.
Может быть интересно:

Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram‑канале ↩