Балансируем трафик
t.me/thisnotesЗачем балансировать трафик растекаться не буду. Все понимают важность масштабирования. А где масштабирование, там надо и учить систему работать в новой парадигме.
Задача балансировки трафика в идеальном случае решается так: берём имеющееся у нас знание про нагрузку от пользователей в будущем (на бесконечности, конечно же), дополняем знанием о тяжести каждого запроса в каждом отдельном используемом ресурсе (CPU, RAM, диск и т.д.) и решаем стандартную задачу о назначениях какой-нибудь венгеркой, но мегабыстрой, чтобы за наносекунду ответ на всю бесконечность и получить.
Каждый день таким занимаюсь.
Вообще написать свой базовый балансер, который трафик туда-сюда проксирует, не очень сложно. Какая-нибудь базовая кастомная версия спокойно выдаст вам пропускную способность в пару тысяч запросов на среднем ноутбуке (если запросы небольшие). Это если вы не пытаетесь в безопасность с шифрованием и разными сложными протоколами...
Жизнь чуть более прозаична, потому приходится обмазываться эвристиками. Но перед этим поймём, какие балансеры вообще бывают.
Существует их три основных:
- L3 работает на 3м уровне в модели OSI и занимается скорее маршрутизацией, а не распределением нагрузки
- L4 (на 4м уровне) нужен чтобы отправлять все UDP пакеты и сообщения в рамках одного TCP подключения на один и тот же бэкенд
- L7 (хаха попались да на 7 уровне) нужен для сложной кастомной логики типа балансировки по урлу/кукам/чему угодно ещё.
Алгоритмы балансировки
- Рандомное распределение : )
- Round Robin: раскидываем запросы по очереди по кругу (если у нас 3 сервера, то 1 2 3 1 2 3 1 2).
Почти наверняка вам этого будет достаточно в случае стандартного бэкенда, если у ваc гомогенный бэкенд (все сервера одинаковые). Нам вот достаточно. - Weighted Round Robin: взвешенная версия, для которой можно указать вероятность улетания запроса в конкретный сервер.
Может применяться если у вас каким-то чудом сервера разной мощности. Может вы там их в подвале собирали и один обделили железом.
Используется во всяких сетевых устройствах, когда есть несколько маршрутов и хочется утилизировать пропускную способность канала максимально. - Least connections/Least response time: это когда мы отправляем запросы на сервера, с которыми у балансера либо меньше всего соединений, либо накоплена статистика про самые быстрые запросы. Важно, что обычно такие алгоритмы замеряют скорость ответа по условному /ping, что может не отражать реальную ситуацию на сервере, который может умирать по CPU, но справляться отвечать на лёгкие запросы. Или который может зависать на сложных запросах с походами в базу, но опять же легко обрабатывать пинг.
Если предыдущие два алгоритма были статическими, то эти -- динамические. То бишь могут менять своё распределение со временем. - IP hashing: хешируем ip source port и протокол и по хешу вычисляет сервер, на который надо отправить запрос.
Нужно это для каких-нибудь stateful серверов. Правда является уже не очень юзабельным, т.к. сейчас уже могут быть огромные подсети с одним и тем же IP, что будет сильно грузить конкретный сервер. Плохо он нагрузку распределяет. А ещё если один из серверов из строя выходит, весь маппинг юзера на сервер может сильно изменится. Не кайф.
Подобную задачу сегодня решают с помощью consistent hashing. - Consistent hashing: мапим всю числовую прямую на кольцо, вычисляем хеш каждого хоста и при запросе вычисляем хеш юзера. Простейшим бинпоиском находим ближайший хост. Вы великолепны!
При желании (если у вас различных хешей мало) можно вычислить хост для каждого хеша заранее, чтобы не тратить время на логарифм операций. И доставать из памяти за константу готовый хост.
Однако это решение тоже не идеально. Представим, что у нас есть 4 хоста, которые обслуживают по четверти различных хешей каждый. Если упадёт один из хостов, то на его соседа прилетит в 2 раза больше нагрузки! Потому иногда в кольцо добавляют виртуальные ноды, которые мапят на физические. В итоге распределение получается более равномерным.
Хотя пример с 4 серверами довольно вырожденный. В общем случае у вас изменится не более чем 1/n связей клиент-сервер, где n -- кол-во серверов. - Rendezvous hashing: заморочки с кольцами и виртуальными нодами бывают довольно неприятными, потому иногда прибегают к другому решению. Тут мы хешируем все пары от запроса и номеров нод. Например (пусть запросы == числа):
hash(123, 1) = 24 hash(123, 2) = 19 hash(123, 3) = 3 hash(123, 4) = 45
После выбираем максимальный хеш и отправляем запрос на соответствующую ноду (в нашем примере запрос улетит на 4ю ноду, ведь 45 -- максимум). Если надо понять, где забрать ответ, то повторяем операцию и получаем ту же ноду. Если какая-то нода отвалилась, то просто берём следующую по порядку. Есть свою нюансы с появлением новых нод, как и у консистентного хеширования, но как с ними разбираться, зависит от вашей задачи.
Есть ещё всякие разные эвристики и способы балансироваться, чтобы не задушить хосты нагрузкой и обеспечить адекватный UX: получать инфу о загрузке хостов от них, а не опрашивать самому; приоритезация трафика по классам нагрузки (Voice over Video в Zoom, например); locality aware балансировка; 100% кто-то ML прикрутил для предикта нагрузки.
Когда всё ломается
Что будет, если хост выпал?
Обычно балансер шлёт простые запросы-хелсчеки, проверяющие состояние хостов. Дёргает тот самый /ping. Если хелсчек неудачный, то и запросы на хост слать больше не стоит. Если для активных запросов клиент получит timeout или что-нибудь такое, то он должен сам поретраить, ведь в общем случае у балансера не хватает контекста, чтобы понять, что ему делать. В зависимости от задачи можно настраивать авторетрай некоторых видов запросов на L7.
А как балансер узнаёт, в какие хосты ему вообще ходить? Это т.н. задача дискавери. Можно рядом прикрутить сервис, который будет балансеру эту информацию сообщать. Или какой-нибудь статический конфиг прикрутить. Или ещё чего.
А что если сам балансер сломался? Ведь это такая же железка, как любая другая. Можно поставить ещё один балансер : )
Идея довольно простая: за одним и тем же IP стоит несколько серверов. Или можно использовать Anycast, который броадкастит запросы повсюду, и конкретный запрос будет обработан на произвольный балансер.
Но, несмотря на все плюсы, балансеры не идеальны:
- их нужно настраивать.
Любая нагрузка на поддержку инфраструктуры приводит к тому, что вы не пишите новые фичи, которые помогут заработать больше деняк. - балансер -- единая точка отказа.
Если у вас несколько ДЦ, не переживёте, но всё равно не очень приятно потерять все хосты из ДЦ.
Если вы вдруг почему-то боитесь, что упрётесь в пропускную способность балансера, то волноваться не стоит. Скорее проблемы будут с сетью. - дополнительный хоп по сети.
Который иногда может вносить задержку в единицы-десятки мс, что для некоторых приложений и сфер может быть критично.
Ну и конечно есть альтернатива -- балансировка на клиенте! У неё
- нет единой точки отказа (если вы можете решать задачу дискавери без введения таковой)
- нет хопа по сети
- можно писать произвольно сложные алгоритмы балансировки
- можно писать более сложные хелсчеки.
Такое часто применяется в сложный системах. Например в разных распределённых БД. Не призываю вас делать балансировку на мобилке юзера.
Но понятно, что подобный подход приводит к более сложному коду, да и клиенту доверять может быть сложно (потому обычно применяется в защищённой среде).
Не переусердствуем
Несмотря на то, что теперь у вас есть балансер, который умеет слать запросы на много-много разных stateless хостов, обычно эти самые хосты ходят в один единственный инстанс БД. Это может быть проблемой, пусть БД обычно оптимизированы под большое кол-во запросов (ведь их писали умные дядьки на плюсах, а не джуны на питоне), они умеют в репликацию и другие важные-бумажные механизмы.
Т.к. пост основан на рассказе моего близкого друга с одного из наших семинаров, тут я позволю себе процитировать его слово в слово:
Постоянно на разных докладах и в коммьюнити обсуждают проблемы бигтехов, как работают разные сложные системы. Мы привыкли, что у нас сотни тысяч RPS, большие нагрузки, нужно много-много-много девяток. Но на самом деле, если открыть дверь и посмотреть в мир, где люди решают реальные проблемы, развивают стартапы и зарабатывают миллиарды долларов, можно понять, что код у них выглядит скорее всего по-другому. Да и мир по-другому у них устроен.
Посмотрим на кейс Notion.
В 2020-м у них было что-то около 1к юзеров. А в 2024-м уже 100М. При этом в 2021-м они имели единственную postgres базу.
В какой-то момент они перестали в одну инсталляцию вмещаться, потому пришлось шардироваться. Why complicate your life? Чуваки взяли один большой postgres и заменили на 32 postgres'а поменьше, шардируя все данные по user_id. Единственная проблема -- переход со старого сетапа на новый. Но в целом подход довольно простой и позволяет не помирать.
Переключение происходило в 4 этапа:
- Double-write.
При записи пишем и в старый сетап, и в новый. - Backfill.
Фоновый процесс переноса данных из старой бд в новые. - Verification.
Проверка данных в двух сетапах на консистентность. - Switch-over.
Непосредственно переключение со старой базы на новую.
Мысль за этим следующая: до какого-то масштаба, до которого доходят большие компании, не нужно сложных алгоритмов, решений и систем. Нужно сделать что-то простое, чтобы работало. И больше про это не думать.
Это касается не только балансировки нагрузки, но и всего остального. Отшардируйте втупую ваш postgres. Сделайте простой тупой эксперимент, чтобы проверить гипотезу, а не тратьте дохера времени на сложную фичу без профита. Напишите код попроще, пусть он и не так красиво выглядит. Зато осознать легче.
Решайте задачи, а не изобретайте велосипеды. Всё-таки нам ровно за это деньги платят.
Ссылочки про Notion:
- Herding elephants: Lessons learned from sharding Postgres at Notion: https://www.notion.com/blog/sharding-postgres-at-notion
- The Great Re-shard: adding Postgres capacity (again) with zero downtime: https://www.notion.com/blog/the-great-re-shard
- Building and scaling Notion’s data lake: https://www.notion.com/blog/building-and-scaling-notions-data-lake
И ещё одна по теме:
- Масштабируемая конфигурация nginx от Игоря Сысоева (основного автора nginx): https://www.youtube.com/watch?v=jf3wIN-FwW4