Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Привет, Хабр! Меня зовут Виталий Гуцалюк, я разработчик в команде инфраструктуры в Учи.ру. Я отвечаю за разработку внутренних сервисов, которые дают возможность нашим инженерам самостоятельно создавать и поддерживать текущие проекты в облаке. Сегодня я расскажу, как мы в нашей компании организовали управление маршрутизацией приложений и какую инфраструктуру для этого используем.
Инфраструктура
Весь поступающий интернет-трафик проходит через NLB — это ноды с ролью балансера. Они разворачивают экземпляр в OpenResty, из которого трафик дальше проксируется во внутренние конечные сервисы через приложение Ingress-proxy — это наша разработка, о которой я расскажу ниже.
Но сначала хотел бы рассказать, почему мы решили использовать промежуточное прокси.
За добавление новых раутов отвечала команда инфраструктуры. Все задачи на добавление шли через общий support-канал в Slack. Подобных запросов было много — естественно, появлялись расхождения.
С быстрым появлением новых сервисов маршрутизация требовала все большей поддержки.
Не было прозрачного процесса — разработчики не могли полностью отвечать за свой продукт.
Не было поддерживаемого инструмента для управления маршрутизацией стендов.
Попытки решить проблемы
Для тестирования маршрутов на стендах мы стали поднимать отдельные Nginx-приложения, которые умели работать с нашей Service Mesh. В конфигурации Nginx можно было прописывать alias, который указывал, к какому бэкенду необходимо обратиться через Service Mesh. За счет этого генерировались нужные апстримы — таким образом мы работали с первыми прокси.
location /foo/ {
proxy_pass http://app1;
proxy_set_header Host $host;
add_header Access-Control-Allow-Origin $scheme://$host;
}
location /foo/bar/ {
proxy_pass http://app2;
proxy_set_header Host $host;
add_header Access-Control-Allow-Origin $scheme://$host;
}
location /demo/ {
proxy_pass http://app3/courses/;
proxy_set_header Host $host;
add_header Access-Control-Allow-Origin $scheme://$host;
}
Эта реализация упростила проверку нужной маршрутизации заранее на стендах, но в продакшене ее по-прежнему невозможно было использовать: у таких прокси нет прозрачных владельцев, что вызвало сложность в их поддержке.
Поэтому мы пошли дальше — решили отдавать отдельные прокси в каждую команду (иногда сразу в несколько). В качестве уже самого сервера перешли с Nginx на Traefik. Нам было интересно его использовать из-за его характеристик: он умеет делать множество фич из коробки, а также поддерживается консулом-каталогом.
Однако и в этом подходе нашлись свои недостатки:
Нет централизованного просмотра для всех раутов. Чтобы посмотреть рауты соседних приложений, надо запросить доступ в эту команду или в отдельное прокси.
Проблемы с самим Traefik. Например, нельзя было сделать честный редирект при обработке ошибок, так как Traefik предоставляет только проксируемую страницу. Также в ForwardAuth при описании раута нельзя было указать сервис из консула. Для этого нам приходилось поднимать приватные домены.
Консул-каталог в Traefik смотрит на всю площадку. Это нам не подходило, поскольку в компании некоторые кластеры являются достаточно жирными, и полинг всей площадки доставлял проблемы. Чтобы решить эту ситуацию, мы стали фильтровать сервисы, которые смотрят Traefik. Для этого мы в нашей PaaS при регистрации нового сервиса навешивали лейблы вроде команды или названия приложения. Потом их же стали использовать в настройках Traefik для фильтрации сервисов, которые он смотрит. Это сузило области, с которыми работает Traefik, и, соответственно, решило проблему. Но недостатком стало то, что такую настройку надо было производить отдельно.
Сложно передавать командные прокси в другую команду, а такая необходимость периодически возникала. Вообще, в нашей платформе все ресурсы передаются по кнопке. Поскольку Traefik смотрит в консул-каталог с настроенной фильтрацией, то при банальном перемещении прокси в другую команду могла возникнуть ситуация, когда некоторые сервисы было невозможно найти.
Решение — собственная реализация
Поняв, что в полной мере мы не уйдем от всех проблем при использовании готовых решений, мы сделали собственное. Оно называется Ingress-proxy. По сути, это написанный на Go сервис, который разворачивает экземпляры OpenResty.
Работоспособность сервиса достигается с помощью динамического создания конфига. Он формируется с помощью двух основных процессов:
Первый занимается парсингом правил маршрутизации. Мы записываем раутинг в собственном DSL в нашей PaaS, откуда он синхронизируется через Consul KV с Ingress-proxy. Далее прокси уже парсит маршруты в нужные участки конфига в OpenResty — в директивы server и location.
Второй основной процесс сосредоточен на генерации апстримов. Здесь сильно помогает интеграция с консулом. Мы знаем о том, какие сервисы нам необходимы для апстримов, и подписываемся на изменения этих сервисов в консуле. Таким образом, у нас есть все необходимые адреса.
Мы в инфраструктуре имели многолетний опыт работы с маршрутизацией с помощью OpenResty — и имели понимание, какой routing нужен в компании. При написании DSL мы убрали все ненужные нам фичи, оставив только необходимые, а также постарались сделать синтаксис достаточно простым и удобным.
На сниппете представлен пример нашего правила маршрутизации DSL, написанный специально для Ingress-proxy:
match:
host: demo.domain.ru
prefix: /prefix
middlewares:
auth:
- backend:
service: sys-foo-auth-srv-ep
path: /authentication/authorize
upstream_headers:
- A-B-C
- Example-Header
error_handler:
- on_code:
- 401
redirect: /404.html
proxy:
backend:
service: sys-foo-main-srv-ep
version: ingress/v2
Поле version в примере выше означает, что в правилах маршрутизации есть версионирование. Оно работает таким образом, что Ingress-proxy для генерации конфига всегда обрабатывает только последнюю версию правила. Однако в хранилище возможно содержание других правил разных версий — для этого предусмотрена миграция до последней версии. Это делается в отдельном репозитории, который называется API machinery — он вдохновлен Kubernetes. Там мы описываем все общие модели сущностей, логику для валидации и версионирование.
Шаблонизацию конфигураций мы ведем с помощью утилиты Quint template. Этот пакет, по сравнению со стандартным Go template, работает намного быстрее. У него достаточно простой и понятный синтаксис.
Также в Ingress-proxy есть еще ряд метрик — это количество запросов, статусы ответов, задержки, общее количество соединений и так далее. Для всех метрик мы записываем лейблы, которые важны нам для работы, например: namespace, команда, проксируемое приложение. Они позволяют составлять узкоспециализированные дашборды, что довольно удобно.
Общая архитектура сервиса
Общая архитектура работы Ingress-proxy выглядит следующим образом:
PaaS передает правила маршрутизации в Consul KV.
Далее из консула Ingress-proxy парсит правила и забирает информацию об адресах сервисов.
В итоге мы получаем все сущности и данные, нужные для создания валидного конфиг в OpenResty, после чего трафик уже проксируется до конечных сервисов.
Отдельно отмечу, что группа Ingress-proxy объединяется так называемым ingress-классом. Это схожая с Kubernetes концепция — она позволяет выделить группу прокси под общую конфигурацию.
Эксплуатация маршрутизации в нашей PaaS
Создание и редактирование правил в нашей PaaS ведется с помощью редактора Monaco (аналог VS Code). Для удобства их ведения в простом и удобном формате у нас есть отдельные фичи — realtime-валидация или автодополнение.
Для стендов создание нужной маршрутизации ведется из коробки. В нашей платформе для каждой новой фичи мы поднимаем полностью новый стенд. У нас они достаточно быстро разворачиваются. Помогает их развернуть приложение, которое мы называем «шотилкой». У него есть набор манифестов для разных приложений, куда мы просто добавили наши манифесты для раутинга. Как итог — получили готовое приложение и маршрутизацию для него.
Возможные риски и челленжи на этапе внедрения Ingress-proxy
Конечно же, мы задумывались о рисках, с которыми могли столкнуться на этапе внедрения Ingress-proxy. И сразу решили предусмотреть, как будем справляться с трудностями:
Первый риск связан с последствием перекрытия уже существующего правила новым.
В Nginx есть правила приоритетов для location разных типов. Поскольку Ingress-proxy работает вокруг OpenResty, мы наследуем те же правила. Поэтому в теории можно было бы перекрыть уже существующий маршрут новым. Для избежания этой проблемы наша PaaS при редактировании и создании правил заранее проверяет, были ли перекрыты какие-то маршруты. Если это произошло, изменение блокируется.
Второй риск связан с зоной ответственности за правила.
Чтобы не возникало сложностей, в нашей системе чтение правил доступно всем. Их можно отфильтровать в общем списке по различным метаданным: namespace, команда или хост. Но конкретно за изменения отвечают команды-владельцы правил. У нас есть и аудит: мы записываем все изменения конфигураций, время создания, кем было произведено обновление. Таким образом, мы можем полностью отследить всю историю любого правила маршрутизации.
И последний риск — валидное правило в DSL могло сгенерировать невалидный участок конфигурации для OpenResty.
Мы стараемся максимально подробно валидировать правила еще на этапе их создания. Полностью повторить всю логику OpenResty достаточно сложно. Однако, если произойдет ситуация, когда новое правило маршрутизации сгенерирует невалидный участок конфига, то при обновлении в рестарте OpenResty с новым конфигом сервис поймет, что он невалиден, и не будет убивать сервер, который уже запущен с валидным конфигом. Именно эта логика предотвратит падение. Во время подобной ситуации срабатывает алертинг с информацией о сбое при рестарте последней версии конфига. Это помогает найти дефектное правило, пофиксить его и в дальнейшем провалидировать похожие ситуации. Такая функция, например, была очень полезна на этапе внедрения Ingress-proxy, сейчас она является дополнительным пунктом безопасности в нашей системе.
Челленджи, с которыми мы столкнулись
Внедрение Ingress-proxy не обошлось без трудностей.
Первая трудность — это высокое потребление CPU на старте в нагруженных кластерах. Самая первая версия решения была запущена стейджах. В момент запуска мы увидели на графиках высокое потребление CPU.
При профилировании оказалось, что причиной этой проблемы является перезагрузка конфига. Оказалось, что мы делали рестарт OpenResty на каждое изменение, которое записывали. А при достаточно большом кластере эти изменения происходили постоянно. Чтобы разобраться с этим, мы стали делать троттлинг: пропускать сразу несколько обновлений за одну итерацию, которая занимает 50 миллисекунд.
Вторая трудность — при прогонке Ingress-proxy на кластере stage мы замечали, что иногда приходит невалидный конфиг OpenResty, несмотря на то, что все правила являются рабочими.
Получилось это из-за того, что мы записывали конфиг не атомарно: запись новых изменений мы вели непосредственно в том конфиге, который использует OpenResty. Чтобы решить эту сложность, мы стали создавать временный файл, куда вносили все изменения. И только потом подменяли его на настоящий конфиг OpenResty.
Следующей трудностью стало то, что нужно было переместить большое количество уже существующего раутинга. Причем, поскольку у нас было несколько подходов с маршрутизацией, мы имели несколько источников для создания раутов. Их нужно было как-то привести в наш DSL.
Поэтому мы написали отдельные скрипты, которые умели из разных источников генерировать нужную нам маршрутизацию. И с такими скриптами мы приходили в каждую команду и обновляли всю существующую маршрутизацию. Так нам удалось относительно быстро перевести весь раутинг на Ingress-proxy.
Последняя трудность — метаданные для фильтрации раутов в первой версии приходилось писать руками. Во-первых, это довольно нетривиальный процесс. Во-вторых, многие стали копировать данные из одного раута в другой. Все это стало теряться и ломать поиск.
Мы решили проблему с помощью того, что точкой входа для каждого раута сделали отдельный namespace. Это наша абстракция в PaaS. Она знает о том, какой команде принадлежит namespace, и имеет различные данные — вроде того, какие приложения находятся в namespace. Этого стало достаточно, чтобы генерировать всю мету динамически. И мы одновременно добавили овнерство над изменениями правил маршрутизации за счет доступа в команду namespace.
Итоги
Мы получили легкое в эксплуатации решение, с помощью которого передали зону ответственности над управлением раутингом приложений непосредственно разработчикам. При этом мы сохранили влияние на сами процессы: если возникает необходимость добавить новую фичу, мы можем это спокойно сделать.
Сама Ingress-proxy показала себя как стабильный инструмент, который держит хорошую нагрузку: например, в обычный день у нас порядка 3000–3500 RPS только на одну группу прокси. Сам трафик сильно растет при проведении масштабной олимпиады, в которой участвует более 1 млн пользователей, или во время учебных недель. При этом у нас нет проблем с горизонтальным масштабированием.
Хочешь развивать школьный EdTech вместе с нами — присоединяйся к команде Учи.ру!