Лишние элементы или как мы балансируем между серверами
Привет, Хабр! Какое-то время назад люди осознали, что увеличивать мощность сервера в соответствии с ростом нагрузки просто невозможно. Тогда-то мы и узнали слово «кластер». Но как бы красиво это слово не звучало, всё равно приходится технически объединять разрозненные серверы в единое целое – тот самый кластер. По городам и весям мы добрались до наших узлов в моём предыдущем опусе. А сегодня мой рассказ пойдёт о том, как делят нагрузку между членами кластера системные интеграторы, и как это сделали мы.

Внутри публикации вас также ждёт бонус в виде трёх сертификатов на месячную подписку ivi+.
Делай как все
Какие задачи ставят для кластера?
1. Много трафика
2. Высокая надёжность
03d63a0996fb
Как этого добиться? Самый простой способ поделить нагрузку между серверами – не делить её. Вернее так: дадим полный список серверов – пусть клиенты сами разбираются. Как? Да просто прописав все IP-адреса серверов в DNS для заданного имени. Древняя и знаменитейшая балансировка round-robin DNS. И в целом неплохо работает, пока не возникает потребность добавить узел – я уже писал про инертность DNS-кэшей. Выглядит DNS-балансировка так:

А если надо убрать сервер из кластера (ну, сломался он), наступает каюк. Чтобы каюк на нас не наступил, надо куда-то быстро повесить IP сломавшегося сервера. Куда? Ну, допустим, на соседа. Окей, а как мы это можем автоматизировать? Для этого придумана куча протоколов вроде VRRP , CARP со своими достоинствами и недостатками.
Первое, во что обычно упираются, это ARP-кэш на роутере, который не хочет понимать, что IP-адрес переехал на другой MAC. Впрочем, современные реализации либо «пингуют» роутер с нового MAC’а (обновляя кэш таким образом), либо вообще используют виртуальный MAC, который не меняется во время работы.
Второе, что бьёт по голове – это серверные ресурсы. Мы ведь не будем держать один сервер из двух в горячем standby? Сервер должен работать! Поэтому мы будет резервировать по VRRP два адреса на каждом сервере: один – primary и один backup. Если один из серверов пары сломается, второй примет на себя всю его нагрузку… может быть… если справится. И вот такая «парность» и будет главным недостатком, ибо не всегда есть возможность или целесообразность держать двойной запас серверной мощности.
Также нельзя не заметить, что каждый сервер требует своего собственного, глобально маршрутизируемого IP-адреса. В наше нелёгкое время это может стать большой проблемой.
В целом, мне скорее не нравится такой способ балансировки и резервирования, но для целого ряда задач и объёмов трафика он хорош. Прост. Не требует дополнительного оборудования – всё делается серверным софтом.
На шару
Продолжая перечисление простых решений, а может, стоит просто навесить один и тот же IP-адрес на несколько серверов? Ну, в IPv6 есть возможность сделать anycast в одном домене (и то балансировка там будет только для хостов внутри него, а для внешних – совсем не так), а вот в IPv4 такая штука просто создаст ARP-конфликт (более известный как конфликт адресов). Но это если «в лоб».
А если использовать небольшой наворот под кодовым названием «shared address» (разделяемый адрес), то такое возможно. Суть трюка заключается в том, чтобы сначала превратить входящий уникаст в броадкаст (ладно, если кто хочет – пусть делает мультикаст), а затем только один из серверов отвечает на пакеты от данного клиента. Как осуществляется превращение? Очень просто: все серверы кластера в ответ на ARP-запрос возвращают один и тот же MAC: либо несуществующий на сети, либо мультикастовый. После этого сеть сама размножит входящие пакеты на все члены кластера. А как серверы договариваются о том, кто отвечает? Для простоты скажем так: по остатку от деления srcIP на количество серверов в кластере. Дальше – дело техники.

Балансировка с разделяемым адресом
Такую технику реализуют разные модули и разные протоколы. Для FreeBSD такая возможность реализована в CARP. Для Linux в прошлой жизни мне доводилось использовать ClusterIP. Сейчас он, видимо, не развивается. Но я уверен, что есть и другие реализации. Для Windows такая штука есть во встроенных средствах кластеризации. В общем, выбор есть.
Преимуществом такой балансировки всё так же является чисто серверная реализация: никакой особой настройки со стороны сети не требуется. Публичный адрес используется всего один. Добавление или отключение одного сервера в кластере происходит быстро.
В недостатках же, во-первых, числится необходимость дополнительной проверки на уровне приложения, а во-вторых (и даже «в главных») – это ограничение по входящей полосе: количество входящего трафика не может превышать полосу в физическом подключении серверов. И это очевидно: ведь входящий трафик попадает на все серверы одновременно.
Так что в целом это неплохой способ балансировать трафик, если у вас немного входящего трафика, но употреблять его следует с умом. И тут я скромно скажу, что сейчас мы этот метод не используем. Как раз из-за проблемы входящего трафика.
Никогда не заговаривайте с интеграторами
Мне почему-то кажется, что задача балансировки между серверами должна быть типовой. А для типовой задачи должны быть типовые решения. А кто хорошо продаёт типовые решения? Интеграторы! И мы спросили…
Надо ли говорить, что нам предложили решения из вагонов самого разного оборудования? От Cisco ACE и до всяческих F5 BigIP LTM. Дорого. Но хорошо ли? Ладно, есть ещё прекрасный бесплатный софтовый L7-балансировщик haproxy.
В чём же смысл этих штучек? Смысл в том, что балансировку они делают на уровне приложения – Layer 7. Фактически, такие балансировщики являются полными прокси (full proxy): они устанавливают соединение с клиентом и с сервером от своего имени. В теории это хорошо тем, что они могут осуществлять прилипание клиента к определённому серверу (server affinity) и даже выбирать бэкэнд в зависимости от запрошенного контента (крайне полезная штука для такого ресурса как ivi.ru). А при определённой настройке – и фильтровать запросы по URL, защищаясь от различного рода атак. Главное же преимущество – такие балансировщики могут определять жизнеспособность каждого узла в кластере самостоятельно.

Балансировщики включаются в сеть как-то так
Признаюсь, мой прошлый опыт кричал «Без балансировщиков сделать ничего не возможно». Мы посчитали… И ужаснулись.
Самые производительные балансировщики, которые нам предложили, имели пропускную способность 10Гбит/с. По нашим меркам не смешно (серверы с тяжелым контентом у нас подключаются 2 * 10Гбит/с). Соответственно, чтобы получить нужную полосу, надо было бы набить целую стойку такими балансировщиками, с учётом резервирования. Посмотрите вот на эту схему:

Так выглядит физическое подключение балансировщика.
Конечно, есть ещё режим частичного прокси (half-proxy), когда через балансировщик проходит только половина трафика: от клиента к серверу, а обратно от сервера – идёт напрямую. Но такой режим отключает L7 функциональность, и балансировщик становится L3-L4, что резко снижает его ценность. А дальше последовательно возникает две проблемы: сначала нужно сделать достаточную мощности сети (в первую очередь – по портам, потом – по надёжности). Затем возникает вопрос: а как балансировать нагрузку между балансировщиками? В Москве поставить стойку с оборудованием для балансировки, в теории, наверно, можно. А вот в регионах, где наши узлы минималистичны (серверы да циска) добавление нескольких балансировщиков уже как-то не смешно. К тому же, чем длиннее цепочка, тем ниже надёжность. А оно нам надо?
ECMP или ничего лишнего
Решение нагуглилось почти случайно. Оказывается, современный роутер сам по себе умеет балансировать трафик. Если подумать, это разумно: ведь одна и та же подсеть может быть доступна через разные каналы с одинаковым качеством. Такая функция называется ECMP – Equal Cost Multiple Paths. Видя одинаковые по своей метрике (в общем смысле этого слова) маршруты, роутер просто делит пакеты между этими маршрутами.
ОК, идея интересная, но будет ли это работать? Тестовый запуск мы провели, прописывая статические маршруты с роутера в сторону нескольких серверов. Концепт оказался работоспособным, но потребовалась дополнительная проработка.
Во-первых, необходимо обеспечить попадание всех IP-пакетов, относящихся к одной TCP-сессии на один и тот же сервер. Ведь иначе TCP-сессия просто не состоится. Это так называемый режим «per-flow» (или «per-destination»), и он вроде как включен по умолчанию на роутерах Cisco.
Во-вторых, статические маршруты не подходят – ведь нам надо иметь возможность автоматически убирать сломанный сервер из кластера. Т.е. надо применять какой-либо протокол динамической маршрутизации. Для единообразия мы выбрали BGP . На серверах установлен программный роутер (сейчас это quagga, но в ближайшем будущем перейдём на BIRD), который анонсирует «серверную» сеть на роутер. Как только сервер перестаёт слать анонсы, роутер перестаёт распределять трафик. Соответственно, для балансировки на роутере настроено значение maximum-paths ibgp равное количеству серверов в кластере (с некоторыми оговорками):
Но сам BGP гарантирует только сетевую доступность сервера, но не прикладную. Чтобы проверять работоспособность прикладного софта, на сервере запущен скрипт, который осуществляет ряд проверок, и если что-то пошло не так, просто гасит BGP. Дальше дело роутера – перестать слать пакеты на этот сервер.
В третьих, была замечена неоднородность распределения запросов между серверами. Небольшая, и компенсируемая нашим ПО кластеризации, Но хотелось-то равномерности. Выяснилось, что по умолчанию роутер балансирует пакеты исходя из адресов отправителя и получателя пакета, то есть L3-балансировка. Исходя из того, что адрес получателя (сервера) всегда один и тот же, это указывает на неоднородность в адресах-источниках. Учитывая массовую NAT’изацию интернета, это неудивительно. Решение оказалось простым – заставить роутер учитывать порты получателя и источника (L4-балансировка) при помощи команды вроде
в зависимости от IOS. Главное только, чтобы у вас не было такой команды:
Итоговая схема выглядит вот так:

