15 KiB
Балансировка трафика и шардирование
На семинаре разбираем, как распределять запросы между серверами и данные между узлами хранения, чтобы масштабировать сервисы при росте нагрузки и объёма данных. Сравнить алгоритмы балансировки можно в лабораторной работе, а запустить небольшие сервисы — в примерах Nginx и key-value.
Балансировка трафика
При росте "читающей" нагрузки на сервис одно из простых решений — запустить несколько копий сервиса и научиться распределять нагрузку между ними. Балансировщик принимает запрос или пакет и выбирает сервер для его обработки. На выбор могут влиять адрес клиента, загрузка и производительность серверов, а также состояние уже установленных соединений.
При переходе к модели с несколькими копиями сервиса необходимо продумать, нужно ли обмениваться информацией между данными копиями. Как правило, проще всего балансировать нагрузку в случае реплицированного и/или stateless-сервиса.
Одним из вопросов при выборе балансировщика нагрузки является вопрос: на каком "уровне" вы хотите балансировать нагрузку? L3/L4-балансировщики работают на третьем или четвёртом уровне модели OSI и распределяют входящие TCP- и UDP-пакеты. На данном уровне мы не знаем о клиенте ничего, кроме его IP-адреса и порта, и не смотрим на содержимое запросов, поэтому балансировка менее точная, но более легковесная. L7-балансировщики оперируют на уровне протоколов приложений, например HTTP. Данные балансировщики более гибкие и имеют больше информации о пользователе. Также достаточно много полезной информации можно получить из заголовков HTTP-запроса (например, идентификатор пользователя). Это позволяет сделать балансировку более точной, но балансировщик начинает выполнять больше задач, из-за чего требуется больше ресурсов.
Запросы и соединения
Stateless-приложение не требует хранить состояние клиентской сессии в конкретном экземпляре между запросами. Оно может работать поверх TCP: состояние транспортного соединения и состояние приложения — разные вещи. Например, HTTP-прокси может распределять независимые запросы между серверами, поддерживая собственные TCP-соединения с ними.
UDP также не означает отсутствие состояния: HTTP/3 работает поверх QUIC, который поддерживает соединения, используя UDP. См. HTTP/3 и QUIC.
При балансировке TCP-пакетов одного соединения они должны попадать на сервер, который знает состояние этого соединения. Для этого можно хранить таблицу соединений или детерминированно выбирать сервер по адресам и портам. Изменение набора серверов требует осторожности: оно может изменить выбор для уже открытых соединений. Короткий таймаут таблицы сам по себе эту задачу не решает.
DNS помогает выбрать адрес перед установлением соединения. Новый DNS-ответ не меняет удалённый адрес существующего TCP-соединения. При anycast один адрес объявляют из нескольких мест, а путь к нему выбирает сеть; изменение этого пути — отдельная причина попадания пакетов на другой балансировщик.
Ответ может возвращаться через прокси или идти напрямую от бэкенда клиенту — схема Direct Server Return/Response (DSR). Подход зависит от архитектуры. Например, в нашем демо Nginx ответы проходят через HTTP-прокси. Подробнее: Google SRE: балансировка на входе.
Алгоритмы выбора сервера
| Алгоритм | Как выбирается сервер |
|---|---|
| Round robin | Серверы перебираются по кругу. |
| Random | Для каждого выбора берётся случайный сервер. Даже равномерный случайный выбор даёт колебания нагрузки. |
| Least connections | Выбирается сервер с наименьшим числом активных соединений. LeastLoaded в лабораторной работе использует другую метрику — число незавершённых запросов. |
| Power of Two Choices | Выбираются два случайных сервера, затем менее загруженный из них по выбранной метрике. Обобщение на k кандидатов называется Power of k Choices. |
| IP hashing | Сервер выбирается детерминированно по адресу клиента, чтобы его запросы попадали на один бэкенд при неизменном составе серверов. |
| Consistent hashing | Устойчивое отображение ключа на сервер уменьшает число переназначений при изменении состава серверов. Применяется и в балансировке, и в шардировании. |
Серверы могут иметь разные мощности; тогда в выборе учитывают веса. Число соединений или запросов не всегда точно отражает нагрузку: запросы различаются по стоимости, а сведения о состоянии успевают устареть. Несколько балансировщиков, видя один и тот же наименее загруженный сервер, могут одновременно направить на него много запросов.
Балансировщику также нужны сведения о доступности бэкендов. Активные проверки здоровья и наблюдение за ошибками обычных запросов позволяют временно исключать проблемные экземпляры. Например, Keepalived поддерживает проверки здоровья и VRRP для переключения виртуального адреса; его название не следует путать с TCP keepalive.
В Nginx, Envoy и HAProxy многие из этих алгоритмов уже реализованы. В лабораторной работе мы сравним их на общем наборе серверов при разной нагрузке и частоте обновления сведений.
Шардирование и репликация
Шардирование делит данные на части — шарды. Оно нужно, когда данные или нагрузка на их обработку уже не помещаются на одном сервере. Репликация создаёт несколько копий данных для доступности и надёжности. Эти приёмы можно сочетать: каждый логический шард хранить в нескольких репликах.
Шард, реплика и физический сервер не обязательно соответствуют друг другу один к одному. Один сервер может хранить несколько шардов и реплики разных частей данных. Есть и схемы, где набор узлов хранения выбирается отдельно для каждого ключа; тогда эти наборы могут пересекаться.
Ключ шардирования позволяет детерминированно определить, где находятся нужные данные. В демо kv можно отдельно сравнить шардирование и репликацию; сочетание обоих подходов в нём не реализовано.
Остаток от деления
Простой вариант: shard_id = hash(key) % num_shards. При равномерном хеше ключи распределяются по шардам примерно равномерно, но при изменении числа шардов приходится перемещать большую часть данных. Популярность отдельных ключей при этом может быть очень разной: равное число ключей не означает равную нагрузку.
Пример реализации: simple-sharding.
Consistent hash ring
Представим пространство хешей как кольцо. Хеш идентификатора узла задаёт его позицию на кольце, а хеш ключа — позицию данных. Для ключа выбираем ближайший узел по часовой стрелке. Можно договориться и об обратном направлении; все участники должны применять одно правило.
При добавлении узла к нему переходит часть ключей соседнего участка; при удалении его участок переходит следующему узлу. При равномерном хешировании и фиксированном коэффициенте репликации ожидаемый объём перемещений имеет порядок N / n, где N — объём данных, n — число узлов.
Одна случайная точка на физический сервер может давать заметный перекос размеров участков. Поэтому одному серверу назначают несколько виртуальных узлов — позиций на кольце. Сумма нескольких участков обычно распределена ровнее; число виртуальных узлов можно выбирать с учётом мощности сервера.
Если нужны r копий ключа, обходят кольцо и выбирают r разных физических узлов. Виртуальные позиции, принадлежащие уже выбранному серверу, пропускают: несколько копий на одной машине не защищают от её отказа. Пример такой схемы описан в Dynamo, разделы 4.2–4.3.
Полезные материалы
- Оригинальная статья про балансер Твиттера
- Описание алгоритмов балансировки в envoy proxy
- Load Balancing in the Datacenter
- Power of 2 choices
- Test Driving “Power of Two Random Choices” Load Balancing
- A Guide to Consistent Hashing
- Consistent hashing
- Consistent Hashing: Algorithmic Tradeoffs
- Sharding with consistent hashing и cassandra partitioning (секция 5)

