Files
hse-2026/materials/08-scaling/seminar/readme.md
T
2026-11-04 19:23:39 +03:00

95 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Балансировка трафика и шардирование
На семинаре разбираем, как распределять запросы между серверами и данные между узлами хранения, чтобы масштабировать сервисы при росте нагрузки и объёма данных. Сравнить алгоритмы балансировки можно в [лабораторной работе](./load-balancing-lab/), а запустить небольшие сервисы — в примерах [Nginx](./nginx-balancing/) и [key-value](./kv/).
## Балансировка трафика
При росте "читающей" нагрузки на сервис одно из простых решений — запустить несколько копий
сервиса и научиться распределять нагрузку между ними. Балансировщик принимает запрос или пакет и выбирает сервер для его обработки. На выбор могут влиять адрес клиента, загрузка и производительность
серверов, а также состояние уже установленных соединений.
При переходе к модели с несколькими копиями сервиса необходимо продумать,
нужно ли обмениваться информацией между данными копиями. Как правило, проще всего балансировать
нагрузку в случае реплицированного и/или stateless-сервиса.
Одним из вопросов при выборе балансировщика нагрузки является вопрос: на каком "уровне" вы хотите
балансировать нагрузку? L3/L4-балансировщики работают на третьем или четвёртом уровне модели OSI и распределяют
входящие TCP- и UDP-пакеты. На данном уровне мы не знаем о клиенте ничего, кроме его IP-адреса и порта,
и не смотрим на содержимое запросов, поэтому балансировка менее точная, но более легковесная.
L7-балансировщики оперируют на уровне протоколов приложений, например HTTP. Данные балансировщики более
гибкие и имеют больше информации о пользователе. Также достаточно много полезной информации можно
получить из заголовков HTTP-запроса (например, идентификатор пользователя). Это позволяет сделать
балансировку более точной, но балансировщик начинает выполнять больше задач, из-за чего требуется
больше ресурсов.
### Запросы и соединения
Stateless-приложение не требует хранить состояние клиентской сессии в конкретном экземпляре между запросами. Оно может работать поверх TCP: состояние транспортного соединения и состояние приложения — разные вещи. Например, HTTP-прокси может распределять независимые запросы между серверами, поддерживая собственные TCP-соединения с ними.
UDP также не означает отсутствие состояния: HTTP/3 работает поверх QUIC, который поддерживает соединения, используя UDP. См. [HTTP/3](https://www.rfc-editor.org/rfc/rfc9114.html) и [QUIC](https://www.rfc-editor.org/rfc/rfc9000.html).
При балансировке TCP-пакетов одного соединения они должны попадать на сервер, который знает состояние этого соединения. Для этого можно хранить таблицу соединений или детерминированно выбирать сервер по адресам и портам. Изменение набора серверов требует осторожности: оно может изменить выбор для уже открытых соединений. Короткий таймаут таблицы сам по себе эту задачу не решает.
DNS помогает выбрать адрес перед установлением соединения. Новый DNS-ответ не меняет удалённый адрес существующего TCP-соединения. При anycast один адрес объявляют из нескольких мест, а путь к нему выбирает сеть; изменение этого пути — отдельная причина попадания пакетов на другой балансировщик.
Ответ может возвращаться через прокси или идти напрямую от бэкенда клиенту — схема Direct Server Return/Response (DSR). Подход зависит от архитектуры. Например, в нашем [демо Nginx](./nginx-balancing/) ответы проходят через HTTP-прокси. Подробнее: [Google SRE: балансировка на входе](https://sre.google/sre-book/load-balancing-frontend/).
### Алгоритмы выбора сервера
| Алгоритм | Как выбирается сервер |
| --- | --- |
| **Round robin** | Серверы перебираются по кругу. |
| **Random** | Для каждого выбора берётся случайный сервер. Даже равномерный случайный выбор даёт колебания нагрузки. |
| **Least connections** | Выбирается сервер с наименьшим числом активных соединений. **LeastLoaded** в лабораторной работе использует другую метрику — число незавершённых запросов. |
| **Power of Two Choices** | Выбираются два случайных сервера, затем менее загруженный из них по выбранной метрике. Обобщение на k кандидатов называется **Power of k Choices**. |
| **IP hashing** | Сервер выбирается детерминированно по адресу клиента, чтобы его запросы попадали на один бэкенд при неизменном составе серверов. |
| **Consistent hashing** | Устойчивое отображение ключа на сервер уменьшает число переназначений при изменении состава серверов. Применяется и в балансировке, и в шардировании. |
Серверы могут иметь разные мощности; тогда в выборе учитывают веса. Число соединений или запросов не всегда точно отражает нагрузку: запросы различаются по стоимости, а сведения о состоянии успевают устареть. Несколько балансировщиков, видя один и тот же наименее загруженный сервер, могут одновременно направить на него много запросов.
Балансировщику также нужны сведения о доступности бэкендов. Активные проверки здоровья и наблюдение за ошибками обычных запросов позволяют временно исключать проблемные экземпляры. Например, [Keepalived](https://github.com/acassen/keepalived#readme) поддерживает проверки здоровья и VRRP для переключения виртуального адреса; его название не следует путать с TCP keepalive.
В Nginx, Envoy и HAProxy многие из этих алгоритмов уже реализованы. В [лабораторной работе](./load-balancing-lab/) мы сравним их на общем наборе серверов при разной нагрузке и частоте обновления сведений.
## Шардирование и репликация
Шардирование делит данные на части — **шарды**. Оно нужно, когда данные или нагрузка на их обработку уже не помещаются на одном сервере. **Репликация** создаёт несколько копий данных для доступности и надёжности. Эти приёмы можно сочетать: каждый логический шард хранить в нескольких репликах.
Шард, реплика и физический сервер не обязательно соответствуют друг другу один к одному. Один сервер может хранить несколько шардов и реплики разных частей данных. Есть и схемы, где набор узлов хранения выбирается отдельно для каждого ключа; тогда эти наборы могут пересекаться.
<p align="center"><img src="./content/sharding.png" width="100%" /></p>
Ключ шардирования позволяет детерминированно определить, где находятся нужные данные. В [демо kv](./kv/) можно отдельно сравнить шардирование и репликацию; сочетание обоих подходов в нём не реализовано.
### Остаток от деления
Простой вариант: `shard_id = hash(key) % num_shards`. При равномерном хеше ключи распределяются по шардам примерно равномерно, но при изменении числа шардов приходится перемещать большую часть данных. Популярность отдельных ключей при этом может быть очень разной: равное число ключей не означает равную нагрузку.
Пример реализации: [simple-sharding](./simple-sharding/).
### Consistent hash ring
Представим пространство хешей как кольцо. Хеш идентификатора узла задаёт его позицию на кольце, а хеш ключа — позицию данных. Для ключа выбираем ближайший узел по часовой стрелке. Можно договориться и об обратном направлении; все участники должны применять одно правило.
<p align="center"><img src="./content/ring_composition.png" width="100%" /></p>
*[Источник иллюстрации](https://medium.com/@kewei.train/deterministic-aperture-algorithm-%E7%AD%86%E8%A8%98-3a3bbb5e30e2)*
При добавлении узла к нему переходит часть ключей соседнего участка; при удалении его участок переходит следующему узлу. При равномерном хешировании и фиксированном коэффициенте репликации ожидаемый объём перемещений имеет порядок N / n, где N — объём данных, n — число узлов.
Одна случайная точка на физический сервер может давать заметный перекос размеров участков. Поэтому одному серверу назначают несколько **виртуальных узлов** — позиций на кольце. Сумма нескольких участков обычно распределена ровнее; число виртуальных узлов можно выбирать с учётом мощности сервера.
Если нужны r копий ключа, обходят кольцо и выбирают r разных физических узлов. Виртуальные позиции, принадлежащие уже выбранному серверу, пропускают: несколько копий на одной машине не защищают от её отказа. Пример такой схемы описан в [Dynamo, разделы 4.2–4.3](https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf).
## Полезные материалы
- [Оригинальная статья про балансер Твиттера](https://blog.twitter.com/engineering/en_us/topics/infrastructure/2019/daperture-load-balancer.html)
- [Описание алгоритмов балансировки в envoy proxy](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/load_balancers)
- [Load Balancing in the Datacenter](https://sre.google/sre-book/load-balancing-datacenter/)
- [Power of 2 choices](https://www.eecs.harvard.edu/~michaelm/postscripts/mythesis.pdf)
- [Test Driving “Power of Two Random Choices” Load Balancing](https://www.haproxy.com/blog/power-of-two-load-balancing/)
- [A Guide to Consistent Hashing](https://www.toptal.com/big-data/consistent-hashing)
- [Consistent hashing](http://michaelnielsen.org/blog/consistent-hashing/)
- [Consistent Hashing: Algorithmic Tradeoffs](https://dgryski.medium.com/consistent-hashing-algorithmic-tradeoffs-ef6b8e2fcae8)
- [Sharding with consistent hashing](https://blog.yugabyte.com/four-data-sharding-strategies-we-analyzed-in-building-a-distributed-sql-database/) и [cassandra partitioning (секция 5)](https://www.cs.cornell.edu/projects/ladis2009/papers/lakshman-ladis2009.pdf)