Ничего знакомого не заметили? Ну там, что это фактически anycast, только не между регионами, а внутри одного узла? Так это он и есть!
Преимуществами L3-L4 балансировки можно назвать эффективность: роутер должен быть, без него никак. Надёжность тоже на должном уровне – если вдруг сломается роутер, то дальше уже неважно. Дополнительное оборудование не приобретается – это хорошо. Публичные адреса тоже не расходуются – один и тот же IP обслуживается сразу несколькими серверами.
Есть, увы, и недостатки.
1. На сервер приходится ставить дополнительный софт, настраивать его и т.п. Правда, этот софт достаточно скромен, и не расходует ресурсы сервера. Так что – терпимо.
2. Переходные процессы одного сервера (включение в кластер или исключение) влияют на весь кластер. Ведь роутер ничего не знает о серверах – он думает, что имеет дело с роутерами и каналами. Соответственно, все соединения распределяются на все доступные каналы. Никакого server-affinity. В результате при выключении сервера все соединения перетасовываются между всеми оставшимися серверами. И все активные TCP-сессий разрываются (строго говоря – не все, есть шанс, что часть вернётся на тот же сервер). Это плохо, но учитывая нашу защиту от таких ситуаций (плеер перезапрашивает контент при разрыве соединений) и некоторые финты ушами (о которых я ещё расскажу), жить можно.
3. Существуют ограничения, на сколько равных маршрутов роутер может разделить трафик: на cisco 3750X и 4500-X это 8, на 6500+Sup2T – это 32 (но там есть один прикол). В целом этого достаточно, к тому же есть трюки, которые позволяют раскрутить это ограничение.
4. При такой схеме нагрузка на все серверы распределяется равномерно, что накладывает требование одинаковости на все серверы. И если добавлять в кластер более современный, мощный, сервер, нагрузка на него будет не больше, чем на соседа. К счастью, наш софт кластеризации эту проблему в значительной степени нивелирует. К тому же, в роутерах еще есть функция unequal cost multiple paths, которую мы пока не задействовали.
5. Типовые таймауты BGP могут приводить к таким ситуациям, когда сервер уже недоступен, а балансирующий роутер всё ещё распределяет на него нагрузку. Но с этим нам поможет справиться BFD . Подробнее о протоколе — читайте wikipedia. Собственно, именно из-за BFD мы и решили перейти на BIRD.
Промежуточный итог
Исключение балансировщиков из цепочки прохождения трафика позволило сэкономить на оборудовании, повысило надёжность системы и сохранило минималистичность наших региональных узлов. Нам пришлось применить несколько трюков, чтобы компенсировать недостатки такой схемы балансировки (надеюсь, у меня ещё будет шанс рассказать об этих трюках). Но в существующей схеме всё ещё есть лишний элемент. И я очень надеюсь убрать его в этом году и рассказать, как нам это удалось.
Кстати, пока я писал (точнее, черновик вылёживался) эту статью, коллеги на Хабре написали очень неплохую статью про алгоритмы балансировки. Рекомендую к прочтению!
Введение в современную балансировку сетевой нагрузки и проксирование
Не так давно я услышал, что существует недостаток вступительных образовательных материалов о современной балансировке сетевой нагрузки и проксировании. И я подумал: как это возможно? Балансировка нагрузки — одна из основных концепций, необходимых для построения надежных распределенных систем. Разумеется, должна быть доступна качественная информация. К сожалению, это не так. Я искал и обнаружил, что материалы действительно поверхностны. В статье Википедии балансировка нагрузки и прокси-серверы содержатся обзоры некоторых понятий, но не полное разъяснение объекта, особенно в том, что касается современных микросервисных архитектур. Поиск в Google по запросу балансировки нагрузки также не предоставил мне хороших и понятных материалов.
В этой статье я пытаюсь исправить недостаток информации, предоставляя плавное введение в современную балансировку сетевой нагрузки и проксирования. Это серьезная тема, которая может быть предметом целой книги.
Что такое балансировка сетевой нагрузки и проксирование?
Wikipedia определяет балансировку нагрузки как:
В компьютерах балансировка нагрузки распределяет нагрузку между несколькими вычислительными ресурсами, такими как компьютеры, компьютерные кластеры, сети, центральные процессоры или диски. Цель балансировки нагрузки — оптимизация использования ресурсов, максимизация пропускной способности, уменьшение времени отклика и предотвращение перегрузки какого-либо одного ресурса. Использование нескольких компонентов балансировки нагрузки вместо одного может повысить надежность и доступность за счет резервирования. Балансировка нагрузки предполагает обычно наличие специального программного обеспечения или аппаратных средств, таких как многоуровневый коммутатор или система доменных имен, как серверный процесс.
Вышеприведенное определение применяется ко всем аспектам вычислений, а не только к сетям. Операционные системы используют балансировку нагрузки для планирования задач по физическим процессорам. например, в оркестраторах контейнеров, таких как Kubernetes, используется балансировка нагрузки для планирования задач в вычислительном кластере, а сетевые балансировки нагрузки используют балансировку нагрузки для планирования сетевых задач через доступные бэкенды. Остальная часть поста будет охватывать только балансировку сетевой нагрузки.

Обзор балансировки сетевой нагрузки
Рисунок показывает высокоуровневый обзор балансировки сетевой нагрузки. Некоторое количество клиентов запрашивает ресурсы из некоторого количества бэкендов. Балансировщик нагрузки находится между клиентами и внутренними компонентами и на высоком уровне выполняет несколько важных задач:
- Обнаружение служб: Какие бэкенды доступны в системе? Каковы их адреса (например, как должен работать балансировщик нагрузки)?
- Проверка работоспособности: Какие бэкенды в настоящее время здоровы и доступны для приема запросов?
- Балансировка нагрузки: Какой алгоритм следует использовать для балансировки отдельных запросов по здоровым бэкендам?
Правильное использование балансировки нагрузки в распределенной системе дает несколько преимуществ:
- Именование абстракции: Нет необходимости в том чтобы каждый клиент знал о каждом бэкенде (service discovery). Клиент может обращаться к балансировщику нагрузки через предопределенный механизм, а затем действие разрешения имен можно делегировать в балансировщик нагрузки. Предопределенные механизмы включают встроенные библиотеки и хорошо известные DNS/IP/port и будут обсуждаться более подробно ниже.
- Отказоустойчивость: Благодаря проверке работоспособности и различным алгоритмическим методам балансировщик нагрузки может эффективно маршрутизировать вокруг неработоспособного или перегруженного бэкенда. Это означает, что администратор может спокойно, без спешки устранить проблему на нездоровом узле.
- Стоимость и производительность: Распределенные системные сети редко бывают однородными. Наиболее вероятно, что система будет охватывать несколько сетевых зон и регионов. Интеллектуальная балансировка нагрузки может максимально увеличить трафик запросов в зонах, что увеличивает производительность (меньше латентности) и снижает общую стоимость системы (меньше полосы пропускания и прокладки оптоволокна, требуемого между зонами).
Балансировка нагрузки vs прокси-сервер
Когда речь идет о балансировщиках сетевой нагрузки, термины балансировки нагрузки и прокси используются примерно одинаково. Этот пост также будет рассматривать термины как эквивалентные. (Если быть точными, не все прокси-серверы являются балансировщиками нагрузки, но подавляющее большинство прокси-серверов выполняют балансировку нагрузки в качестве основной функции).
Есть мнение, что когда балансировка нагрузки выполняется как часть встроенной клиентской библиотеки, балансировщик нагрузки на самом деле не является прокси-сервером. Тем не менее, я бы сказал, что различие добавляет сложности уже запутанной теме. Типы топологий балансировки нагрузки подробно обсуждаются ниже, но этот пост рассматривает встроенную топологию балансировки нагрузки как просто особый случай проксирования; приложение проксирует через встроенную библиотеку, которая предлагает все те же абстракции, что и балансировщик нагрузки, находящийся вне процесса приложения.
L4 (connection/session) балансировка нагрузки
В масштабах отрасли сегодня решения по балансировке нагрузки часто разделяются на две категории: L4 и L7. Они относятся к уровням 4 и 7 модели OSI. Модель OSI представляет очень плохое приближение к сложности решений балансировки нагрузки, которые включают традиционные протоколы уровня 4, такие как TCP и UDP, но часто заканчиваются включением битов и частей протоколов на разных уровнях OSI. Возникает вопрос: если балансировщик нагрузки L4 TCP также поддерживает терминирование TLS, является ли он теперь балансировщиком нагрузки L7?

