В современной инфраструктуре нет недостатка в решениях для балансировки и межсервисных коммуникаций. Почти все используют nginx, HAProxy, есть адепты Treafik, а публичные облака предлагают Load Balancer как сервис. Но что делать, если инструменты не справляются с ростом масштабов и необходимо автоматизируемое cloud-native-решение?
Я Дмитрий Самохвалов, архитектор в K2 Cloud. В этой статье поделюсь, как мы из-за ограничений старых систем для динамической конфигурации перешли с работающих решений nginx и HAProxy на модный Envoy. Расскажу, почему сочли это решение подходящим, какие возможности есть у Envoy, каким был опыт внедрения и оставлю рекомендации для эффективного перехода. Будет полезно разработчикам cloud-native-приложений и инфраструктуры, а также всем, кто хочет создать единое платформенное решение для взаимодействия сервисов и инфраструктуры.
Почему nginx и HAProxy может стать недостаточно
Представим себе «нормальный» мир — обычное окружение, где существует проект с собственной инфраструктурой, находящейся под полным контролем.
Есть фронтальный L7 балансировщик, в данном случае nginx. Он способен не только направлять трафик на другие бэкэнды, но и выполнять дополнительную логику. Например, может работать в качестве веб-сервера, издавать статику. Часто такой функционал используется во фронтенд-приложениях. На L7 балансировщике можно также добавлять и другие функции, основанные на уровне приложения. В дополнение обычно присутствует также L4 балансировщик, чаще всего HAProxy.
Всё это нормально работает, когда у вас простая инфраструктура, например, для одного или нескольких проектов, которыми вы полностью управляете. Однако всё усложняется при переходе к сервисной платформе, которая способна не только использовать инфраструктуру, основанную на nginx и HaProxy, но и динамически предоставлять её заказчикам.
Здесь проблема не только в том, что количество микросервисов и инфраструктуры экспоненциально возрастает, но и в том, что начинаются трудности с разделением зон ответственности между командой или заказчиком, использующими инфраструктуру, и вами как сервисной платформой, которая обеспечивает управление этой инфраструктурой.
Подобные проблемы связаны с тем, что nginx и HAProxy представляют собой продукты «старой школы», созданные во времена, когда деревья были большими, а облака — маленькими. Первый релиз nginx состоялся в 2004 году, хотя разработка началась ещё раньше. HAProxy был выпущен в 2000 году — в нашей команде есть ребята моложе.
И, конечно, эту инфраструктуру проектировали так, что управлять ею будут в первую очередь живые люди. Язык конфигурации как у nginx, так и у HAProxy разработан с учётом удобства для человека, чтобы было легче находить ошибки и вносить правки.
Когда речь заходит об автоматизации такой конфигурации, начинается самое интересное. Обычно, по крайней мере так было у нас, это смесь шаблонов Ansible, разнообразных bash-скриптов для тестирования. Иногда даже прибегают к использованию Lua-плагинов. При этом ни nginx, ни HAProxy идеологически не предусматривают динамического обновления. И когда вам нужно изменить большое количество конфигураций, это превращается в эксплуатационный ад.
Также у nginx и HAProxy отсутствует контрольная панель, необходимая для управления конфигурацией на множестве узлов. Не потому, что это ошибка, а потому, что это их особенность. Так они спроектированы.
При эксплуатации большого количества этих инструментов часто возникали трудности: обычные изменения в конфигурациях требовали множество эксплуатационных усилий. Особенно когда коллеги из отдела безопасности начали уделять внимание настройке nginx, возникли вопросы не столько о технической реализации nginx, сколько о его конкретной настройке. Как правило, они начинали с удаления информации о версии nginx, чтобы уменьшить вероятность поиска уязвимостей по версии, и акцентировали внимание на процессе конфигурации. Следующий шаг был связан с более сложными задачами, такими как настройка рейтинговых лимитёров.
Обновление большого количества конфигураций каждый раз было головной болью, в частности, при использовании шаблонов на ansible. Приходилось работать и с Jinja-шаблонами, а затем проводить дополнительные настройки, тестирование и так далее.
Также мы столкнулись с недостатком observability у nginx и HAProxy. Метрики из коробки обычно не достаточно информативны. Например, в случае поиска аномалий и расследования инцидентов, особенно если имеется большое количество хопов между балансировщиками. Нам явно была нужна какая-то более надёжная и информативная телеметрия.
В этом контексте расширение функциональности nginx и HAPRoxy не выглядело простым решением. HAProxy в целом сложно расширяется. А nginx имеет отдельный дистрибутив OpenResty, который надо дополнительно обновлять и конфигурировать. Это неудобно, если мы стремимся к поддержке нормальной работы retry и circuit breakers в L7-инфраструктуре. Для этого придётся либо писать костыли, либо искать готовые фильтры.
Может возникнуть и другая проблема: использование неоптимизированного load-плагинаможет серьёзно ухудшить производительность nginx. Тогда возникнут проблемы с обновлением, управлением репозиториями и др.
Поиск альтернативного решения
После обсуждения недостатков существующих решений мы собрали критерии для выбора альтернативы в балансировке нагрузки. И между собой назвали новое решение «единорогом». Вот что нам хотелось получить в результате:
Конфигурация на YAML с дополнительной поддержкой интеграции с Kubernetes naive. Так как YAML сейчас — стандарт конфигурации, который хорошо автоматизируется и относительно легко читается.
Собственный Control Plane, через который можно в рантайме динамически настраивать конфигурации на всех узлах и хорошо интегрироваться с Service Discovery наших инструментов. Например, с Kubernetes.
Обширный набор observability метрик для мониторинга аномалий и анализа трафика.
Из коробки получить фильтры, перехватчики, health-check, которых нет в бесплатных версиях nginx и HAProxy. И возможность их динамически расширять при необходимости.
На момент исследования доступных решений наиболее подходящим показался Traefik. Но относительно наших задач он имел ограничения. Во-первых, у Traefik был только API и не было Control Plane, а это было совсем неудобно. Во-вторых, этот функционал на тот момент нельзя было расширить и масштабировать.
После исследований альтернатив мы остановись на Envoy. Он полностью удовлетворял требованиям:
Несмотря на сложность и объём, Envoy имеет конфигурацию на YAML. Сложность заключается в особенностях проектирования этого решения: его настройкой должна заниматься автоматизированная система, а не администраторы вручную.
У Envoy есть Control Plane.
Обширные возможности observability.
Богатый набор готовых, работающих из коробки фильтров, которые могут пригодиться для работы как нам самим, так и разработчикам и заказчикам.
Базовые элементы конфигурации Envoy
Расскажу подробнее что включают в себя Envoy Core Elements. На рисунке ниже они расположены лесенкой, чтобы показать логику работы и обработки запросов.
На верхнем уровне — listener. Этот компонент похож на сервер nginx — он прослушивает определённый сокет или порт. Чаще всего устанавливается локальный хост, потому что Envoy предполагает работу на конкретном хосте и прослушивание локального.
Дальше запрос проходит через фильтры, например, Route. Здесь это HTTP-фильтр, который матчит все домены со звёздочкой, все префиксы и дальше направляет запрос в кластер. Кластер — это самый близкий аналог апстрима в nginx, состоящий из ряда эндпоинтов.
Полная конфигурация, естественно, богаче. В ней есть возможность выбрать LB Policy — ROUND_ROBIN или другое. Самих элементов конфигурации больше, но эти четыре — основные. Envoy конфигурирует их так называемым xDS-протоколом, который разрабатывается отдельно от Envoy и позволяет конфигурировать блоки. Это можно делать по отдельности — xDS, Discovery Service, а вместо x можно подставить как элемент конфигурации, так и всё сразу — так называемый ADS, aggregate discovery service.
Этот xDS-протокол основан на Protocol Buffers и, как я уже сказал, это, по сути, gRPC стрим между Control Plane и Envoy.
И ещё одна важная особенность конфигурации в том, что Envoy можно настроить в двух режимах. Например, State of The World, когда Envoy получает с Control Plane всю конфигурацию сразу, или инкрементальный режим, когда получает только дельту конфигурации.
State of The World прост в реализации: он только отправляет текущее состояние в каждый Envoy. Но хуже по производительности. Особенно при большом объёме данных, конфигураций и получении информации с большого количества узлов с Control Plane. Инкрементальный режим в целом более производителен. Насколько мне известно, в данный момент Istio переходит на инкрементальный xDS. Однако его реализация сложнее, и сама концепция инкрементального xDS требует больших усилий для внедрения.
Итак, архитектура решения Envoy выглядит следующим образом. Есть Control Plane, который включает в себя xDS-сервис, получающий информацию о своих апстримах из облака, Kubernetes или другого источника. xDS-сервис формирует конфигурацию xDS и распределяет её по дата-плейну для Envoy, которые используют Control Plane в качестве источника конфигурации. Естественно, в Envoy можно указать файл вместо Control Plane, но в таком случае теряется смысл.
Envoy Control Plane
Если погрузиться глубже в Control Plane, то на самом деле он представляет собой просто gRPC сервера, написанные на Go, и реализует логику обновления элементов конфигурации Envoy, таких как cDS, rDS и т.д.
Существует фреймворк Go Control Plane, разработанный авторами Envoy. И есть его официальная версия на Java, которая используется меньше, поскольку основное инфраструктурное программирование в основном ведётся на Go. Я видел также реализации на Rust, но это уже скорее для истинных ценителей.
В целом под капотом у Control Plane много технических нюансов. Рассказ о них претендует на отдельную статью. Поэтому пока сосредоточимся на том, как мы переезжали на такое решение.
Control Plane v1
Первая версия нашего Control Plane выглядела довольно просто, даже немного примитивно. Был запущен сервер Control Plane, который получал информацию о конфигах и распределял её между теми Envoy и Data Plane, которые были с ней связаны. Новые конфигурации загружались уже с платформы непосредственно в xDS-сервер, а старый мы постепенно переводили на язык Envoy.
Здесь нас ждали сюрпризы. Сначала мы думали, что процесс будет простым: возьмём серверы, переложим их на листенеры, заменим апстримы на кластеры, и всё заработает. Но оказалось, что конфигурация nginx нацелена на то, чтобы её читали живые люди, а конфигурация Envoy рассчитана на автоматизацию.
Разрыв между человекочитаемой и автоматизируемой конфигурациями оказался слишком большим. Например, если для nginx можно было просто написать «allow» и «deny», то в случае с Envoy это превращалось в каскад конфигураций access control list. И проще было сгенерировать всё заново, чем пытаться перевести из текущей конфигурации.
Но настоящая проблема крылась в другом.
В nginx есть функция, которую сами создатели называют злом, — это «if». Она вообще не рекомендуется к использованию. Но из-за ограничений в настройке nginx и простоты написания «if» многие разработчики и инженеры создали свои собственные фильтры, которые нельзя было без проблем перенести на язык Envoy.
Мы не будем касаться случаев, когда использование «if» изначально используется неправильно, например, в локейшене. Это типичные примеры плохой практики.
Были кейсы, когда идеология Envoy не соответствовала логике «if» написанных на языке nginx. В языке Envoy предусмотрены HTTP и TCP-фильтры, применяемые к одному и тому же флоу, но при этом должны быть разные фильтры и разные запросы. Поэтому в некоторых случаях пришлось кардинально менять логику, а иногда даже переносить эту логику на другие инструменты.
Когда мы набрали опыт в эксплуатации, стало ясно, что архитектура, разработанная для пилотного проекта, в реальности не жизнеспособна. Когда платформа является единственным источником правды и Control Plane зависит от неё, это перестаёт эффективно работать при большом количестве запросов из-за дополнительной нагрузки на платформу. К тому же профиты от автоматизации нивелируются тем, что при управлении конфигурациями самой платформой с использованием веб-интерфейса мы всё равно вынуждены дополнительно поддерживать ещё один репозиторий и накручивать ещё один уровень автоматизации.
Так мы пришли ко второй версии решения.
Control Plane v2
Мы перенесли Control Plane Envoy в Kubernetes с оператором. И таким образом начали использовать кастомные ресурсы для управления Kubernetes кластером.
Стоит упомянуть, что если в инфраструктуре используется только Kubernetes и нет планов взаимодействовать с легаси-инфраструктурой, как в нашем случае, где уже существует значительное количество написанного и активно развивающегося Control Plane, то можно обратить внимание на амбассадоров, в большом количестве представленных на GitHub. Они в целом хорошо написаны и надёжно работают. Однако мы обнаружили у них два основных недостатка: во-первых, не все умеют взаимодействовать с инфраструктурой за пределами Kubernetes, и во-вторых, у каждого из них есть своё особенное представление о том, как должен настраиваться Envoy. В итоге они делают, как правило, очень гранулированную конфигурацию. Она превращается в огромное количество кастомных ресурсов, которые дальше становятся копипастом и шаблонизируются.
Мы решили, что написать свой оператор проще. Плюс разработали небольшой CDS Bridge — сетевой адаптер между CNA Kubernetes и некоторыми нашими сетевыми VPC, которые напрямую с Kubernetes не взаимодействовали. Но это уже детали внутренней кухни работы протокола, не будем подробно на них останавливаться.
На этой конфигурации системы мы остановились. Её можно дополнительно улучшить, сделав типовую настройку Envoy не при помощи CRD, а через веб-хук в Kubernetes. И посредством специальных меток на деплойменте генерировать этим оператором конфигурацию Envoy. Но это уже пути дальнейшего улучшения.
Итоги
Подводя итоги, обозначу, что у этого решения были свои плюсы и минусы. И прежде чем суммировать бенефиты, полученные от Envoy, рассмотрим сначала недостатки. Они действительно были.
Минусы перехода на Envoy
Envoy, в отличие от nginx, не предназначен для раздачи статического контента. Это стало проблемой для фронтенд-разработчиков, которые привыкли, что их приложения обслуживаются nginx. Мы не хотели использовать заплатки, такие как хранение статического контента на S3 и его передачу через Envoy. Поэтому мы договорились с фронтенд-разработчиками, что они либо переносят раздачу статики на JavaScript-веб-сервер, если у них есть такая возможность и нет серьёзных требований к производительности, либо остаются на nginx в качестве исключения.
Также стоит упомянуть статический конфиг, который использовали коллеги для баз данных. Он остался на HAProxy, потому что настройка была минимальной. Конфиг статический, никогда не меняется, и это, наверное, тот случай, когда администратор говорит, что у него всё работает, и у него действительно работает.
Самое главное в списке минусов — это, конечно же, стоимость разработки и ресурсы, которые необходимо вложить в такое решение. Подобное имеет смысл делать только тогда, когда вы чётко понимаете, зачем это нужно.
Инфраструктурная разработка отличается от продуктовой и в случае с Envoy становится сложнее из-за плохо структурированной документации. Многие вещи в ней написаны языком, понятным только авторам, хотя постепенно они приводят описания в порядок. Плюс — это опенсорсное решение, которое быстро развивается. Например, вышла уже третья версия xDS-протокола. Мы начали со второй и потом переезжали на новую. И в процессе миграции обнаружили проблемы, которые за три месяца после заявленных issue никто так и не пофиксил, и нам пришлось его просто закрыть. Проблема исчезла только после переезда. Поэтому, естественно, заплатить ресурсами разработки придётся.
Плюсы перехода на Envoy
Плюсов получилось больше, чем минусов. Мы создали динамическую систему настройки всех балансировщиков во всех наших кластерах и решениях, которые использовали внутри компании. Система не просто настраивается в рантайме. Это полноценный инфраструктурный код, а значит, все конфигурации могут быть предварительно провалидированы. К тому же это настраивается на gitops-процесс, если есть желание.
Ещё у нас появились богатые возможности по телеметрии всей инфраструктуры. Например, в то время как nginx из коробки предоставляет около десятка метрик, у Envoy их несколько десятков. Мы даже пришли к тому, что иногда отключали часть метрик, так как если включить все, то браузеры переставали справляться.
В итоге получили то, что хотели. Теперь у нас есть одно решение для всех кейсов, которые раньше размазывали по нескольким инструментам. И конечный профит в эксплуатации оказался выше ожидаемого: несмотря на потраченные на разработку ресурсы в короткий период времени, решение оказалось выгодным в длительной перспективе.