Привет, Хабр! На связи Максим Чудновский @chudnovskiy и Александр Козлов @ZDragon из СберТеха. Мы занимаемся развитием Platform V Synapse — облачной платформы, которая объединяет множество интеграционных шаблонов, в том числе классический стиль интеграции request-response через Service Mesh.
В этой статье хотим поговорить о Service Mesh в gRPC Java-сервисах: чем полезен подход, как реализовать его c помощью протокола xDS и с какими сложностями можно столкнуться.

Как устроен 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-клиент

Здесь — минимальные изменения, которые потребуются, чтобы создать xDS-клиент в сети. Всё действительно просто: если в приложении используется gRPC, то поддержка xDS уже есть. Остаётся прописать XdsChannelCredentials вместо обычных credentials, которые вы используете. В настройках клиента ничего не меняется: никаких новых каналов, всё работает «из коробки», как и обещали.
Шаг 2. Делаем xDS-сервер
В части сервера придётся повозиться чуть больше, потому что, помимо авторизации и получения конфигурации, есть ещё этап с регистрацией сервиса. Всё начинается с данных авторизации для xds: для их инициализации используем XdsServerCredentials. Также вместо обычного ServerBuilder, который мы используем для создания gRPC-сервера, мы создаём XdsServerBuilder. В нём реализована логика подключения к контрольной панели и передачи информации о нашем сервисе.

Шаг 3. Готовим Bootstrap
Одна из самых необходимых вещей, без которой ничего не будет работать. Проще всего использовать существующие реализации Service Mesh, так как обычно агент сам готовит Bootstrap-файл со всей необходимой конфигурацией. Если у вас собственный xDS-сервер, файл придётся написать вручную.
Сам Bootstrap состоит из нескольких разделов. Первый — xds_servers, описание доступных xDS-серверов, где участвует хост. Описаны параметры подключения к серверу, а также поддерживаемый функционал. От наличия или отсутствия последних будет зависеть то, какие данные станут необходимыми в вашем Bootstrap-файле.

Дальше секция node с описанием экземпляра сервиса. Как в случае клиента, так и в случае сервера у нас будет информация о том, кто запрашивает конфигурацию в кластере. Если это серверная часть, то информация будет использоваться ещё и для регистрации в Control Plane. Также мы можем передать некоторые метаданные, которые служат дополнительной информацией для контрольной панели. При использовании геобалансировки не стоит забывать про locality и следует задать регион, где расположен сервис.

И финальная часть из двух элементов, в которых написано, откуда брать сертификаты и каким будет шаблон для формирования listener. Конфигурация поставщиков сертификатов может быть разная: например, локальное слежение за файлом или папкой, куда складываются сертификаты, или удалённый сервер, занимающийся выпуском сертификатов. Далее идёт server_listener_resource_name_template, который отвечает за формирование имени обработчиков ресурсов. Без корректного заполнения сервис не сможет зарегистрироваться в контрольной панели и создать у себя объекты listener.

Недостаточно просто создать сервис — его нужно отлаживать, обслуживать и вообще как-то с ним работать. В этом может помочь набор базовых инструментов, которые уже есть в библиотеке, и мы можем подключить их при инициализации нашего сервиса.
ProtoReflection позволит использовать gRPCurl и делать запросы к серверу, не используя proto-файл, которого часто нет под рукой при удалённой отладке;
ChannelzService — вспомогательный сервис для отладки сети, который позволяет видеть, куда открыты коннекты и в каком они состоянии;
CsdsService позволяет посмотреть конфигурацию клиента. Это одна из самых полезных вещей, которая позволяет видеть реальную xds-конфигурацию в рамках нашего приложения. Это может оказаться очень полезным при откладке правил маршрутизации в Service Mesh.
Ещё несколько деталей.
Журналирование
В самой технологии нет ничего нового: gRPC «из коробки» поддерживает возможность перехвата своих контроллеров, что позволяет настроить журналирование в рамках сервиса, а также позаботиться о трассировке и мониторинге.
Возможно, не получится найти готовую реализацию interceptor’a для журналирования, но самостоятельная разработка в данном случае не займёт много времени. Это несложно, а кроме того, можно имплементировать собственную логику с доставкой сообщений в нужные вам журналы.

