open atlas
↑ К треку
Основы System Design SD · 03 · 01

Балансировка нагрузки

Балансировщик распределяет запросы по пулу серверов. Выбор между L4 и L7 и между round-robin и алгоритмами с учётом нагрузки решает твой хвост задержки — а наивный health-check на failover может устроить давку выживших и второй сбой.

SD Middle ◷ 18 min
Уровень
ОсновыJuniorMiddleSenior
Уже знаешь этот юнит? Пройди быструю проверку за минуту →

Команда держала шесть одинаковых API-узлов за обычным round-robin балансировщиком и спала спокойно — пока один узел не ушёл в долгие паузы сборки мусора. Round-robin продолжал слать ему один запрос из шести, идеально равномерно, прямо в сервер, который отвечал теперь 4 секунды вместо 40 мс. Дашборды показывали пять здоровых узлов и один больной, но p99 всего сервиса шёл за больным узлом, потому что шестую часть всех пользователей балансировщик направлял на него — балансировщик, который считал запросы и ни разу не посмотрел, вернулись ли они. «Равномерное распределение» оказалось ровно неправильной целью: им нужно было распределение с учётом нагрузки, и нужно было, чтобы балансировщик замечал медленный узел раньше пользователей.

Что балансировщик на самом деле делает

Балансировщик нагрузки (load balancer) стоит перед пулом одинаковых серверов и решает, для каждого соединения или запроса, какой из них его обслужит. Он покупает три вещи сразу: горизонтальное масштабирование (пул растёт за одним адресом), отказоустойчивость (мёртвый сервер выводится из ротации, трафик течёт на остальные) и стабильную точку входа (клиенты обращаются к одному виртуальному адресу, а не к отдельным машинам). Это та парадная дверь, которая делает масштабирование stateless-уровня из прошлого юнита реально работающим — «добавлять одинаковые узлы» можно, только если что-то раскидывает по ним трафик.

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

L4 против L7: где балансировщик читает запрос

Уровень 4 (транспортный) балансирует по TCP/UDP-соединению: IP и порт источника/назначения. Он пересылает пакеты (или сшивает соединение), вообще не разбирая байты внутри. Это делает его крайне быстрым и не зависящим от протокола — он двигает что угодно поверх соединения и не имеет мнения о HTTP. Цена в том, что он не может принимать решения по содержимому: не маршрутизирует /api в один пул, а /images в другой, не читает cookie, не терминирует TLS, чтобы увидеть путь.

Уровень 7 (прикладной) терминирует соединение, читает HTTP-запрос и маршрутизирует по его содержимому: путь, заголовок Host, метод, cookie. Это открывает маршрутизацию по контенту, пулы на маршрут, переписывание заголовков, терминацию TLS и ретраи на уровне запроса. Цена — CPU и задержка: он делает реальный парсинг на каждом запросе и становится местом, где скапливаются прикладные заботы. (Терминация TLS здесь связана с уроком про TLS из трека networking; мы остаёмся на уровне композиции — важно где заканчивается рукопожатие, а не как оно устроено.)

Старшее правило большого пальца: L4, когда нужна сырая пропускная способность и независимость от протокола (базы данных, gRPC-стримы, всё, где не нужно заглядывать внутрь); L7, когда решение о маршрутизации зависит от содержимого запроса (веб-API, шлюзы микросервисов, всё, что зависит от пути или заголовка).

Почему это работает

Почему L4 стоит так дёшево, а L7 так дорого? L4-балансировщик часто может передать соединение и вообще выйти из пути данных (direct server return) или просто гонять пакеты — несколько микросекунд работы на соединение. L7-балансировщик обязан быть на пути весь запрос: он терминирует TCP и TLS, буферизует и парсит HTTP-сообщение, применяет правила маршрутизации, открывает отдельное соединение к апстриму и проксирует тело в обе стороны. Это два соединения, парсинг и копирование на каждый запрос вместо одной пересылки. Этот налог ты платишь за право принимать решения по содержимому — потому и заносишь L7-функции (аутентификацию, переписывание, агрегацию) внутрь только тогда, когда тебе реально нужно видеть запрос.

