АрхитектураНадёжность и производительностьИнженер по надёжности и производительности платформы

Балансировщик отправляет запросы по очереди, но длительность обработки у экземпляров заметно различается. К...

Балансировщик отправляет запросы по очереди, но длительность обработки у экземпляров заметно различается. Какой перекос нагрузки возникнет в следующей схеме?

backends = ["fast", "slow"]
index = 0

def choose_backend():
    global index
    backend = backends[index % len(backends)]
    index += 1
    return backend
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

Исторический контекст

Round-robin появился как простой способ распределять поток запросов между одинаковыми экземплярами без сложного сбора состояния. Он хорошо работает, когда экземпляры сопоставимы по производительности, а длительность запросов примерно одинакова.

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

Постановка проблемы

Пусть один экземпляр завершает запрос за 10 мс, а другой — за 1 с. Round-robin отправит каждому примерно половину новых запросов, но медленный экземпляр будет дольше удерживать каждый запрос и быстрее накопит активную работу.

В результате среднее число запросов может выглядеть равномерным, а распределение активных запросов, очередей и задержек — сильно неравномерным. Пользовательский p95 или p99 начнёт определяться медленным экземпляром, особенно если балансировщик продолжает направлять ему новые запросы без учёта его занятости.

Подробное решение

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

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

Взвешенный round-robin полезен, когда производительность экземпляров известна и относительно стабильна. Но фиксированный вес плохо реагирует на временную деградацию, GC-паузы, сетевые проблемы или изменение характера трафика.

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

Минимальная иллюстрация альтернативы:

backends = {"a": 2, "b": 0} def choose_backend(): return min(backends, key=backends.get) def start(name): backends[name] += 1 def finish(name): backends[name] -= 1

Здесь новые запросы направляются экземпляру с наименьшим числом активных операций. Такой подход требует корректного учёта начала и завершения запросов; при ошибках учёта балансировка сама может стать источником перекоса.

Ситуация из практики

Сервис работал на двух экземплярах: один использовал более производительный узел, второй периодически выполнял тяжёлые операции. При round-robin количество запросов на экземпляры было почти одинаковым, но p99 второго был значительно выше, а его очередь постоянно росла.

Рассматривались три варианта. Увеличение числа экземпляров уменьшало среднюю нагрузку, но не устраняло перекос между быстрыми и медленными обработчиками. Фиксированные веса были просты, однако требовали ручной настройки после каждого изменения профиля нагрузки. Балансировка по активным запросам лучше реагировала на текущую занятость, но потребовала корректного мониторинга завершения запросов.

Выбрали балансировку по активным запросам с ограничением максимального числа одновременных операций на экземпляр. Дополнительно отслеживали p95/p99 и длину локальных очередей. Это уменьшило накопление работы на медленном экземпляре, но не отменило необходимости отдельно искать причину его деградации: балансировщик распределяет нагрузку, а не устраняет узкое место.

Что кандидаты часто упускают

1. Всегда ли балансировка по наименьшему числу активных запросов лучше round-robin?

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

2. Почему одинаковое число запросов не гарантирует одинаковую загрузку CPU?

Потому что запросы могут выполнять разный объём вычислений, обращаться к разным зависимостям или проводить разное время в ожидании I/O. Число запросов — лишь косвенный показатель работы. Для диагностики нужно сопоставлять его с CPU, временем обработки, активностью зависимостей, очередями и распределением задержек.

3. Может ли сам алгоритм балансировки ухудшить хвостовую задержку?

Да. Если балансировщик использует устаревшее состояние или не учитывает стоимость запросов, он может направлять новые операции уже перегруженному экземпляру. Возникает положительная обратная связь: очередь увеличивает задержку, задержка дольше удерживает запросы активными, а высокий счётчик активных запросов может вызвать резкие переключения на другие экземпляры. Поэтому алгоритм оценивают по устойчивости распределения и пользовательским p95/p99, а не только по доле запросов на каждый экземпляр.