Трассировка
Для распределённой трассировки часто используют спецификацию Open Tracing, и мы не станем исключением. Чтобы начать, можно развернуть Jaeger Tracer, после чего создаём interceptor с трассировкой и подключаем его к развёрнутому ранее Jaeger.

Мониторинг
Для мониторинга лучше (но необязательно) использовать метрики в формате Prometheus, при этом можно не регистрировать всё вручную, а использовать готовую реализацию MonitoringServerInterceptor (от dinowernli) для gRPC. Он автоматически перехватывает входящие и исходящие запросы, на основе этой информации рассчитывает и сразу публикует полный набор необходимых метрик.
Что может пойти не так
Итак, мы только что рассмотрели весь процесс подключения xDS к gRPC-сервисам. В теории всё просто и гладко, но в жизни без ошибок и поломок никуда. Давайте посмотрим, с какими сложностями можно столкнуться в процессе и что с этим можно сделать.
Ситуация № 1

Мы сделали java-приложение и подготовили стандартный набор свойств и ресурсов. На первый взгляд этого вполне достаточно для того, чтобы наш Java gRPC-сервис работал с xDS. Но он не работает. Как думаете, почему?
Ответ
Всё дело в том, что мы забыли Bootstrap-файл. Без него приложение даже не пробует запуститься, потому что библиотеки тут же начинают спрашивать: «А где bootstrap-конфигурация, как подключиться к контрольной панели и где вообще взять сертификаты?»

Ситуация № 2

Мы добавили Bootstrap, откуда видно, что контрольная панель работает через unix///etc/istio/proxy/XDS. В наличии протокол xDS v3 и интересная нода с красивым ID — в таком режиме всё должно работать. Но это не так, в чём может быть проблема?
Ответ
Несмотря на то, что единственным провалом тут кажется неоднозначный ID, ошибка на самом деле в отсутствии обязательного параметра server_listener_resource_name_template. Без него xDS не сможет правильно создавать объекты listener и регистрировать их в контрольной панели.

Ситуация № 3
Наступаем на новые грабли: Bootstrap есть, server_listener_resource_name_template тоже, а сервис запускается локально из IDE. Заработает или нет?

Ответ
Увы, но нет — в нашем Bootstrap-файле указано подключение к серверу контрольной панели (через UDS), которого на нашей локальной машине нет. В итоге мы видим, что template добавлен, имя listener сформировалось, но gRPC не смог зарегистрировать его в контрольной панели, ввиду чего работа была остановлена.

Ситуация № 4
Закончив с локальной разработкой, мы решили уйти в облако. Подготовили «джентльменский» набор ресурсов (configmap, deployment) и отправили всё в Kubernetes, чтобы приложение дальше работало в контейнерах. В качестве сервера контрольной панели в нашем кластере используется Istio Service Mesh. Но вместо запуска получаем ошибку о том, что listener опять недоступен. Почему?

Ответ
Несмотря на то, что ошибка повторяется, причина уже совсем в другом. Сейчас наше приложение живёт в Kubernetes, контрольная панель Istio тоже работает, кажется, всё хорошо. Проблема тут в том, что для работы Service Discovery Istio в Kubernetes необходимы ресурсы Service или Service Entry. Без этого наше приложение не попадает в Service Registry, следовательно, и xDS работать не будет.

Ситуация № 5
Напоследок — небольшой фрагмент кода. Казалось бы, стандартный канал gRPC, стандартные builder, xDS-credentials. Вроде бы всё хорошо, это в общем работает.

Есть ли тут проблемы?
Ответ
Несмотря на то, что все запросы успешно ходят, функциональность xDS и Service Mesh нам недоступна. Дело в том, что без прямого указания протокола xDS при инициализации канала grpc мы будем использовать стандартный name resolver. Соответственно заработает dns name resolver, и мы получим просто IP-адрес сервиса в Kubernetes. Именно по этому адресу и будут уходить запросы — визуально всё работает, а фактически нет.

Важно внимательно относиться к указанию протокола, причём в данном случае это не 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
Молодцы