Алгоритмы — и почему они решают твой хвост

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

  • Round-robin — отдавать каждый новый запрос следующему серверу по кругу. Просто, равномерно по числу, слепо к нагрузке. Годится, когда каждый запрос стоит одинаково и серверы идентичны; опасно в момент, когда один запрос дорогой или один узел медленный (хук).
  • Взвешенный round-robin — давать более крупным серверам бóльшую долю. Справляется с разнородным парком, всё ещё слеп к текущим условиям.
  • Least-connections (наименьшее число соединений) — слать следующий запрос серверу с наименьшим числом активных соединений. Дешёвый прокси для «наименее занят» и куда лучше round-robin, когда длительности запросов разнятся: медленный узел накапливает открытые соединения и естественно перестаёт получать новые.
  • Least-response-time / EWMA — отслеживать недавнюю задержку каждого сервера (часто экспоненциально-взвешенное скользящее среднее) и предпочитать самый быстрый. Именно это спасло бы команду из хука: узел, начавший отвечать 4 с, получает меньше трафика, а не свою честную шестую часть. Это учёт нагрузки в самом честном смысле — реакция на то, как сервер ведёт себя прямо сейчас.
  • Консистентное хеширование (consistent hashing) — хешировать ключ (IP клиента, ID пользователя, ключ кэша) на кольцо серверов так, чтобы один ключ попадал на один сервер в большинстве случаев. Незаменимо, когда бэкенд держит состояние по ключу — кэш, сессию, шард — потому что минимизирует, сколько ключей переедет при входе или выходе сервера (переотображается только 1/N ключей, а не все).

Вместе эти алгоритмы образуют спектр от «считай и забудь» до «реагируй в реальном времени». Прежде чем выбрать алгоритм, спроси себя: все ли мои запросы стоят одинакового объёма работы? Если нет — round-robin тихо убивает твой хвост задержки, и least-connections или EWMA — это фикс.

Health-checks и sticky-сессии

Балансировщик хорош ровно настолько, насколько верна его картина того, кто здоров. Health-checks опрашивают каждый сервер (TCP-коннект или, лучше, HTTP /healthz, реально дёргающий приложение) и выкидывают падающие узлы из ротации. Тонкость — активные против пассивных: активные пробы спрашивают «ты жив?» по таймеру; пассивные смотрят реальный трафик и выкидывают узел, начавший ошибаться или таймаутить. Лучшие системы делают и то, и другое — и, что важно, health-check должен проверять цепочку зависимостей, нужную серверу (достаёт ли он БД?), а не просто «процесс поднят», иначе ты радостно направишь трафик на узел, мгновенно отдающий 500-е.

Sticky-сессии прикрепляют клиента к тому же бэкенду (через cookie или хеш IP-источника), чтобы состояние пользователя, лежащее на этом узле, оставалось доступным. Прошлый юнит уже отметил, почему это костыль: он заново вводит единую точку отказа на пользователя и ломает чистую ребалансировку. Чистая альтернатива — stateless-уровень: вынести состояние сессии в общее хранилище или подписанный токен, чтобы любой сервер обслуживал любой запрос и stickiness никогда не понадобилась. Используй консистентное хеширование для локальности кэша (оптимизация производительности, которую можно безопасно потерять), а не для корректности (которую stickiness подразумевает и которая укусит, когда узел умрёт).

Частая ошибка

Дорогой режим отказа — давка (thundering herd) на failover. Один узел из десяти умирает; балансировщик корректно перераспределяет его трафик — но теперь девять узлов мгновенно вбирают по ~11% лишней нагрузки каждый, и если они уже были у колена кривой очередей, этот толчок может опрокинуть следующий. Он падает, его нагрузка перераспределяется на восемь — и получается каскадный коллапс, где восстановление и вызывает сбой. С ретраями хуже: каждый запрос, бывший на мёртвом узле, ретраит разом (синхронизированный всплеск), и ещё хуже с холодным кэшом у выживших. Защиты — те, что AWS документирует для ретраев: запас ёмкости, чтобы выжившие вобрали долю соседа, джиттер вместо синхронных ретраев, и circuit breaker / сброс нагрузки (load shedding), чтобы перегруженный выживший сбрасывал новую работу, а не умирал. Планируй ёмкость на N−1 (или N−2), иначе твой failover и есть твой сбой.