Базовая балансировка нагрузки TCP L4
Рисунок показывает традиционный балансировщик нагрузки L4 TCP. В этом случае клиент устанавливает TCP-соединение с балансировщиком нагрузки. Балансировщик нагрузки завершает соединение (т.е. отвечает непосредственно на SYN), выбирает бэкенд и устанавливает новое TCP-соединение с бэкендом (т.е. отправляет новый SYN). Детали диаграммы не важны и будут подробно обсуждаться ниже — в разделе, посвященном балансировке нагрузки L4.
Основной вывод этого раздела: заключается в том, что балансировщик нагрузки L4 обычно работает только на уровне соединения/сеанса L4 TCP/UDP. Таким образом, балансировщик нагрузки грубо перетасовывает байты взад и вперед и гарантирует, что байты с того же сеанса завершаются на одном и том же бэкенде. Балансировщик L4 не знает о каких-либо деталях байтов приложения, которые перетасовывает. Байтами могут быть HTTP, Redis, MongoDB или любой другой протокол приложения.
L7 (приложение) балансировка нагрузки
Балансировка нагрузки L4 проста и по-прежнему широко используется. Каковы недостатки балансировки нагрузки L4, которые гарантируют инвестиции в балансировку нагрузки L7 (приложение)? В качестве примера возьмем следующий конкретный случай L4:
- Два клиента gRPC/HTTP2 хотят поговорить с бэкендом, поэтому подключаются через балансировщик нагрузки L4.
- Балансировщик нагрузки L4 делает одно исходящее TCP-соединение для каждого входящего TCP-соединения, что приводит к двум входящим и двум исходящим соединениям.
- Однако клиент A отправляет по 1 запрос в минуту (RPM), а клиент B отправляет 50 запросов в секунду (RPS) по его соединению.
В предыдущем сценарии бэкенд, выбранный для обработки клиента A, будет обрабатывать нагрузку примерно в 3000x раз меньше, чем бэкенд, выбранный для обработки клиента B! Эта серьезная проблема, как правило, в первую очередь нарушает цель балансировки нагрузки. Также обратите внимание, что эта проблема возникает для любого протокола мультиплексирования. (Мультиплексирование означает отправку одновременных запросов приложений по одному соединению L4, а keep-alive означает команду не закрывать соединение, когда нет активных запросов). Все современные протоколы развиваются как для мультиплексирования, так и для поддержания жизнеспособности по соображениям эффективности (как правило, соединения, которые шифруются с использованием TLS, создавать дорого). Поэтому с течением времени сопротивление балансировки нагрузки L4 становится более выраженным. Эта проблема устраняется балансировщиком нагрузки L7.

