Привет, Хабр! На связи Максим Чудновский @chudnovskiy и Александр Козлов @ZDragon из СберТеха. Мы занимаемся развитием Platform V Synapse — облачной платформы, которая объединяет множество интеграционных шаблонов, в том числе классический стиль интеграции request-response через Service Mesh.
В этой статье хотим поговорить о Service Mesh в gRPC Java-сервисах: чем полезен подход, как реализовать его c помощью протокола xDS и с какими сложностями можно столкнуться.
![](https://habrastorage.org/getpro/habr/upload_files/c12/be1/6c6/c12be16c634961e6fed3d5dd14bdb855.png)
Как устроен Service Mesh
Service Mesh — шаблон, который используется для интеграции приложений в облачных средах. Концептуально Service Mesh состоит из двух частей: control plane и data plane.
Data plane отвечает за исполнение сетевых политик, политик безопасности и сбор телеметрии. Чаще всего эта часть строится на базе сетевых прокси, которые запускаются рядом с приложением в формате sidecar-контейнера, если речь идёт про контейнерные облачные среды (например, Kubernetes). Data plane обеспечивает маршрутизацию и балансировку сетевого трафика, исполнение политик безопасности и экспорт метрик для мониторинга, как правило в Prometheus-формате.
Control Plane — часть, которая управляет data plane и отвечает за тиражирование политик, а также нередко включает в себя компоненты для сбора телеметрии. Control plane используется для управления маршрутизацией и балансировкой запросов, распространения ключей, секретов и в целом политик безопасности, например правил RBAC или ACL. Сюда же чаще всего входит функция сбора телеметрии, интеграция с корпоративной инфраструктурой, скажем, с инфраструктурой открытых ключей, или интеграция с системами мониторинга.
Основной сценарий применения Service Mesh — это интеграция микросервисов в контейнеризированных средах (Kubernetes). Другая распространённая технология в облаках — фреймворк gRPC, который также используется для коммуникации приложений, поэтому логично посмотреть на симбиоз этих решений.
Что такое gRPC
gRPC (Google RPC) — кроссплатформенный и производительный фреймворк для распределённого вызова процедур. В gRPC поддерживается эволюция схем и есть много полезных вещей «из коробки»: deadlines, cancellation и т. д.
Но самое главное для любителей Service Mesh то, что gRPC поддерживает xDS, стандартный протокол, через который управляется data plane в большинстве решений Service Mesh.
XDS — это сетевой протокол для управления data plane. Состоит из нескольких уровней:
LDS (Listener Discovery Service)
Каждый listener — это некоторый виртуальный сервисный IP (на самом деле пара IP:Port), необходимый для работы service discovery. В общем случае за счёт listener трафик попадает в исполняемую часть Service Mesh, чтобы произошла вся последующая магия: применение политики балансировки, TLS Origination и др.
RDS (Routing Discovery Service)
Отвечает за правила маршрутизации: matching rules и action configurations. За счёт RDS обеспечивается функциональность управления трафиком в Service Mesh: канареечные релизы, распределение трафика и т. д.
CDS (Cluster Discovery Service)
Отвечает за конфигурацию бэкенда. Определяет настройки политики балансировки между endpoint, настройки circuit breaker и другие параметры для настройки пула соединений — например, время жизни tcp-сессий.
EDS (Endpoint Discovery Service)
Обеспечивает discovery для всех endpoint в рамках одного кластера (бэкенда), которые непосредственно будут обрабатывать трафик. Самое главное здесь — веса, приоритеты, locality для работы circuit breaker, чтобы трафик мог переходить из одного дата-центра в другой в случае сбоев.
Что такое Proxyless Service Mesh в gRPC Java-приложениях
Как мы увидели, в xDS есть всё для того, чтобы покрыть функциональность Service Mesh на уровне Data Plane. Что важно, в последних версиях gRPC также поддерживается xDS, а это сильно меняет дело, так как за счёт этого мы можем модифицировать традиционную архитектуру Service Mesh.
Если большинство реализаций Service Mesh работает через дополнительный слой сетевых прокси, которые могут размещаться в виде sidecar-контейнеров или демонов на вычислительных узлах Kubernetes, то в случае с gRPC можно отказаться от этого и перейти к упрощённой версии, которая построена на библиотеках. Фактически мы интегрируем слой Data Plane сразу в код приложения, просто подключив соответствующую зависимость (gRPC).
Тут читатели могут возмутиться и сказать: «Мы так писали код за много лет до контейнеров и не называли это Service Mesh!» — и будут правы!
Но есть оговорка. Если вы разрабатываете gRPC-сервис, то без соответствующих зависимостей вам в любом случае не обойтись. При этом поддержка xDS реализована сразу на уровне фреймворка, дополнительных модулей не требуется, и таким образом вы получаете Service Mesh фактически «из коробки».
Этот подход называется proxyless и позволяет добиться значительного сокращения потребляемых ресурсов, а также делает Service Mesh быстрее за счёт снижения задержек. Бонусом мы значительно упрощаем схему деплоймента приложений, и сопровождение становится в разы дешевле.
Здорово звучит? Давайте посмотрим, как это выглядит на практике в Java.
Proxyless Service Mesh в gRPC за 4 шага
Чтобы сделать gRPC-сервис с поддержкой xDS-протокола, вам понадобится:
Java 7.0 и выше;
gRPC — 1.48 (несмотря на то, что поддержка xDS появилась уже в версии 1.39, приемлемый набор функциональности для применения в промышленной среде появился позднее);
xDS-сервер (Control Plane);
xDS-bootstrap.
Для подключения к xDS-серверу понадобится Bootstrap — конфигурация, которая рассказывает о подключении к Control Plane.
Переходим к коду.
Шаг 1. Делаем xDS-клиент
![](https://habrastorage.org/getpro/habr/upload_files/969/2c5/752/9692c575276a51c84a70a1c044295648.png)
Здесь — минимальные изменения, которые потребуются, чтобы создать xDS-клиент в сети. Всё действительно просто: если в приложении используется gRPC, то поддержка xDS уже есть. Остаётся прописать XdsChannelCredentials вместо обычных credentials, которые вы используете. В настройках клиента ничего не меняется: никаких новых каналов, всё работает «из коробки», как и обещали.
Шаг 2. Делаем xDS-сервер
В части сервера придётся повозиться чуть больше, потому что, помимо авторизации и получения конфигурации, есть ещё этап с регистрацией сервиса. Всё начинается с данных авторизации для xds: для их инициализации используем XdsServerCredentials. Также вместо обычного ServerBuilder, который мы используем для создания gRPC-сервера, мы создаём XdsServerBuilder. В нём реализована логика подключения к контрольной панели и передачи информации о нашем сервисе.
![](https://habrastorage.org/getpro/habr/upload_files/518/88f/b31/51888fb3182383347b139c6ad7827cb7.png)
Шаг 3. Готовим Bootstrap
Одна из самых необходимых вещей, без которой ничего не будет работать. Проще всего использовать существующие реализации Service Mesh, так как обычно агент сам готовит Bootstrap-файл со всей необходимой конфигурацией. Если у вас собственный xDS-сервер, файл придётся написать вручную.
Сам Bootstrap состоит из нескольких разделов. Первый — xds_servers, описание доступных xDS-серверов, где участвует хост. Описаны параметры подключения к серверу, а также поддерживаемый функционал. От наличия или отсутствия последних будет зависеть то, какие данные станут необходимыми в вашем Bootstrap-файле.
![](https://habrastorage.org/getpro/habr/upload_files/87f/b60/2e8/87fb602e8b8e34fb1251ccfbe0fb6934.png)
Дальше секция node с описанием экземпляра сервиса. Как в случае клиента, так и в случае сервера у нас будет информация о том, кто запрашивает конфигурацию в кластере. Если это серверная часть, то информация будет использоваться ещё и для регистрации в Control Plane. Также мы можем передать некоторые метаданные, которые служат дополнительной информацией для контрольной панели. При использовании геобалансировки не стоит забывать про locality и следует задать регион, где расположен сервис.
![](https://habrastorage.org/getpro/habr/upload_files/f41/b89/ab2/f41b89ab2dbd09d5038b90e57a39fab2.png)
И финальная часть из двух элементов, в которых написано, откуда брать сертификаты и каким будет шаблон для формирования listener. Конфигурация поставщиков сертификатов может быть разная: например, локальное слежение за файлом или папкой, куда складываются сертификаты, или удалённый сервер, занимающийся выпуском сертификатов. Далее идёт server_listener_resource_name_template, который отвечает за формирование имени обработчиков ресурсов. Без корректного заполнения сервис не сможет зарегистрироваться в контрольной панели и создать у себя объекты listener.
![](https://habrastorage.org/getpro/habr/upload_files/813/a62/522/813a62522a00731262cb7138c1ba0834.png)
Недостаточно просто создать сервис — его нужно отлаживать, обслуживать и вообще как-то с ним работать. В этом может помочь набор базовых инструментов, которые уже есть в библиотеке, и мы можем подключить их при инициализации нашего сервиса.
ProtoReflection позволит использовать gRPCurl и делать запросы к серверу, не используя proto-файл, которого часто нет под рукой при удалённой отладке;
ChannelzService — вспомогательный сервис для отладки сети, который позволяет видеть, куда открыты коннекты и в каком они состоянии;
CsdsService позволяет посмотреть конфигурацию клиента. Это одна из самых полезных вещей, которая позволяет видеть реальную xds-конфигурацию в рамках нашего приложения. Это может оказаться очень полезным при откладке правил маршрутизации в Service Mesh.
Ещё несколько деталей.
Журналирование
В самой технологии нет ничего нового: gRPC «из коробки» поддерживает возможность перехвата своих контроллеров, что позволяет настроить журналирование в рамках сервиса, а также позаботиться о трассировке и мониторинге.
Возможно, не получится найти готовую реализацию interceptor’a для журналирования, но самостоятельная разработка в данном случае не займёт много времени. Это несложно, а кроме того, можно имплементировать собственную логику с доставкой сообщений в нужные вам журналы.
![](https://habrastorage.org/getpro/habr/upload_files/6ba/6ce/7d4/6ba6ce7d4a1d4c6a341f401b19cd7b4b.png)
Трассировка
Для распределённой трассировки часто используют спецификацию Open Tracing, и мы не станем исключением. Чтобы начать, можно развернуть Jaeger Tracer, после чего создаём interceptor с трассировкой и подключаем его к развёрнутому ранее Jaeger.
![](https://habrastorage.org/getpro/habr/upload_files/777/5d6/bd8/7775d6bd81c6dd6f15692f95247b5e5d.png)
Мониторинг
Для мониторинга лучше (но необязательно) использовать метрики в формате Prometheus, при этом можно не регистрировать всё вручную, а использовать готовую реализацию MonitoringServerInterceptor (от dinowernli) для gRPC. Он автоматически перехватывает входящие и исходящие запросы, на основе этой информации рассчитывает и сразу публикует полный набор необходимых метрик.
Что может пойти не так
Итак, мы только что рассмотрели весь процесс подключения xDS к gRPC-сервисам. В теории всё просто и гладко, но в жизни без ошибок и поломок никуда. Давайте посмотрим, с какими сложностями можно столкнуться в процессе и что с этим можно сделать.
Ситуация № 1
![](https://habrastorage.org/getpro/habr/upload_files/3bc/14a/9a9/3bc14a9a9b1cd75d45092e697b3bc0ee.png)
Мы сделали java-приложение и подготовили стандартный набор свойств и ресурсов. На первый взгляд этого вполне достаточно для того, чтобы наш Java gRPC-сервис работал с xDS. Но он не работает. Как думаете, почему?
Ответ
Всё дело в том, что мы забыли Bootstrap-файл. Без него приложение даже не пробует запуститься, потому что библиотеки тут же начинают спрашивать: «А где bootstrap-конфигурация, как подключиться к контрольной панели и где вообще взять сертификаты?»
![](https://habrastorage.org/getpro/habr/upload_files/4c6/1b1/6bb/4c61b16bb7abc0d3245ae371935530e5.png)
Ситуация № 2
![](https://habrastorage.org/getpro/habr/upload_files/cff/63d/da2/cff63dda2030757dad79b65b65d18710.png)
Мы добавили Bootstrap, откуда видно, что контрольная панель работает через unix///etc/istio/proxy/XDS. В наличии протокол xDS v3 и интересная нода с красивым ID — в таком режиме всё должно работать. Но это не так, в чём может быть проблема?
Ответ
Несмотря на то, что единственным провалом тут кажется неоднозначный ID, ошибка на самом деле в отсутствии обязательного параметра server_listener_resource_name_template. Без него xDS не сможет правильно создавать объекты listener и регистрировать их в контрольной панели.
![](https://habrastorage.org/getpro/habr/upload_files/75d/08e/363/75d08e3632bcaea5519a5ba41f613055.png)
Ситуация № 3
Наступаем на новые грабли: Bootstrap есть, server_listener_resource_name_template тоже, а сервис запускается локально из IDE. Заработает или нет?
![](https://habrastorage.org/getpro/habr/upload_files/f79/1dc/54a/f791dc54a5b70f35fcedb742b5b89ed9.png)
Ответ
Увы, но нет — в нашем Bootstrap-файле указано подключение к серверу контрольной панели (через UDS), которого на нашей локальной машине нет. В итоге мы видим, что template добавлен, имя listener сформировалось, но gRPC не смог зарегистрировать его в контрольной панели, ввиду чего работа была остановлена.
![](https://habrastorage.org/getpro/habr/upload_files/9cf/f08/a8d/9cff08a8d4a767411d197fa9c91f6e18.png)
Ситуация № 4
Закончив с локальной разработкой, мы решили уйти в облако. Подготовили «джентльменский» набор ресурсов (configmap, deployment) и отправили всё в Kubernetes, чтобы приложение дальше работало в контейнерах. В качестве сервера контрольной панели в нашем кластере используется Istio Service Mesh. Но вместо запуска получаем ошибку о том, что listener опять недоступен. Почему?
![](https://habrastorage.org/getpro/habr/upload_files/290/f49/818/290f49818d3fc172a652c644d3edbff5.png)
Ответ
Несмотря на то, что ошибка повторяется, причина уже совсем в другом. Сейчас наше приложение живёт в Kubernetes, контрольная панель Istio тоже работает, кажется, всё хорошо. Проблема тут в том, что для работы Service Discovery Istio в Kubernetes необходимы ресурсы Service или Service Entry. Без этого наше приложение не попадает в Service Registry, следовательно, и xDS работать не будет.
![](https://habrastorage.org/getpro/habr/upload_files/df9/c02/4b4/df9c024b45ac4d23220f894dd1219466.png)
Ситуация № 5
Напоследок — небольшой фрагмент кода. Казалось бы, стандартный канал gRPC, стандартные builder, xDS-credentials. Вроде бы всё хорошо, это в общем работает.
![](https://habrastorage.org/getpro/habr/upload_files/bff/be9/f49/bffbe9f4942b19905f35d22d3fbe2eb7.png)
Есть ли тут проблемы?
Ответ
Несмотря на то, что все запросы успешно ходят, функциональность xDS и Service Mesh нам недоступна. Дело в том, что без прямого указания протокола xDS при инициализации канала grpc мы будем использовать стандартный name resolver. Соответственно заработает dns name resolver, и мы получим просто IP-адрес сервиса в Kubernetes. Именно по этому адресу и будут уходить запросы — визуально всё работает, а фактически нет.
![](https://habrastorage.org/getpro/habr/upload_files/8bc/c0e/7a0/8bcc0e7a0f78acde0537b9a71b26b547.png)
Важно внимательно относиться к указанию протокола, причём в данном случае это не http или gRPC, а именно xDS.
Бонусный вопрос про политику балансировки
А именно: можно ли задать политику балансировки Least Request на gRPC-клиенте при использовании xDS?
На первый взгляд всё очень просто: мы уже знаем, что в протоколе xDS есть уровни CDS и EDS, отвечающие за правила балансировки трафика. Очевидно, чтобы изменить балансировку, достаточно сменить конфигурацию кластера в CDS. Идём в контрольную панель и меняем конфигурацию с помощью YAML-файла и соответствующего API.
И это действительно сработает, но не с использованием xDS в gRPC. Увы, но эта доработка пока только в планах community, и нам остаётся только надеяться, что в скором времени она появится.
Что в итоге
С одной стороны, поддержка xDS и Service Mesh в gRPC значительно сокращает сложность деплоймента, открывает множество возможностей и работает «из коробки». С другой — может вызывать трудности на этапе отладки. Часть этих сложностей решается быстро, но с некоторыми придётся повозиться: здесь мы описали далеко не все возможные ситуации.
Если времени и ресурсов разбираться с особенностями подключения xDS к gRPC-сервисам нет, всегда можно обратиться к готовым решениям. Например, Platform V Synapse поддерживает proxyless-подход наряду с другими функциями, необходимыми для интеграции прикладных микросервисов.
В любом случае важно хорошо продумать, на какой результат вы рассчитываете. Не вся функциональность Service Mesh уже поддерживается в xDS, поэтому важно отслеживать статус появления фичей в gRPC и заранее планировать изменения.
mRKoKc
Молодцы