Расставь шаги по порядку

Упорядочи шаги диагностики и устранения высокого p99 в сервисе за балансировщиком, когда большинство узлов выглядят здоровыми:

  1. 1 Проверить метрики задержки по каждому узлу — найти, у какого p99 резко отличается от остальных, раскрывая медленный аутлайер, которому round-robin продолжает слать трафик.
  2. 2 Подтвердить алгоритм балансировки — убедиться, что используется round-robin (счёт запросов, без учёта нагрузки), а не алгоритм с учётом нагрузки, такой как least-connections или EWMA.
  3. 3 Исследовать медленный узел на первопричину — GC-паузы, насыщение CPU или downstream-зависимость, удерживающая соединения открытыми.
  4. 4 Переключить алгоритм на least-connections или least-response-time/EWMA, чтобы балансировщик автоматически обходил медленный узел вместо равномерной его загрузки.
  5. 5 Добавить пассивный health-check или latency-aware пробу, чтобы медленный, но принимающий соединения узел помечался и исключался или депрайоритизировался до того, как пользователи это заметят.
Викторина

Один узел в round-robin пуле из шести начинает отвечать 4 с на запрос (было 40 мс), но всё ещё принимает соединения. Что делает балансировщик и какой алгоритм лучше?

Викторина

Нужно маршрутизировать по URL-пути (/api в один пул, статику в другой) и читать cookie маршрутизации. На каком уровне обязан работать балансировщик и какова цена?

Закончи аналогию

Чтобы слать данный ключ кэша или ключ шарда на тот же бэкенд в большинстве случаев — и переотображать лишь ~1/N ключей при входе или выходе сервера — балансировщик использует _______ хеширование вместо простого остатка от деления, который перетасовывает каждый ключ при смене состава.

Вспомните перед уходом
  1. 01
    Сравни L4 и L7 балансировку — что каждая может и не может, и когда какую брать.
  2. 02
    Назови алгоритмы балансировки от слепых к учитывающим нагрузку и какой бережёт хвост задержки.
  3. 03
    Что такое thundering herd на failover и как защищаться?
Итог

Балансировщик — это парадная дверь, делающая горизонтальное масштабирование реальным: он раскидывает запросы по пулу, выкидывает мёртвые узлы и даёт клиентам один стабильный адрес. Доминируют два решения. Уровень: L4 маршрутизирует по IP/порту — быстро, слеп к протоколу, не видит внутри; L7 терминирует и парсит запрос, чтобы маршрутизировать по пути/заголовку/cookie и терминировать TLS, ценой CPU. Алгоритм: round-robin и взвешенный RR слепы к нагрузке, тогда как least-connections и least-response-time/EWMA учитывают нагрузку и берегут хвост задержки, обходя медленный узел вместо подачи ему равной доли; консистентное хеширование шлёт ключ на тот же бэкенд и переотображает лишь ~1/N ключей при смене состава — потому кэш- и шард-ключевая маршрутизация на нём и держится. Health-checks должны проверять реальную цепочку зависимостей, а не просто «процесс поднят»; sticky-сессии — костыль, заново создающий SPOF на пользователя — вынеси состояние и держи уровень stateless. И всегда планируй давку на failover: размер на N−1, джиттер ретраев и сброс нагрузки, иначе восстановление одного мёртвого узла станет каскадным сбоем. Теперь, когда встретишь сервис с необъяснимо высоким p99 при «сбалансированном» трафике, первый вопрос — какой алгоритм использует балансировщик и видит ли он вообще, насколько медленен один из его узлов.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.