Привет, Хабр! На связи Максим Чудновский @chudnovskiy и Александр Козлов @ZDragon из СберТеха. Мы занимаемся развитием Platform V Synapse — облачной платформы, которая объединяет множество интеграционных шаблонов, в том числе классический стиль интеграции request-response через Service Mesh.  В этой статье хотим поговорить о Service Mesh в gRPC Java-сервисах: чем полезен подход, как реализовать его c помощью протокола xDS и с какими сложностями можно столкнуться. 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 (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, чтобы трафик мог переходить из одного дата-центра в другой в случае сбоев. Как мы увидели, в 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. Чтобы сделать gRPC-сервис с поддержкой xDS-протокола, вам понадобится: Java 7.0 и выше; gRPC — 1.48 (несмотря на то, что поддержка xDS появилась уже в версии 1.39, приемлемый набор функциональности для применения в промышленной среде появился позднее);  xDS-сервер (Control Plane); xDS-bootstrap. Для подключения к xDS-серверу понадобится Bootstrap — конфигурация, которая рассказывает о подключении к Control Plane. Переходим к коду. Здесь — минимальные изменения, которые потребуются, чтобы создать xDS-клиент в сети. Всё действительно просто: если в приложении используется gRPC, то поддержка xDS уже есть. Остаётся прописать XdsChannelCredentials вместо обычных credentials, которые вы используете. В настройках клиента ничего не меняется: никаких новых каналов, всё работает «из коробки», как и обещали. В части сервера придётся повозиться чуть больше, потому что, помимо авторизации и получения конфигурации, есть ещё этап с регистрацией сервиса. Всё начинается с данных авторизации для xds: для их инициализации используем XdsServerCredentials. Также вместо обычного ServerBuilder, который мы используем для создания gRPC-сервера, мы создаём XdsServerBuilder. В нём реализована логика подключения к контрольной панели и передачи информации о нашем сервисе. Одна из самых необходимых вещей, без которой ничего не будет работать. Проще всего использовать существующие реализации 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-сервисам. В теории всё просто и гладко, но в жизни без ошибок и поломок никуда. Давайте посмотрим, с какими сложностями можно столкнуться в процессе и что с этим можно сделать. Мы сделали java-приложение и подготовили стандартный набор свойств и ресурсов. На первый взгляд этого вполне достаточно для того, чтобы наш Java gRPC-сервис работал с xDS. Но он не работает. Как думаете, почему? Всё дело в том, что мы забыли Bootstrap-файл. Без него приложение даже не пробует запуститься, потому что библиотеки тут же начинают спрашивать: «А где bootstrap-конфигурация, как подключиться к контрольной панели и где вообще взять сертификаты?»   Мы добавили Bootstrap, откуда видно, что контрольная панель работает через unix///etc/istio/proxy/XDS. В наличии протокол xDS v3 и интересная нода с красивым ID — в таком режиме всё должно работать. Но это не так, в чём может быть проблема? Несмотря на то, что единственным провалом тут кажется неоднозначный ID, ошибка на самом деле в отсутствии обязательного параметра server_listener_resource_name_template. Без него xDS не сможет правильно создавать объекты listener и регистрировать их в контрольной панели.  Наступаем на новые грабли: Bootstrap есть, server_listener_resource_name_template тоже, а сервис запускается локально из IDE. Заработает или нет? Увы, но нет — в нашем Bootstrap-файле указано подключение к серверу контрольной панели (через UDS), которого на нашей локальной машине нет. В итоге мы видим, что template добавлен, имя listener сформировалось, но gRPC не смог зарегистрировать его в контрольной панели, ввиду чего работа была остановлена.   Закончив с локальной разработкой, мы решили уйти в облако. Подготовили «джентльменский» набор ресурсов (configmap, deployment) и отправили всё в Kubernetes, чтобы приложение дальше работало в контейнерах. В качестве сервера контрольной панели в нашем кластере используется Istio Service Mesh. Но вместо запуска получаем ошибку о том, что listener опять недоступен. Почему? Несмотря на то, что ошибка повторяется, причина уже совсем в другом. Сейчас наше приложение живёт в Kubernetes, контрольная панель Istio тоже работает, кажется, всё хорошо. Проблема тут в том, что для работы Service Discovery Istio в Kubernetes необходимы ресурсы Service или Service Entry. Без этого наше приложение не попадает в Service Registry, следовательно, и xDS работать не будет.   Напоследок — небольшой фрагмент кода. Казалось бы, стандартный канал 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 и заранее планировать изменения.