Add week 8 materials

This commit is contained in:
osukhoroslov committed 2026-11-04 19:23:39 +03:00
1 parent a987b82834
commit 0bf71ed374
38 files changed
+2908

No files matched your search

+94
View File
@@ -0,0 +1,94 @@
# Балансировка трафика и шардирование
На семинаре разбираем, как распределять запросы между серверами и данные между узлами хранения, чтобы масштабировать сервисы при росте нагрузки и объёма данных. Сравнить алгоритмы балансировки можно в [лабораторной работе](./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)