HTTP/2 балансировка нагрузки L7
Рисунок показывает балансировщик нагрузки L7 HTTP/2. В этом случае клиент делает одно соединение HTTP/2 TCP с балансировщиком нагрузки. Затем балансировщик выполняет два внутренних подключения. Когда клиент отправляет два потока HTTP/2 в балансировщик нагрузки, поток 1 отправляется на бэкенд 1, в то время как поток 2 отправляется на бэкенд 2. Таким образом, даже мультиплексирование клиентов, которые имеют значительно разные нагрузки на запросы, будет эффективно балансироваться по внутренним компонентам. Вот почему балансировка нагрузки L7 так важна для современных протоколов. (Балансировка нагрузки L7 дает также множество дополнительных преимуществ из-за возможности проверки трафика приложений, но более подробно это будет рассмотрено ниже).
Балансировка нагрузки L7 и модель OSI
Как я сказал выше в разделе о балансировке нагрузки L4, использование модели OSI для описания функций балансировки нагрузки проблематично. Причина в том, что L7 уже включает несколько дискретных уровней абстракции балансировки нагрузки (по крайней мере, как описано в модели OSI). Например, для HTTP-трафика рассмотрим следующие подуровни:
- Дополнительная безопасность транспортного уровня (TLS). Обратите внимание: люди до сих пор спорят о том, в какой OSI-уровень входит TLS. Ради этой дискуссии мы рассмотрим TLS L7.
- Физический протокол HTTP (HTTP/1 или HTTP/2).
- Логический HTTP-протокол (headers, body data, и trailers).
- Протокол обмена сообщениями (gRPC, REST, и т.д.).
Усовершенствованный балансировщик нагрузки L7 может предлагать функции, связанные с каждым из вышеперечисленных подслоев. Еще один балансировщик L7 может иметь только небольшой набор функций, которые помещают его в категорию L7. Короче говоря, ландшафт балансировки нагрузки L7 значительно сложнее с точки зрения сравнения функций, чем категория L4. (И, конечно, этот раздел только что затронул HTTP: Redis, Kafka, MongoDB и т.д. — всё это примеры протоколов приложений L7, которые выигрывают от балансировки нагрузки L7).
Функции балансировки нагрузки
В этом разделе я кратко расскажу о функциях высокого уровня, которые обеспечивают балансировки нагрузки. Не все балансировочные устройства обеспечивают все функции.
Обнаружение службы
Обнаружение служб — это процесс, с помощью которого балансировщик нагрузки определяет набор доступных бэкендов. Методы довольно разнообразны, и некоторые примеры включают:
- Статичный файл конфигурации.
- DNS. , Etcd, Consul, и т.д.
- Envoy’s универсальный data plane API.
Проверка работоспособности
Проверка работоспособности — это процесс, с помощью которого балансировщик нагрузки определяет, доступен ли бэкенд для обслуживания трафика. Проверка работоспособности обычно подразделяется на две категории:
- Активная: Балансировщик нагрузки периодически отправляет ping (например, HTTP-запрос конечной точке /healthcheck) на бэкенд и использует это для оценки работоспособности.
- Пассивная: Балансировщик нагрузки определяет состояние здоровья из основного потока данных. Например, балансировщик нагрузки L4 может решить, что бэкенд нездоровый, если в строке было три ошибки соединения. Балансировщик нагрузки L7 может решить, что бэкенд нездоровый, если в строке было три кода ответа HTTP 503.
Балансировка нагрузки
Да, балансировщики нагрузки должны фактически балансировать нагрузку! Учитывая набор здоровых бэкендов, как выбирается бэкенд, который будет обслуживать соединение или запрос? Алгоритмы балансировки нагрузки являются активной областью исследований и варьируются от упрощенных, таких как случайный выбор и циклический алгоритм, до более сложных алгоритмов, учитывающих переменную задержку и нагрузку на бэкенд. Один из самых популярных (учитывая его производительность и простоту) алгоритмов балансировки нагрузки известен как [power of 2 least request] (https://brooker.co.za/blog/2012/01/17/two-random.html).
Sticky sessions
В некоторых приложениях важно, чтобы запросы на один и тот же сеанс достигли одного и того же бэкенда. Это может быть связано с кешированием, временным сложным состоянием и т.д. Определение сеанса варьируется и может включать HTTP-файлы cookie, свойства клиентского соединения или какой-либо другой атрибут. У многих балансировщиков нагрузки L7 есть определенная поддержка липких сеансов. Отмечу, что липкость сеанса по своей сути является хрупкой (бэкенд-хостинг сеанса может умереть), поэтому будьте осторожны при разработке системы, которая опирается на них.
TLS-терминирование
Тема TLS и ее роль достойны отдельного поста. С учетом сказанного многие балансировочные устройства L7 выполняют обработку TLS, которая включает в себя терминирование, проверку сертификатов, фиксацию и т.д.
Возможность наблюдения
Как мне нравится говорить: «Наблюдение, наблюдение, наблюдение». Сети по своей сути ненадежны, и балансировщик нагрузки часто несет ответственность за экспорт статистики, следов и журналов, которые помогают операторам выяснить, что не так, чтобы они могли решить проблему. Балансировщики нагрузки сильно различаются по показателям наблюдаемости. Самые продвинутые балансировщики предлагают богатые возможности, которые включают числовые характеристики, распределенную трассировку и настраиваемое логирование. Я укажу, что усиленная наблюдаемость не является бесплатной; балансировщик нагрузки должен выполнить для этого дополнительную работу. Однако преимущества данных значительно перевешивают незначительные последствия для производительности сети.
Безопасность и предотвращение DoS
В частности, в топологии развертывания краев (см. ниже) балансировочные устройства часто реализуют различные функции безопасности, включая ограничение скорости, аутентификацию и смягчение DoS (например, маркирование и идентификацию IP-адреса, tarpitting и т.д.).
Конфигурация и плоскость управления
Балансировщики нагрузки нуждаются в настройке. В крупных развертываниях это может стать существенной задачей. Система, которая настраивает балансировщики нагрузки, известна как «плоскость управления» и широко варьируется в реализации. Для получения дополнительной информации по этой теме, пожалуйста, см. мое [сообщение о плоскости данных сетки обслуживания и плоскости управления] (https://medium.com/@mattklein123/service-mesh-data-plane-vs-control-plane-2774e720f7fc).
И еще многое другое
Этот раздел просто поцарапал поверхность типов функциональных возможностей, которые обеспечивают балансировщики нагрузки. Дополнительную информацию можно найти в разделе о балансировщиках L7 ниже.
Типы топологий балансировки нагрузки
Теперь, когда я предоставил высокоуровневое описание балансировки нагрузки, разницу между балансировщиками нагрузки L4 и L7 и краткое описание функций балансировки нагрузки, перейду к различным топологиям распределенных систем, в которых развернуты балансировщики нагрузки. (Каждая из следующих топологий применима как для балансировщиков нагрузки L4, так и для L7).
Прокси по середине

Топология балансировки нагрузки прокси по середине
Топология прокси по середине, показанная на рисунке выше, вероятно, является наиболее известным способом получения балансировки нагрузки для большинства читателей. Эта категория включает аппаратные устройства от Cisco, Juniper, F5 и т.д.; облачные программные решения, такие как Amazon ALB и NLB и Сloud Load Balancer; и чистые программные решения, такие как HAProxy, NGINX и Envoy. Достоинства подобной топологии — простота применения для пользователей. В общем случае пользователи подключаются к балансировщику нагрузки через DNS и не должны беспокоиться ни о чем другом. Соглашением решения прокси по середине является тот факт, что прокси (даже в случае кластеризации) является единственной точкой отказа, а также узким местом масштабирования. Прокси по середине также часто является черным ящиком, что затрудняет работу. Является ли наблюдаемая проблема у клиента? В физической сети? В прокси по середине? В бэкенде? Ответить на эти вопросы может быть очень сложно.
Краевой прокси

Топология балансировки нагрузки краевого прокси
Топология краевого прокси, показанная на рисунке, на самом деле является лишь вариантом топологии прокси по середине, в которой балансировщик нагрузки доступен через Интернет. В этом случае балансировщик нагрузки обычно должен предоставлять дополнительные функции «шлюза API», такие как терминирование TLS, ограничение скорости, аутентификация и сложная маршрутизация трафика. Все плюсы и минусы краевого прокси-сервера такие же, как и прокси по середине. Клиентам обычно требуется доступ к системе через DNS с использованием произвольных сетевых библиотек, которые владелец сервиса не контролирует (создание встроенной клиентской библиотеки или топологий прокси-сервера sidecar, описанных в следующих разделах, нецелесообразно запускать непосредственно на клиенте). Кроме того, по соображениям безопасности желательно иметь один шлюз, через который весь интернет-трафик проходит через систему.
Встроенная клиентская библиотека

Балансировка нагрузки через встроенную клиентскую библиотеку
Чтобы избежать присущих топологии сервера прокси по середине проблем отказа и масштабирования, более сложные инфраструктуры переключились на то, чтобы встроить балансировщик нагрузки непосредственно в службы через библиотеку, как показано на рисунке. Библиотеки сильно различаются по поддерживаемым функциям, но некоторые из наиболее известных и многофункциональных в этой категории — Finagle, [Eureka/Ribbon/Hystrix](https: //netflix.github.io/) и gRPC (на основе внутренней системы Google, называемой Stubby). Основным достоинством решения на базе библиотеки является то, что он полностью распределяет всю функциональность балансировщика нагрузки каждому клиенту, тем самым устраняя описанные ранее проблемы отказа и масштабирования. Первичным решением библиотечного решения является то, что библиотека должна быть реализована на всех языках, которые использует организация. Распределенные архитектуры становятся все более «полиглотными» (многоязычными). В этой среде стоимость переоснащения чрезвычайно сложной сетевой библиотеки на разных языках может стать непомерно высокой. Наконец, развертывание обновлений библиотек в большой архитектуре обслуживания может быть чрезвычайно болезненным.
Учитывая сказанное, упомянутые выше библиотеки были успешными для компаний, которые смогли ограничить распространение разных языков программирования и библиотек, тем самым преодолев проблемы обновления.
Прокси-сервер Sidecar

Балансировка нагрузки через прокси-сервер sidecar
Вариантом топологии балансировки нагрузки встроенной клиентской библиотеки является топология прокси-сервера sidecar, показанная на рисунке. В последние годы она была популяризирована как «служебная сетка». Идея прокси-сервера sidecar заключается в том, что ценой незначительного штрафа за латентность переходом на другой процесс все преимущества встроенного библиотечного подхода могут быть получены без программирования. Наиболее популярными балансировщиками нагрузки прокси-сервера sidecar на данный момент являются Envoy, NGINX, [HAProxy](https: //www.haproxy.com/) и Linkerd. Для более детального изучения подхода прокси-сервера sidecar см. мой пост в блоге о Envoy, а также мой пост [the service mesh data plane vs. control plane] (https://medium.com/@mattklein123/service-mesh-data-plane-vs-control-plane-2774e720f7fc).
Резюме и плюсы/минусы различных топологий балансировки нагрузки
- Топология прокси по середине — это, как правило, самая легкая топология балансировки нагрузки. Топология не соответствует одной точке отказа, ограничениям масштабирования и работе черного ящика.
- Топология краевого прокси-сервера похожа на сервер прокси по середине.
- Топология встроенной клиентской библиотеки обеспечивает лучшую производительность и масштабируемость, но обладает минусом — необходимостью реализовать библиотеку на разных языках и обновлять библиотеки для всех служб.
- Топология прокси-сервера sidecar не работает так же, как и встроенная топология клиентской библиотеки, но не страдает от каких-либо ограничений.
Думаю, топология прокси-сервера sidecar (служебная сеть) постепенно заменяет все другие топологии для связи между сервисами. Точка топологии краевого прокси-сервера всегда будет необходима до того, как трафик войдет в служебную сетку.
Современное состояние в балансировке нагрузки L4
Балансировщики нагрузки L4 по-прежнему актуальны?
В этом сообщении уже обсуждалось, насколько значимы балансировщики L7 для современных протоколов. Теперь более подробно перейдем к функциям балансировки нагрузки L7. Означает ли это, что балансировщики нагрузки L4 больше не актуальны? Нет! Хотя, на мой взгляд, балансировщики нагрузки L7 в конечном счете полностью заменяют балансировщики нагрузки L4 для связи между сервисами, балансировщики нагрузки L4 по-прежнему крайне важны, потому что почти все современные крупные распределенные архитектуры используют двухуровневую архитектуру балансировки нагрузки L4 / L7 для интернет-трафика. Преимущества размещения выделенных балансировщиков нагрузки L4 до балансировщика нагрузки L7 при развертывании на краю:
- Поскольку балансировочные устройства L7 выполняют значительно более сложный анализ, преобразование и маршрутизацию трафика приложения, они могут обрабатывать меньшую часть нагрузки (измеренной в пакетах в секунду и байтах в секунду), чем оптимизированный балансировщик нагрузки L4. Этот факт, как правило, делает балансировщики нагрузки L4 лучшим местом для обработки определенных типов DoS-атак (например, SYN-флуда, общих атак на передачу пакетов и т.д.).
- Балансировщики нагрузки L7, как правило, более активно развиваются, развертываются чаще и имеют больше багов, чем балансировщики нагрузки L4. Наличие спереди балансировщика нагрузки L4, который может выполнять проверку работоспособности и слив во время развертывания балансировщика нагрузки L7, значительно проще, чем применение механизмов развертывания с современными балансирами нагрузки L4, которые обычно используют BGP и ECMP (подробнее об этом ниже). И, наконец, поскольку балансировщики L7 с большей вероятностью имеют ошибки из-за сложности их функциональности, наличие балансировщика нагрузки L4, который может маршрутизировать ошибки и аномалии, приводит к более стабильной общей системе.
В следующих разделах я опишу несколько различных конструкций для балансировщика нагрузки L4 для прокси по середине и краевого прокси. Следующие проекты, как правило, неприменимы к клиентской библиотеке и топологиям прокси-сервера sidecar.
TCP/UDP балансировщик нагрузки

L4 балансировщик нагрузки
Первым типом балансировщика нагрузки L4, который все еще используется, является балансировка нагрузки на выключение, показанная на рисунке. Это тот же самый балансировщик нагрузки, который мы видели в введении к балансировке нагрузки L4 выше. В этом типе используются два дискретных TCP-соединения: один между клиентом и балансировщиком нагрузки и один между балансировщиком нагрузки и бэкендом.
Балансировщики нагрузки L4 по-прежнему используются по двум причинам:
- Их относительно просто реализовать.
- Прекращение соединения в непосредственной близости (низкая латентность) к клиенту имеет существенные последствия для производительности. В частности, если конечный балансировщик нагрузки можно разместить рядом с клиентами, которые используют сеть с потерями (например, сотовую), повторные передачи, скорее всего, будут происходить быстрее до того, как данные будут перемещены в надежный транзит по маршруту до конечного местоположения. Другими словами, этот тип балансировщика нагрузки может использоваться в сценарии Point of Presence (POP) для прерывания TCP-соединения.
Отказоустойчивость через пары высокой доступности

L4-отказоустойчивость через пары HA и отслеживание соединений
До сих пор мы рассматривали дизайн балансировщика нагрузки L4 изолированно. Для балансировки нагрузки Passthrough и DSR требуется некоторое отслеживание соединения и состояния в самом балансировщике нагрузки. Что делать, если балансировщик нагрузки умирает? Если один экземпляр балансировщика умирает, все проходящие через него соединения будут разорваны. В зависимости от приложения это может существенно повлиять на производительность.
Исторически балансировщики нагрузки L4 были аппаратными устройствами, приобретенными у типичных поставщиков (Cisco, Juniper, F5 и т.д.). Эти устройства чрезвычайно дороги и обрабатывают большой объем трафика. Чтобы избежать отказа одного балансировщика нагрузки, разрушающего все соединения и приводящего к существенному отключению приложений, балансировщики обычно развертывались в парах с высокой доступностью, как показано на рисунке. Типичная установка балансировки нагрузки HA имеет следующий дизайн:
- Пара пограничных маршрутизаторов HA обслуживает некоторое количество виртуальных IP-адресов (VIP). Эти пограничные маршрутизаторы объявляют VIP-клиентов, используя протокол пограничных шлюзов (BGP). Основной пограничный маршрутизатор имеет более высокий вес BGP, чем резервный, поэтому в рабочем состоянии он обслуживает весь трафик. (BGP — чрезвычайно сложный протокол, для целей этой статьи просто представляйте BGP как механизм, с помощью которого сетевые устройства сообщают, что они доступны для приема трафика с других сетевых устройств, и учитывайте, что каждая ссылка может иметь вес, который определяет приоритетность трафика).
- Аналогично, основной балансировщик нагрузки L4 объявляет себя граничным маршрутизатором с более высоким значением BGP, чем резервный, поэтому в рабочем состоянии он обслуживает весь трафик.
- Первичный балансировщик нагрузки перекрестно подключен к резерву и разделяет состояние отслеживания соединений. Таким образом, если первичный балансировщик умирает, резервный может взять на себя обработку всех активных подключений.
- Два пограничных маршрутизатора и два балансировщика нагрузки связаны друг с другом. Это означает, что если один из пограничных маршрутизаторов или один из балансировщиков нагрузки умирает или имеет собственные объявления маршрутов BGP по какой-либо другой причине, резервный может взять на себя обслуживание всего трафика.
Вышеприведенная настройка показывает, насколько много трафика по-прежнему обслуживается. Однако у такого подхода есть существенные недостатки:
- VIP должны быть правильно распределены по парам балансировки нагрузки HA с учетом использования мощности.
- Использование ресурсов системы невелико. 50% мощности находится в режиме ожидания. Учитывая, что аппаратные балансировщики нагрузки чрезвычайно дороги, это приводит к существенному количеству «простаивающих» денег.
- Современному дизайну распределенной системы нужна большая отказоустойчивость, чем могут обеспечить активный/резервный балансировщики. Например, система должна выдерживать множественные одновременные сбои и продолжать работать. Пара балансировщиков нагрузки HA подвержена общему сбою, если одновременно активизируются как активный, так и резервный балансировщик нагрузки.
- Собственные крупные аппаратные устройства от поставщиков чрезвычайно дороги и приводят к привязке к поставщику. Как правило, желательно заменить эти аппаратные устройства горизонтально масштабируемыми программными решениями, построенными с использованием обычных серверов.
Отказоустойчивость и масштабирование с помощью кластеров с распределенным консистентным хешированием

L4-отказоустойчивость и масштабирование с помощью кластерных балансировщиков нагрузки и консистентного хеширования
В предыдущем разделе была представлена ошибка отказоустойчивости балансировки нагрузки L4 через пары HA, а также указаны присущие этой конструкции проблемы. Начиная с середины 2000-х годов крупные интернет-инфраструктуры начали разрабатывать и развертывать новые массивно-параллельные системы балансировки нагрузки L4, как показано на рисунке. Целями этих систем являются:
- Снижение всех недостатков конструкции пары HA, описанных в предыдущем разделе.
- Уход от проприетарных балансировщиков оборудования в сторону программных решений, построенных с использованием обычных серверов и сетевых адаптеров.
Этот дизайн балансировки нагрузки L4 чаще всего называют отказоустойчивостью и масштабированием посредством кластеризации и распределенного согласованного хеширования. Он работает следующим образом:
- N пограничных маршрутизаторов объявляют все VIP Anycast с одинаковым весом BGP. Равнозначный multi-path-роутинг (ECMP) используется для обеспечения того, чтобы в общем случае все пакеты из одного потока поступали на один и тот же пограничный маршрутизатор. Поток, как правило, представляет собой кортеж источника IP/порта и IP-адреса назначения. (Короче говоря, ECMP — это способ распространения пакетов по набору одинаково взвешенных сетевых ссылок с использованием консистентного хеширования). Хотя сами пограничные маршрутизаторы не особо заботятся о том, какие пакеты поступают в поток, предпочтительно, чтобы все пакеты из потока пересекали один и тот же набор ссылок, чтобы избежать сбоев пакетов, которые ухудшают производительность.
- N машин для балансировки нагрузки L4 объявляют все VIP с одинаковым весом BGP к пограничным маршрутизаторам. Опять же, используя ECMP, пограничные маршрутизаторы обычно выбирают одну и ту же машину балансировки нагрузки для потока.
- Каждая машина балансировки нагрузки L4 обычно выполняет частичное отслеживание соединения, а затем использует консистентное хеширование, чтобы выбрать бэкенд для потока. GRE используется для инкапсуляции пакетов, отправленных из балансировщика нагрузки в бэкенд.
- Затем DSR используется для отправки пакетов непосредственно из бэкенда клиенту через пограничные маршрутизаторы.
- Консистентный алгоритм хеширования, используемый балансировщиком нагрузки L4, является областью активных исследований. Существуют компромиссы в первую очередь вокруг выравнивания нагрузки, минимизации задержки, минимизации сбоев во время изменений бэкенда и минимизации издержек памяти. Полное обсуждение этой темы выходит за рамки этой статьи.
Давайте посмотрим, как приведенный выше пример смягчает все недостатки подхода пары HA:
- По мере необходимости могут добавляться новые пограничные маршрутизаторы и устройства балансировки нагрузки. Консистентное хеширование используется на каждом уровне для максимального уменьшения количества затронутых потоков при добавлении новых машин.
- Использование ресурсов системы может выполняться на как можно более высоком уровне при сохранении достаточной отказоустойчивости.
- Как пограничные маршрутизаторы, так и балансировщики нагрузки теперь могут быть построены с использованием обычного оборудования с небольшой долей стоимости традиционных аппаратных балансировщиков (подробнее об этом ниже).
Вопросы, которые обычно задают об этой конструкции: — «почему пограничные маршрутизаторы не разговаривают напрямую с серверами через ECMP?», «зачем нам нужен балансировщик нагрузки?». Причины в первую очередь связаны с уменьшением эффективности DoS и операционной системой. Без балансировки нагрузки каждый бэкенд должен был бы участвовать в BGP и потребовалось бы значительно большее время для развертывания.
Все современные системы балансировки нагрузки L4 двигаются к этой конструкции (или к ее варианту). Двумя наиболее известными примерами являются Maglev из Google и [Балансировщик сетевой нагрузки (NLB)](http: //docs.aws.amazon.com/elasticloadbalancing/latest/network/introduction.html) от Amazon. В настоящее время нет балансировщика нагрузки OSS, который реализует этот проект, однако есть компания, которая планирует выпуск OSS в 2018 году. Я очень взволнован этим выпуском, поскольку современный балансировщик нагрузки L4 является решающим элементом отсутствия OSS в сетевом пространстве.
Текущее состояние балансировки нагрузки L7
The proxy wars in tech currently is *quite literally* the proxy wars.
Or the “war of the proxies”.
Nginx plus, HAProxy, linkerd, Envoy all quite literally killing it.
And proxy-as-a-service/routing-as-a-service SaaS vendors raising the bar as well. Very interesting times!
— Cindy Sridharan (@copyconstruct) November 28, 2017
Да, в самом деле. За последние несколько лет наблюдается возрождение в балансировщиках нагрузки/прокси L7. Это очень хорошо сочетается с продолжающимся стремлением к микросервисной архитектуре в распределенных системах. Кроме того, рост автомасштабирования, планировщики контейнеров и т.д. означает, что дни предоставления статических IP-адресов в статических файлах давно прошли. Системы не только используют сеть активнее, они становятся значительно более динамичными, требуя новых функциональных возможностей в балансировщиках нагрузки.
Поддержка протокола
Современные балансировщики нагрузки L7 добавляют явную поддержку для многих разных протоколов. Чем большее отношение балансировщик нагрузки имеет к трафику приложения, тем более сложные вещи он может делать в отношении вывода наблюдаемости, расширенной балансировки нагрузки, маршрутизации и т.д. Например, на момент написания этой статьи Envoy явно поддерживает парсинг и маршрутизацию протокола L7 для HTTP/1, HTTP/2, gRPC, Redis, MongoDB и DynamoDB. В будущем, вероятно, появятся новые протоколы, включая MySQL и Kafka.
Динамическая конфигурация
Как описано выше, все более динамичный характер распределенных систем требует параллельных инвестиций в создание динамических и реактивных систем управления. Istio — один из примеров такой системы. Для получения дополнительной информации по этой теме см. мой пост service mesh data plane vs. control plane.
Расширенная балансировка нагрузки
Балансировщики L7 обычно имеют встроенную поддержку расширенных функций балансировки нагрузки, таких как тайм-ауты, повторные попытки, ограничение скорости, прерывание цепи, теневое копирование, буферизация, маршрутизация на основе контента и т.д.
Наблюдаемость
Как описано выше в разделе об общих характеристиках балансировки нагрузки, все более динамичные системы становится все труднее отлаживать. Надежная выходная характеристика протокола — это, пожалуй, самая важная особенность, обеспечиваемая современными балансировщиками L7. Вывод числовой статистики, распределенных трассировок и настраиваемого ведения журнала теперь практически необходим для любого решения балансировки нагрузки L7.
Расширяемость
Пользователи современных балансировщиков L7 часто хотят легко расширять их, чтобы добавить пользовательские функции. Это можно сделать путем записи подключаемых фильтров, которые загружаются в балансировщик нагрузки. Многие балансировки нагрузки также поддерживают сценарии, как правило, через Lua.
Отказоустойчивость
Немного выше я написал о L4-отказоустойчивости балансировки нагрузки. А что насчет отказоустойчивости балансировки нагрузки L7? Мы рассматриваем балансировщики нагрузки L7 как расходный материал и stateless. Использование программного обеспечения для балансировки позволяет L7 легко масштабироваться горизонтально. Кроме того, обработка и отслеживание состояния, которые выполняют балансировщики нагрузки L7, существенно сложнее, чем L4. Попытка построить HA-пару балансировщика нагрузки L7 технически возможна, но это будет тяжело реализовать.
Как в доменах балансировки нагрузки L4 и L7, отрасль в целом отходит от HA-пар к горизонтально масштабируемым системам, которые взаимодействуют через консистентное хеширование.
И еще
Балансировщики нагрузки L7 развиваются ошеломляющими темпами. Пример того, что предлагает Envoy, см. в разделе Обзор архитектуры.
Глобальная балансировка нагрузки и централизованная плоскость управления

Глобальная балансировка нагрузки
Будущее балансировки нагрузки будет все чаще рассматривать отдельные балансировщики как товар. На мой взгляд, настоящие инновационные и коммерческие возможности лежат в плоскости управления. Рисунок показывает пример глобальной системы балансировки нагрузки. В этом примере происходит несколько разных вещей:
- Каждый прокси-сервер sidecar связывается с серверами в трех разных зонах (A, B и C).
- Как показано, 90% трафика отправляется в зону C, а 5% трафика отправляется в обе зоны A и B.
- Прокси-сервер sidecar и внутренние серверы сообщают о периодическом состоянии глобального балансировщика нагрузки. Это позволяет ему принимать решения, учитывающие задержки, затраты, нагрузку, текущие сбои и т.д.
- Глобальный балансировщик нагрузки периодически настраивает каждый прокси-сервер sidecar с текущей информацией о маршрутизации.
Глобальный балансировщик нагрузки будет в состоянии делать сложные вещи, которые ни один балансировщик нагрузки не может сделать самостоятельно. Например:
- Автоматическое обнаружение и маршрутизация вокруг зонального отказа.
- Применение глобальных политик безопасности и маршрутизации.
- Обнаружение и уменьшение аномалий трафика, включая атаки DDoS, с использованием машинного обучения и нейронных сетей.
- Обеспечение централизованного пользовательского интерфейса и визуализации, которые позволяют инженерам понимать всю распределенную систему в совокупности и управлять ей.
Чтобы сделать глобальную балансировку нагрузки возможной, балансировщик нагрузки, используемый в качестве data plane, должен обладать сложными возможностями динамической конфигурации. Для получения дополнительной информации по этой теме, пожалуйста, см. мой пост Envoy’s universal data plane API, а также service mesh data plane vs. control plane..
Эволюция от аппаратного до программного обеспечения
До сих пор этот пост только кратко упоминал аппаратное и программное обеспечение, прежде всего в контексте исторической пары балансировки нагрузки L4. Каковы отраслевые тенденции в этой области?
I’ve seen the new OSI stack of eight layers of software.
I think it’s more like this: pic.twitter.com/6K84IJYGAi
— Brian Glas (@infosecdad) July 21, 2017
Заключение и будущее балансировки нагрузки
Подводя итог, приведем ключевые выводы этой публикации:
- Балансировщики нагрузки являются ключевым компонентом в современных распределенных системах.
- Существуют два общих класса балансиров нагрузки: L4 и L7.
- Оба балансировщика нагрузки L4 и L7 актуальны в современных архитектурах.
- L4-балансировщики двигаются в направлении горизонтально масштабируемых распределенных консистентных решений хеширования.
- В связи с распространением динамических архитектур микросервиса сейчас активно инвестируются балансировщики L7.
- Глобальная балансировка нагрузки и разделение между control и data plane — это будущее балансировки нагрузки с инновационными и коммерческими возможностями.
- Индустрия агрессивно продвигается к аппаратным и программным продуктам OSS для сетевых решений. Я считаю, что традиционные поставщики балансировщиков нагрузки, такие как F5, будут сначала заменены программным обеспечением OSS и облачными поставщиками.
Я думаю, что это захватывающее время в компьютерных сетях! Переход к OSS и программному обеспечению для большинства систем увеличивает темпы итерации на порядок. Кроме того, поскольку распределенные системы продолжают свой путь к динамизму с помощью «бессерверных» парадигм, сложность базовой сети и систем балансировки нагрузки должна быть соразмерно увеличена.
ECMP и превратности балансировки на сетевом оборудовании
Такой привычный и понятный ECMP. Знакомый каждому сетевику с младенческого возраста. Ну о чём тут ещё можно говорить? ECMP особо не вызывает ни теоретических ни практических вопросов в сетях операторов и в энтерпрайзе, где счёт различным путям идёт на единицы. А вот датацентры дышат ECMP, и тут уже приходится считаться: например, закупая ToR-коммутаторы, стоит в спеке обратить внимание на количество ECMP-групп и общее количество доступных некстхопов, а для бордеров — на поддержу Resilient Hashing.
Содержание

Что, по сути своей, такое ECMP? Это балансировка трафика по равноценным путям. Разберёмся сначала со словом «балансировка».
Балансировка нагрузки
А давайте-ка выровняем информационное поле? В такой сети, как ниже, 2a03:21c0:0:20f::105/128 — это маршрут, а 0, 1, 2 и 3 — это возможные пути.
Далее, во всей статье именно это я и буду иметь в виду, говоря «маршрут» и «путь». «Путь» и «некстхоп», при этом, будут в данном контексте синонимами. А ещё иногда буду называть их бакетами — это термин из теории хэширования.
Она может осуществляться либо по пакетам, либо по потокам.
Первый способ — per packet — предполагает примитивный round robin: один пакет в один линк, другой — в другой, и т.д. по кругу. Он давно себя дискредитировал: даже в идеальном случае, задержки доставки будут отличаться, и на получателях будет перманентный реордеринг пакетов. И вроде бы TCP создан с ним справляться, но реордеринг заставляет дольше хранить сегменты в буферах, что может снижать скорость и негативно влиять на приложение. Ну и кроме TCP, у нас есть, как минимум, UDP или какой-нибудь CES, которые устроят вам вакханалию в самый неподходящий момент (всегда). В реальной эксплуатации едва ли вы где-то увидите пакетную балансировку.
Второй — балансировка по потокам (per flow). Тут всегда пакеты одной сессии отправляются по одному и тому же пути. То есть, в случае TCP/UDP учитывает (SIP, DIP, Proto, SPort, DPort) — так называемый 5-Tuple. Для каждого пакета высчитывается хэш на основе этих полей. На основе этого хэша трафик отправляется по тому или иному пути. Для одного и того же 5-Tuple всегда будет получаться один и тот же хэш, соответственно, выбираться один и тот же путь. В простейшем случае формула выбора пути следующая: hash % path_count — или, иными словами, остаток от деления хэша на число доступных путей. Например, есть 4 пути и 5 TCP-потоков. Результаты хэш-функции: 4, 8, 15, 16 и 23. Остатки от деления на 4: 0, 0 и 3, 0 и 3. Соответственно, потоки разложатся так:

При большом числе потоков, они сравнительно равномерно раскладываются по путям.
Описанный подход называется статическим хэшированием, поскольку независимо от обстоятельств чип применят хэш-функцию на заголовки пакетов и возвращает путь, на который указал остаток от деления.
Особенности статического хэширования
Статическое хэширование замечательно работает на статической же сети, в которой ничего не флапает. Однако, стоит убрать или добавить путь, как вся таблица пересчитается. Разберёмся на примере. У Вани было 4 некстхопа и 5 потоков. На основе простейшего алгоритма по остатку от деления потоки распределяются так:

Маша забирает у Вани 1 поток, выдернув провод. И вот как меняется распределение:

Все потоки перераспределились. В случае операторов связи, нас это мало волнует — ну, может, задержки немного изменятся. На фабрике в ДЦ (например, спайн выключился) для обычных маршрутов — аналогично.
Anycast
Ситуация кардинально меняется, когда речь заходит об Anycast»е. Напомню, что это один и тот же префикс, обычно /32, который анонсируется разными устройствами. И трафик на данный префикс отправляется к ближайшему устройству. То есть, запросы к одному IP-адресу могут фактически обрабатываться разными серверами, на которых он настроен. Такая конфигурация используется для балансировки нагрузки. И только таким способом можно обслужить терабиты трафика и миллиарды пользователей, когда имеется максимум пара 25Гб/с порт на одном сервере. Заводим 100 бэкендов за одним IP-адресом и получаем возможность обработать терабит трафика.
Дополнительные плюсы anycast — трафик всегда идёт на ближайшие хост с этим адресом, что уменьшает задержку и общую утилизацию сети — ведь трафику нужно пройти меньший путь. Если на бордере есть несколько равноценных путей на один адрес, он будет по ECMP слать часть запросов по одному пути, часть — по другому. Так они и будут попадать на разные сервера. Допустим, у нас в датацентре есть 4 L3/L4-балансировщика трафика, анонсирующих один и тот же IP-адрес, за которым скрывается web-сервис. Каждый из них отправляет полученные запросы на свой пул реальных серверов.

В этой ситуации перераспределение потоков при выпадении одного балансировщика будет означать, что запросы будут прилетать на сервера, на которых нет установленной сессии, а значит — это ресет сессии, переустановка и весь процесс сначала. То есть была, например, загрузка многогигабайтного файла в хранилище, осталось пара мегабайтов, моргнул балансировщик — и всё! То же самое и при добавлении нового балансировщика. И, конечно, это не только недовольство пользователей, но и резкий рост нагрузки на мощности — переустановление сессий, повторные операции. Возрастает риск получить тут масштабное каскадное падение. С этой проблемой призван справиться механизм Resilient Hashing.
Resilient/Consistent Hashing
Читается, как ризилиент! Били ограничения статического хэширования по всем и больно, поэтому некий Каргер в MIT описал такой алгоритм хэширования, который позволял с минимальными нарушениями переживать изменение количества бакетов. Под бакетами (или слотами) в нашем случае будем иметь в виду доступные пути. И вот как это достигается.
Представим себе окружность. Она разбита на M делений. M — заведомо большое число (типа 2^32).

У нас N бакетов (путей). Каждому из этих N бакетов сопоставлено какое-то число в диапазоне [o, M], то есть они рассредоточены по окружности.

Ключи (хэши от 5-Tuple) тоже находятся в диапазоне [0, M] и тоже наносятся на эту же окружность.

Следующий ближайший к ключу бакет ставится ему в соответствие и заносится в таблицу. Если следующее значение больше M — начинаем с 0.

В чём разница со статическим хэшированием, спросите вы? А давайте посмотрим.
Сценарий 1. Удаление пути
Допустим, путь №21 исчезает. Тогда с окружности удаляется эта точка, а все сессии, перебегают на путь №29.

При этом, остаются неизменными все сессии, что лежат через пути №5, 12 и 29. То есть, фактически, дёрнутся только те, которые и так дёрнулись бы.
Сценарий 2. Добавление пути
Теперь добавляется путь №17. Тогда все сессии, что находятся между путями 12 и 17, которые до этого шли через путь 29, перенаправятся в путь №17.

И опять же ничего не случится с теми, которые идут через пути 5 и 12. И будет затронута только часть маршрутов, идущих через 29. Безусловно, это простая реализация, в которой, например, не поддерживается равномерность балансировки, но она даёт представление, как реализуется Resilient Hashing. С фактической его работой на сетевом оборудовании не всё так просто, поэтому не все устройства его поддерживают. А те, что поддерживают, дают лишь ограниченное число записей.
Поляризация трафика при балансировке
Вернёмся к ECMP. Забавно, что самая что ни на есть естественная сторона хэш-функции — выдавать одинаковый результат для одинаковых входных данных — может стать и помехой.

Именно это и происходит в случае последовательных балансировок. Трафик пришёл на SW0, разбалансировался между SW10 и SW11. А теперь его ещё и SW10 должен побалансировать. Ирония в том, что на SW10 трафик уже пришёл с определённым (отобранным) 5-Tuple, то есть поляризованный. И если на него натравить ту же хэш-функцию, то, предсказуемо, весь трафик разложится в одно плечо, а во второе — не попадёт ничего. Для выхода из этой ситуации в работу хэш-функции вносятся изменения. Они могут называться и работать по-разному: Salt, Seed, Shift — но их задача одна: выдать результат отличный от того, что получится на других устройствах.
Обычно этот Seed/Salt/Shift генерируется случайным образом (или нет) при первом запуске устройства, и далее не меняется. Но тут всё зависит сугубо от вендора. Это опять же (забавно) идёт вразрез с предыдущим параграфом про эникаст, где нам хотелось иметь одинаковый результат на разных устройствах. Но тут тогда неплохо придерживаться такой политики: Shift/Salt/Seed один и тот же для устройств одного уровня, и разный — для разных. Если, конечно, производитель вообще позволяет настраивать.
Ещё про Anycast
Тот, кто нам мешает, тот нам и поможет. Когда мы говорим про Anycast, есть интересные нюансы. Например, два балансировщика трафика — LB0 и LB1 — подключены в два разных Leaf-коммутатора. Каждый балансер отвечает за свою группу реальных бэкендов. Есть одна сессия, которая по своему хэшу пошла на Leaf0 и, соответственно, попала на LB0.

И вдруг падает линк Leaf0<->Spine0. Трафик сессии перетекает на Spine1, где от его 5-Tuple считается хэш. И вот, если хэш-функция одна и та же, и порты подключения Leaf0 и Leaf1 одни и те же, то сессия автоматически снова ляжет через Leaf0 на LB0 и, конечно, тот же самый реальных бэкенд.

Но если хэш получился другой, или соответствие путей хэшам получилось иным, то трафик сессии может попасть на LB1, а значит, другие бэкенды — и получим разрыв.

То есть, для этого сценария нам полезно использовать одни и те же порты для подключения Leaf к Spine.
MPLS Load Balancing
Не IP единым, как говорится. Много где бегает MPLS. Возможно, это не совсем относится к датацентровым сетям, но мы же не только про них говорим.
В MPLS-метке не так уж много разнообразия. Она указывает или на некстхоп, или на операции демультиплексирования/коммутации, и ничего не знает о внутренних протоколах пакета. Так, транспортная одинакова для всего трафика, идущего на конкретный FEC. Сервисная имеет чуть больше деталей. Однако и она скрывает за собой или вообще весь трафик одного VPN, или, в лучшем случае (если это L3), весь префикс. Понимая это, производители научили свои чипы заглядывать под заголовки MPLS. Но лишь под определённое их число. Кто-то может заглянуть под 3, кто-то — под 4. Максимум — под 5, не больше. Поэтому, если у вас на сети Carrier Supports Carrier с каким-нибудь TE Fast Reroute, вам придётся быть очень аккуратным при планировании балансировки.
Но как понять, что нагрузка под заголовком MPLS — IP-пакет? Очень просто: анализируем первые 4 бита. Если в них 0x4 или 0x6, значит это IPv4 или IPv6 — и тогда берётся стандартный 5-Tuple (SIP, DIP, Protocol, SPort, DPort). Это делают все вендоры и не стесняются в этом признаться. Любой другой способ категоризации нижележащего протокола будет несравнимо дороже. Если дальше идёт Ethernet, то следующим полем будет DMAC. И, как бы тут, упаси боже, чтобы DMAC начинался на 0х4/0x6 — тогда такую шикарную балансировку можно получить, что свет белый не взвидишь. Поэтому для сервисов L2VPN может добавляться Control Word, который гарантирует, что в первых четырёх битах не будет 0x4/0x6.
- Сколько-то битов нагрузки (например, 16).
- Самую нижнюю метку.
- Две нижние метки.
- Весь стек меток.
- Любые комбинации пунктов выше.
И всё же, что делать, если меток больше трёх, если тип нагрузки неизвестен и P-маршрутизатор не может определить, как балансировать? На помощь спешит Entropy Label RFC6790.
Entropy Label
Entropy Label (EL) — специальная метка, несущая информацию о нагрузке, позволяющая побалансировать MPLS-трафик более гранулярно и не допустить реордеринга. Грубо говоря, для каждой TCP-сессии будет генерироваться собственная EL и вставлять в стек меток. Поскольку в заголовке MPLS никакого специального флага указания, для чего метка служит, нет, прямо над EL в стеке меток вставляется ещё одна — ELI — Entropy Label Indicator — это зарезервированная метка 7. То есть, любое транзитное устройство, видящее метку 7, или понимает, что это ELI, или не понимает, и тогда не обращает внимания.

Если устройство понимает ELI, то знает, что прямо под ней — EL, считает хэш от метки EL и балансирует трафик на основе него. И уже не нужно делать DPI до, собственно, тела пакета. Если же не понимает, то для него 7 — это просто какая-то метка. Если необходимо, заглядывает по возможности в нагрузку и балансирует на основе этой информации. И просто свопает транспортную метку, оставляя ELI/EL в стеке. Egress, очевидно, должен в любом случае уметь разбираться с EL/ELI.
Как это работает? При сигнализации туннеля Egress LSR сообщает Ingress LSR»у, что он умеет в Entropy Label и их можно ему слать (а можно и не слать) (в LDP это Entropy Label Capability, но вообще любой протокол распространения меток это умеет). Ingress LSR, в свою очередь, знает о нагрузке гораздо больше, чем любой транзитный LSR, поэтому он лучше понимает, как можно трафик балансировать. Кроме того, он работает на более низких скоростях, чем транзитные, и может позволить себе больше операций над пакетом. Поэтому Ingress LSR получает пакет и вешает сервисную метку. Далее, понимая, что Egress LSR готов обработать EL, формирует EL, сверху приклеивает ELI, потом транспортную — и в дальний путь. Соответственно, как я уже сказал выше, транзитные LSR могут обработать пакет независимо от того, поддерживают они EL или нет.

Мыши и слоны
Вопрос «Elephant vs Mice flows «стоит почти столько же, сколько существуют сети передачи данных. Одновременно с короткими TCP-сессиями открытия linkmeup.ru через сеть провайдера идут долгие тяжёлые закачки. Одновременно с HTTP-запросом на объект в Object Storage в сети ДЦ идёт массивная репликация БД. Но в сети провайдера по статистике количество одновременных сессий лежит в диапазоне от нескольких тысяч до десятков и сотен тысяч, среди которых большое количество и Mice, и Elephant flows. И это всё по ECMP может раскладываться сравнительно равномерно. В сети ДЦ ситуация радикально другая — количество сессий может быть и меньше тысячи, среди них буквально единицы-десятки Elephant flows. При статической балансировке велика вероятность, что массивные потоки лягут в один интерфейс и создадут очень неравномерную балансировку. Per-packet балансировка позволяет одинаково нагрузить все линки, но выливается в реордеринг. Как бы нам и один поток разложить в разные линки, и пакеты при этом доставлять упорядоченно? На помощь приходит статистика. Оказывается, что на маленьком масштабе времени TCP имеет взрывной характер — то есть, всплеск-пауза-всплеск-пауза-и т.д. Такие отдельные всплески внутри одного flow назвали flowlet.

И вот, если пауза между двумя соседними флоулетами больше определённого порога, то их можно безопасно отправить в разные линки, таким образом эффективно разложив один поток на несколько путей. Что же до длины паузы, то она должна быть больше, чем возможная разница во времени доставки пакетов по различным путям ECMP. Например, если эта разница лежит в диапазоне 300-500 мкс, то 1 мс паузы должно быть достаточно, чтобы исключить любой реордеринг.
Juniper называет эту технологию AFS — Adaptive Flowlet Splicing. Соответственно, при выборе пути теперь учитывается не только статический хэш от заголовков пакета, но и загрузка интерфейсов и заполненность очереди. Чем менее загружен интерфейс и меньше пакетов в очереди, тем более вероятно, что при следующей возможности флоулет переключится в него.
Чем дальше в детали, тем ближе к чипо-вендорному разнообразию, но, если в общих словах, то реализуется это примерно так. Для того, чтобы отслеживать паузы, для каждого потока вводится элемент, называемый Hash Bucket Table. В каждой записи в нём есть поле с меткой времени, когда последний раз через него прогонялся пакет, и поле с некстхопом. При получении нового пакета, чип эту метку сравнивает с заданным временны́м порогом: если больше — то можно назначать новый интерфейс, если меньше, то слать в уже указанный в записи. По схожим принципам работает динамическая приоритизация пакетов (DPP — Dynamic Packet Prioritization). Но это уже совсем другая история.
Теперь мы готовы обратиться к слову «равноценный» во фразе «Что, по сути своей, такое ECMP? Это балансировка трафика по равноценным путям».
Equal Cost
- Weight/Preference/Metric/etc.
- Local Preference.
- AIGP.
- Длина AS Path.
- Origin.
- MED.
- EBGP или IBGP.
Зачастую, ECMP обычно выключен по умолчанию, и его нужно включать, явно указывая максимальное число путей для балансировки. Если число фактически доступных ECMP-путей больше, чем настроено, то к ним начинают применяться примерно те же критерии выбора лучшего, что и обычно, чтобы оставить только настроенное количество. В результате, из протокола в RIB импортируется набор равноценных путей. А из RIB они уже инсталируются в FIB и в ASIC»и/память. В некоторых случаях для такого экспорта нужна ещё отдельная конфигурация (так на джунипере это применение политики в секции routing-options -> forwarding-table). Каждый раз, как приходит на чип пакет, происходит лукап маршрута. Если чип находит маршрут указывает на ECMP-группу, он рассчитывает хэш от заголовков, согласно настройкам (2-Tuple, 5-Tuple). На основе результата выбирается соответствующий путь из группы.

Всё просто в первом приближении. Однако, тут на сцену выходит гонка за ресурсами и оптимизацией. В случае мощной фабрики на каждый маршрут легко иметь 8 и больше некстхопов. Если счёт маршрутов идёт на тысячи и десятки тысяч, очень легко упереться в ресурсы аппаратного FIB, поэтому прибегают к ухищрениям. Так появилась концепция ECMP-групп.
ECMP Groups
Если вы попытаетесь найти в Интернете описание того, что же такое ECMP-группы, то самое богатое, что встретите: ECMP-группа — это список уникальных некстхопов, на который ссылаются ECMP-маршруты (дословно с сайта Cumulus: «An ECMP group is a list of unique next hops that are referenced by multiple ECMP routes»). Хотите больше — это уже Programmer»s Guides для конкретных чипов с водяными знаками на страницах, подписями «Confidential» и открывающиеся по личному паролю. Пожалуй, этого краткого определения и достаточно.
Или нет? Мне лично оно непонятно. И каждый раз, как я на него смотрю, мне всё так же непонятно. Попробую сделать пару абзацев ECMP Groups Explained. И давайте для примера возьмём вот такую замысловатую сетоньку.

Как видите, в ДЦ слева 4 спайна. Соответственно, с каждого лифа по 4 аплинка, то есть 4 некстхопа для маршрутов. Допустим, из другого ДЦ прилетает 100 маршрутов. В стабильной ситуации у каждого из 100 маршрутов 4 возможных некстхопа. У всех 100 — одинаковый список. Это означает, что все они формируют одну ECMP-группу.

То есть, в FIB есть структура, содержащая 4 некстхопа, на которую указывают 100 маршрутов.
Когда происходит лукап адреса назначения в FIB, возвращается именно эта ECMP-группа. На основе заголовка пакета вычисляется значение хэш-функции, и для этого конкретного пакета выбирается определённый маршрут из этой группы и, соответственно, набор действий, которые нужно осуществить над пакетом.
Ещё раз: вместо того, чтобы хранить 4 некстхопа для каждого из 100 маршрутов, мы храним всего одну группу и ссылаемся на неё. Кроме сохранения ресурсов концепция ECMP-групп позволяет очень быстро вносить изменения в FIB — мы правим всего лишь одну запись, вместо перепрограммирования каждого маршрута.
Теперь добавим хаоса: в удалённом ДЦ вылетает линк между лиф-коммутатором и спайном, через который анонсировалось прежде 10 маршрутов. Теперь на локальной стороне 90 маршрутов доступны через всё те же 4 некстхопа, а 10 — через 3.

Сразу после этого появляется вторая ECMP-группа. Первая состоит из тех же 4, и на неё ссылаются 90 маршрутов. Вторая состоит из 3 некстхопов, и на неё ссылаются 10 маршрутов. Соответственно, если выйдет из строя ещё один лиф на удалённой стороне, анонсировавший 20 маршрутов, то на локальных лифах будет три группы: Первая — 4 некстхопа, ссылаются 70 маршрутов Вторая — 3 некстхопа, ссылаются 10 маршрутов. Третья — другие 3 некстхопа, ссылаются другие 10 маршрутов.

Несмотря на то, что число некстхопов во второй и третьей группах одинаковое, их состав разный — поэтому и группы разные. Это почти вырожденный пример того, как формируются ECMP-группы. Реальный мир посложнее. Но всё равно видно, что в случае простого IP нужно очень-очень сильно выпендриться, чтобы как-то значительно их число увеличить. При этом, стандартным для датацентровых коммутаторов является ограничение в 4096 ECMP-групп при максимум 64 NH в группе.
ECMP-группы при MPLS
А пикантности здесь добавляет MPLS. Как только мы добавим коммутацию по меткам, ситуация заиграет новыми красками. При голом IP некстхоп характеризовался параметрами (Egress_Interface, NH_MAC). В случае наличия метки, некстхоп уже состоит из трёх параметров: (Egress_Interface, NH_MAC, Label). А Label здесь уникален для префикса назначения и пути. То есть, даже если сети за удалёнными лифами будут резолвиться через лупбэки этих лифов, мы, возвращаясь к той же картинке, сходу получим 5 ECMP-групп. Внутри каждой группы по-прежнему сохраняется полный набор некстхопов и конкретный выбирается на основе хэша. Но именно из-за того, что метки, которые нужно записать в стек, разные, приходится держать отдельный набор NH под каждый маршрут.

Иллюстрация из доклада Дмитрия Афанасьева с Yandex Next Hop 2019. Фактически, в случае MPLS на лифах получается отдельная ECMP-группа на каждый маршрут. Представим теперь 8 ДЦ по 512 стоек в каждом. Даже без всяких внешних маршрутов получается 4096 лупбэков и, соответственно, ECMP-групп. А когда группы кончаются, начинает отбрасываться трафик. Такая беда. Ситуация приобретает ещё более тонкие оттенки, когда мы говорим о Transit LSP. В этом случае, в зависимости от конфигурации и производителя, возможна и ситуация с отдельной ECMP-группой на Transit LSP, что увеличивает их кратно.
Заключение
Была у меня идея, наконец, немного разобраться с тем, как работают сети. Так начался СДСМ. Потом СДСМ официально закончился. Но тут захотелось разобраться, что такое ECMP. Ну вы поняли. Разделаться полностью с вопросами балансировки задачи не стояло. Поэтому мы пролетели мимо VIP и туннелирования, DSR, L3/L4/L7 балансировщиков, GSLB, Global Unicast. Вопрос глобальной балансировки тянет на ещё одну статью, но для ознакомления рекомендую два видео, на которые я дал ссылки в следующем разделе.
