В Istio всё начинается с маленькой «удобной» идеи — прокси рядом с каждым подом. А заканчивается тем, что XDS разносит по кластеру тонны Listener’ов, Route’ов и Cluster’ов, дублируя конфиги для сервисов, которые вы даже не трогаете. Память тает, GC злится, а апдейты сервисов превращаются в шторм. В статье пойдет речь о том, как мы в проде с 20K+ подов «урезали аппетит» сайдкаров на гигабайты. Обсудим, где работает жёсткое ограничение видимости, когда спасает Ambient Mesh, зачем нужен ленивый XDS и почему «волшебной кнопки» нет — но есть комбинации, дающие двузначную экономию. Эта статья написана по мотивам моего доклада для конференции Saint HighLoad++.

Привет, Хабр! Меня зовут Максим Чудновский, и я владелец продукта Platform V Synapse Service Mesh, СберТех. В разработке Service Mesh c 2018 года. Дополнительно создаю много полезного вокруг Service Mesh и Kubernetes — контроллеры, операторы, сетевые демоны и так далее.

Масштаб эксплуатации Platform V Synapse Service Mesh:

●        300+ продуктовых команд

●        200+ кластеров Kubernetes

●        20K+ подов в Service Mesh

Суть проблемы

Объясняя, что такое Istio, в последнее время я люблю говорить, что это просто Kubernetes-оператор, ни больше, ни меньше.

Kubernetes-оператор Istio следит за нативными для Kubernetes API — подами, сервисами и так далее. Дополнительно у Istio есть собственные слой API и CRD — Virtual Service, Destination Rule и так далее. Исходя из отслеживаемой информации, Istio её собирает, обрабатывает, строит дерево Discovery. В результате получается Proxy Config – набор параметров, управляющий поведением прокси-сервера.

Конфигурация грузится на прокси-сервер, который размещается там же, где и пользовательский трафик и задает правила его обработки.

Proxy Config и его оптимизация — основная боль в Istio. Концептуально Proxy Config состоит из слоёв. Вот так они выглядят на схеме:

 

А теперь подробнее о каждом:

  • Listener управляется через LDS (Listener Discovery Service). Это объект конфигурации NvProxy, который объясняет, где и как забирать трафик — на какой адрес и по какому порту трафик будет приходить. Это нужно для дальнейшей маршрутизации трафика.

  • Route — конфигурация маршрутизации, управляемая через RDS (Route Discovery Service). Конфигурация роутинга отвечает за то, куда перенаправить трафик, чтобы сработала магия, которая подменит хосты, сервис и так далее.

  • Cluster, который вступает в игру после роутинга. Это первая обескураживающая штука в Istio, потому что кластер обычно понимают как кластер Kubernetes, но здесь это другое. Кластер в терминах Istio это просто объект конфигурации, который описывает некоторое состояние бэкенда, то есть куда в конечном итоге будет маршрутизирован трафик.

  • Endpoint — конечные точки, из которых состоят кластеры. Это пара IP-адрес и порт, куда нужно физически направить трафик, чтобы он обработался. Здесь происходит балансировка по весам, управление connection flow и так далее.

С такой структурой конфига в Istio есть несколько «замечательных» проблем, объединяемых общим слоем амплификации. 

Амплификация Service & pods

Istio следит за нативными ресурсами Kube.

То есть, когда вы что-то создаёте просто в Kube, например, под или какой-нибудь сервис, Istio об этом узнаёт и делает много работы.

То есть, когда вы что-то создаёте просто в Kube, например, под или какой-нибудь сервис, Istio об этом узнаёт и делает много работы.

В случае одного сервиса он делает Listener Cluster Routing Config для каждого прокси, который есть в вашем Service Mesh. Количество прокси масштабируется и эскалируется вместе с Data Plane. Поэтому объём конфигурации просто тиражируется, когда вы создаёте сервис в Kubernetes (даже если не используете его) и становится значительным из-за постоянных обновлений при создании.

Потом под вашим деплойментом создаётся под, обслуживающий этот сервис.

Этот под заезжает во все прокси. Поды в Kubernetes достаточно подвижны. Как все знают, они смертны, соответственно, сегодня запустились, завтра остановились, что-то поменялось. Вся эта информация регулярно прокачивается через весь Service Mesh, чтобы зарядить таблицу маршрутизации рядом с вашим приложением, чтобы, когда будет отправляться запрос, этот запрос отправился куда надо, и всё сработало. Это количество конфигураций нужно, чтобы обеспечить максимальную видимость всего и вся в кластере. Но оно создаёт то самое потребление памяти, о котором мы говорим, и с которым мы постараемся что-то сделать сегодня в рамках этой статьи.

Что можно сделать?

#1. Управление скоупами видимости в Service Discovery

Первый способ достаточно тривиальный. Если вся проблема от Discovery, значит все видят всех в любой момент, он динамически обновляется. Так давайте сделаем так, чтобы никто никого не видел? Кажется, это может решить проблему.

Можно ли сделать так в Istio? Конечно, да. Делается это разными путями:

⮚       Sidecar Resources
https://istio.io/latest/docs/reference/config/networking/sidecar/

Вторая обескураживающая часть в Istio — это sidecar-контейнеры, то есть рядом с вашим приложением размещается дополнительный контейнер с прокси. В Istio есть явный API, который тоже называется Sidecar. Это многих путает, но это разные вещи. Этот Sidecar позволяет регулировать скоуп видимости настоящего sidecar, который будет работать и обрабатывать трафик.

●        Применяется Sidecar Resources к конкретным рабочим нагрузкам по workloadSelector:

workloadSelector:

     labels:

        app: productpage

Вы можете специфицировать с помощью селектора какая конкретно группа подов будет получать правило по ограничению видимости и дальше.

●        Управляют скоупом для ingress или egress трафика:

egress:

   - hosts:

        - "./*"

        - "istio-system/*"

Просто управляете видимостью тех хостов, которые должны быть загружены на этот конкретный Instance Proxy. Вы можете просто отсечь все ненужное и оставить только то, что нужно для приложения, с чем оно будет работать. Например, если приложение ходит на Google.com, вы в ingress-хостах укажете этот ресурс, а всё остальное — не будете.

●        Могут быть глобальными.

Это значит, что можно сделать:

-          Один Sidecar на namespace без workloadSelector. Тогда он применится ко всему живому, что есть в этом namespace.

-          Один Sidecar можно сделать глобальным на весь mesh через MeshConfig. Это значит, что он применится ко всему живому, что есть в Service Mesh.

Так можно установить дефолтные правила, например, никто никого не видит, все таблицы маршрутизации по умолчанию пустые. А дальше по разрешительной модели выдавать только нужные маршруты. Когда я говорил, что это применится ко всему живому, я немного лукавлю, потому что Sidecar не работают с Gateways.

●        Не работают с Gateways.

То есть ingress или egress Gateways всё равно будут видеть всё, что видит Service Mesh, и этой штукой решить эту проблему никак нельзя.

⮚       Service Annotations
https://istio.io/latest/docs/reference/config/annotations/#NetworkingExportTo

Помимо CRD в Istio есть достаточно мощный слой API, построенный на аннотациях. Эти аннотации можно добавлять к нативным объектам Kubernetes, с которыми работает Istio, то есть к подам, деплойментам, сервисам.

⮚       Service Annotations
https://istio.io/latest/docs/reference/config/annotations/#NetworkingExportTo

Помимо CRD в Istio есть достаточно мощный слой API, построенный на аннотациях. Эти аннотации можно добавлять к нативным объектам Kubernetes, с которыми работает Istio, то есть к подам, деплойментам, сервисам.

К сервисам можно добавлять аннотацию экспорту, чтобы управлять зоной видимости. Так Istio поймёт, что этот сервис, например, не планируется к обслуживанию в Service Mesh, соответственно, тиражировать маршрутную информацию по нему не нужно.

Аналогично работает механика ExportTo API в нативных конфигурациях Istio:

⮚       ExportTo APIs

В каждом ресурсе есть поле ExportTo, где вы можете управлять зоной видимости этого ресурса и скоупом его работы на уровне namespace, группы namespaces или всего mesh целиком.

.apiVersion: networking.istio.io/v1

kind: ServiceEntry

metadata:

     name: external-svc-httpbin

spec:

     hosts:

     - example.com

     exportTo:

     - "."

     location: MESH_EXTERNAL

И ещё есть любопытная переменная конфигурация для самого Istio:

⮚       Discovery Selectors
https://istio.io/latest/docs/ops/configuration/mesh/configuration-scoping/#discoveryselectors

С помощью Discovery Selectors можно просто выключить Istio в определённой группе namespaces. Вы можете сказать: «Привет, Istio, вот тебе селектор. Во всех namespaces, которые сопоставляются с этим селектором, ты работаешь. Весь остальной кластер, пожалуйста, не трогай», и Istio это поймёт.

meshConfig:

     discoverySelectors:

          - matchLabels:

               istio-discovery: enabled

          - matchLabels:

               kubernetes.io/metadata.name: kube-system

Это не особо влияет на память, чаще всего так просто делают внутренние тенантирования Istio внутри кластера.

#1. Scope – оценка

Подведем промежуточный итог. Сегодня будем выставлять импровизированные оценки, Istio про sidecars, поэтому оценки будем выставлять в колясочках в трех номинациях «Эффективность», «Простота реализации», «Простота масштабирования», максимальная оценка — 5 колясочек (sidecars).

⮚       Эффективность — 5 sidecars (максимум)

С точки зрения эффективности этот подход очень хорош, потому что:

●        Охватывает все конфигурации.

●        Применяется на весь Data Plane.

Здесь мы получаем максимальную оценку, потому что, в принципе, заявленную проблему статьи можно решить этим подходом. Потратив какое-то время, вы всё оптимизируете.

⮚       Простота реализации — 3 sidecars

С точки зрения простоты реализации оценка ниже. Это не самое удобное решение, потому что требуется глубокое понимание Istio API. Нужно глубоко погрузиться в то, как работает Istio, какие нативные и не нативные конфигурации есть, и как их правильно разметить для эффективной работы.

Дополнительно придётся поменять CI/CD, чтобы это как-то доставлялось до рантайма, потому что в рантайме оно самостоятельно не обновится.

⮚       Простота масштабирования — 1 sidecar

На самом деле в масштабировании кроется основная проблема.

1.       Изменение топологии вызовов – это ваша новая забота.

Если топология сервисов поменялась, например, ваше приложение ходит на Google.com, а завтра пойдёт на другой ресурс, то вам придётся менять конфигурацию в рантайме, и это большая проблема. В случае, если сервисов и кластеров у вас много, это вырождается в конфигурационный ад.

2.       Редеплой.

Если вы используете более сложные вещи а-ля Discovery Selector, то придётся в придачу что-нибудь редеплоить для того, чтобы это применилось.

Поэтому подход управления scope не работает на масштабе: cлишком много изменений нужно отслеживать ежедневно, ежечасно, ежесекундно, и каким-то образом доставлять до рантайма.

Совокупная оценка — 9 колясочек.

Сразу скажу, что это моё личное мнение, не являющееся инвестиционной рекомендацией. Просто можете ориентироваться на это в работе.

#2 Proxyless Mesh

Есть под в Kubernetes, в котором два контейнера (APP и ISTIO PROXY) и служебные демоны Istio, которые всё это обслуживают.

Идея Proxyless проста — уберём Proxy и сделаем так, чтобы приложение конфигурировалось через Istio Control Plane напрямую.

https://istio.io/latest/blog/2021/proxyless-grpc/

Подход понятен, проблема понятна — нет Proxy, нет конфигурации, мы серьёзно экономим по памяти.

#2. Proxyless – оценка

В итоге получаем:

⮚       Эффективность — 3 sidecars

●        Istio – всегда 25 mb на sidecar.

●        Растёт потребление ресурсов в Application Container.

Есть маленький нюанс — в Proxyless Service Mesh прокси нет, а Sidecar Container есть, но только он потребляет стабильное количество памяти, потому что работает как посредник для доставки конфигурации напрямую в ваше приложение. Это не отменяет потребление Data Plane, а просто делает его более предсказуемым и статическим — всегда 25 mb на sidecar.

Одновременно растёт потребление ресурсов в Application Container, потому что маршрутная конфигурация как была, так и осталась — просто переехала из одного контейнера в другой.

Поэтому, с точки зрения эффективности, это не очень удобный подход, и работает чаще всего это просто за счет того, что вы на Application Container изначально даёте количество ресурсов с запасом, потому что там обычно работают бизнес-приложения, пользовательские транзакции, по которым нельзя уйти в троттлинг. А раз вы делаете с запасом, то туда можно прикопать Istio конфигурацию. Бизнес в целом будет доволен — Service Mesh не стоит ничего, а то, что Application Container потребляет в два раза больше памяти говорит о способе зарабатывания денег.

⮚       Простота реализации — 2 sidecars

●        Требуется изменение кода клиента.

●        Требуется изменение кода сервера.

С точки зрения простоты реализации придётся поменять код приложения как клиентов, так и серверов. А значит, либо реализовать самостоятельно, либо обновить фреймворк, в который это встроено, и добавить правильные строчки, чтобы это включилось. Поэтому здесь мы можем говорить только про две колясочки — уж слишком много изменений придётся провести для миграции на эту технологию.

⮚       Простота масштабирования — 1 sidecar

●        Есть SDK для разных языков.

●        Нативная поддержка только gRPC и Dubbo.

Есть SDK для всех языков. Если вы пишете на C или Go, то сможете его использовать. Также вы сможете это использовать нативно, если пишете сервисы на gRPC или Dubbo. Кто не знает, Dubbo — это китайский gRPC, очень похож, но разрабатывается в Поднебесной. Если вы используете любой другой стиль коммуникации, REST API и стандартные HTTP ручки, то это вам не подойдёт, поэтому масштабировать на рантайм этот подход будет сложно.

Совокупная оценка — 6 колясочек.

#3. Ambient Mesh

Это новый режим, появившийся в Istio (GA) и пригодный для промышленной эксплуатации. Идея похожа на предыдущую:

У нас есть стандартные компоненты Istio.

Нам по-прежнему не нравится Proxy, поэтому мы его удаляем из чата.

Но трафик как-то обрабатывать надо, поэтому добавляем новый компонент, который называется Ztunnel.

Отличие от Sidecar состоит в том, что он запускается по модели DaemonSet на каждом вычислительном узле кластера. То есть это daemon, который обслуживает все поды, которые запустились на воркере Kubernetes. Выгода будет значительная, потому что один воркер Kubernetes, как правило, это примерно 100 подов. Больше 100 подов обычно в конфигах Kubelet никто не ставит. Поэтому соотношение будет 1 к 100, а по ресурсам потребление растёт не так сильно, потому что Ztunnel — это сетевой daemon 4 уровня. Он делает только mutual TLS и обработку базовых сетевых вещей.

Если вам нужна функциональность 7 уровня, то в Istio Ambient добавляют ещё один компонент, который называется Waypoint Proxy:

Это уже полноценный NVProxy 7 уровня, где вы можете обрабатывать HTTP-трафик и использовать всю привычную функциональность. Однако разворачивается он на стороне сервера. Выглядит это следующим образом: есть серверная часть, где расположено приложение или сервис, который вы употребляете. А рядом с ним размещается Waypoint proxy. Этот компонент знает только про маршрутизацию на конкретный бэкенд, конкретный сервер и поды, которые обслуживают этот запрашиваемый сервис. Таким образом, конфигурация Waypoint не скалируется на Data Plane и не зависит от количества подов в кластере. Она относительно статична, потому что масштабируется только от количества подов сервера. Поэтому подход с использованием Istio Ambient выглядит эффективным.

Понятно, что трафик приложения теперь обрабатывается так:

Эта схема имеет другие трейд-оффы, в том числе и по безопасности. Но здесь мы их рассматривать не будем, потому что сегодня мы пытаемся сэкономить память в Istio.

#3. Ambient – оценка

⮚       Эффективность — 5 sidecars

●        Ztunnel заменяет в лучшем случае ~ 100 sidecars.

●        Конфигурация Waypoint не скалируется от сервисов.

Waypoint не скалируется от Data Plane напрямую, и это хорошо, потому что выгода просто космическая. Вы просто ставите Embend, и потребление ресурсов падает на порядок, а в некоторых случаях может упасть даже на несколько порядков. Экономия значительная, поэтому здесь максимальный балл.

⮚       Простота реализации — 5 sidecars

Здесь тоже максимальный балл, потому что это стандартная функциональность Istio. Вы открываете документацию, устанавливаете Istio правильной командой, получаете Ambient Mesh и радуетесь снижению потребления. Показываете отчёт руководству и говорите: «Смотрите, как классно мы сократили косты на инфраструктуру, мы очень эффективно работаем!». Сложности начинаются, когда вы хотите это масштабировать.

Простота масштабирования — 3 sidecars

●        Нет обратной совместимости с Istio Classic.

●        Возможен гибридный режим – Ambient + Sidecars.

Вы внедрили пилот Ambient в одной команде — всё хорошо, все довольны. Потом выясняется, что обратной совместимости с обычным Istio нет. А у вас много команд, которые Istio используют. Причём некоторые используют весьма неожиданным для них и для вас способом. Такой способ на Ambient не ложится, потому что в Ambient есть функциональные трейд-оффы, например, работа с Ingress трафиком, client-side балансировка и дальше по списку.

Авторы Ambient говорят, что ничего страшного: пусть те, кому не подходит Ambient, используют sidecars, а мы сделаем совместимость. То есть можно сделать гибридный Service Mesh, кто-то будет использовать Ambient, а кто-то sidecars. Несложно догадаться, что эффективность такого подхода тогда начинает снижаться, потому что sidecars вернулись к нам, это уже не так хорошо.

За простоту масштабирования ставим три колясочки, потому что нужно делать планы миграции, дорабатывать конфигурации, иногда модифицировать приложение, а это больно.

Но в итоге 12 баллов, и это отличная оценка.

#4. Resizable Resources & Policy Engines

Этот поход стал возможен с новыми версиями Kubernetes и применением менеджера политик - Policy Engine.

Когда вы сохраняете что-то в Kubernetes, есть определённый процесс того, как обрабатывается ваш ресурс. Например, развертывание. В этот процесс можно встроиться через admission контроллеры:

  • Мутация.

  • Валидация.

Через мутацию можем менять спецификацию сохраняемого ресурса, прежде чем он попадёт в ETCD и будет обработан соответствующим контроллером. Через валидацию — проверять. Подключаем менеджер политик, и он начинает получать запросы на мутацию и валидацию. Дальше в Policy Engine подкидываем определённую конфигурацию в виде политики, стратегии, как нужно изменить развертывание, и он это меняет.

На схеме приведен менеджер политик Kubelatte, доступный для скачивания как открытое ПО на платформе GitVerse — заходите, пользуйтесь. Это опенсорс-проект СберТеха, который развивает команда Platform V Synapse Service Mesh.

Что нового в Кубере:

⮚       Desired Resources

A container's spec.containers[*].resources represent the desired resources for the container, and are mutable for CPU and memory.

Всегда считалось, что поды в Кубере иммутабельны, под можно только создать, а потом — убить. Но это уже не так. Кое-что в спецификации кода можно поменять — ресурсы. Есть ресурсы, запланированные при старте приложения — это те самые указанные requests и limits.

⮚       Actual Resources

The status.containerStatuses[*].resources field reflects the resources currently configured for a running container. For containers that haven't started or were restarted, it reflects the resources allocated upon their next start.

Kubernetes на самом деле теперь знает про Actual Resources — то есть о том, сколько ваше приложение сейчас потребляет ресурсов. Когда прошли стартап-пробы, liveness, readiness-пробы горят зелёным, всё хорошо, приложение работает, поступает пользовательский трафик. Но если оно вышло на плато по ресурсам, часто бывает так, что это плато ниже, чем то, что вы изначально закладывали, потому что вам нужны были дополнительные косты на инициализацию приложения, прогрев контекстов и так далее.

⮚       Triggering a Resize

You can request a resize by updating the desired requests and limits in the Pod's specification. This is typically done using kubectl patch, kubectl apply, or kubectl edit targeting the Pod's resize subresource. When the desired resources don't match the allocated resources, the Kubelet will attempt to resize the container.

Когда вы вышли на плато, вы можете триггернуть изменение спецификации пода, сработает Policy Engine и поменяет ресурсы, которые вы планировали, на те, что в реальности используют приложения. Таким образом, планировщик Kubernetes использует эти ресурсы для других подов.

Подход, кажется, хороший. Возникает вопрос, а много ли можно сэкономить в Istio с его помощью?

#4. Resizable Resources – оценка

⮚       Эффективность — 1 sidecar

Нет, немного, потому что прокси достаточно оптимизированы, им не нужно прогреваться, им не нужно инициализировать сложные, большие контексты. Поэтому подрезать ресурсы можно только в том случае, если вы изначально делали ресурсное планирование под Service Mesh с большим запасом или использовали дефолтную конфигурацию, которая оказалась ниже вашего фактического употребления.

⮚       Простота масштабирования — 2 sidecars

С точки зрения простоты реализации это сложно, если делать самому, и просто, если взять из интернета. Есть готовые опенсорсные решения, достаточно надёжные, безопасные, и отказоустойчивые.

⮚       Простота масштабирования — 5 sidecars

С точки зрения простоты масштабирования это фантастическая штука, потому что вы один раз ставите Policy Engine на кластер, один раз пишите необходимую политику, один раз согласуете её со всеми заинтересованными стейкхолдерами (с инфраструктурой, безопасностью и всеми, кто контролирует изменения в ваших кластерах) и тиражируете это на весь кластер целиком, на все команды, а потом на другие кластера вашей инсталляции. Это растягивается в мультикластер, один простой YAML, и вы сможете распространить этот подход легко и просто. Это стоит 20% и приносит очень много пользы. Поэтому этот подход стоит рассмотреть.

За простоту масштабирования выросла итоговая оценка – 8 колясочек.

#5. External Lazy XDS

Идея подхода проста:

Мы можем загружать маршруты из Service Discovery на прокси в ленивом режиме — только те, что нужны для конкретных вызовов.

Кажется, что нашему приложению не нужны конфигурации, которые оно не использует, пока оно не делает соответствующие вызовы. Значит, мы можем их туда не загружать, и, когда приложению потребуется сделать сетевой вызов, подгрузим один маршрут, и таким образом на прокси будет всего один реально используемый маршрут. В ленивом режиме он в итоге будет инициализирован и потреблять ровно столько памяти, сколько ему нужно.

Хорошая идея? Кажется, что да. Есть ли вопросы? Конечно, есть:

1.     Как настроить Istio?

2.     Как не потерять первый вызов?

3.     Как это автоматизировать?

 

Как вы помните, изначально конфигурации нет, а приложение делает сетевой вызов, и это про latency, нужно его куда-то сохранить и подержать какое-то время, пока не подъедет маршрут, а это может быть проблемной операцией.

Говоря об автоматизации и вспоминая скоупы, ручной тираж конфигурации — это боль в Istio. Нужно как-то эту проблему решать. Объясню, как.

Есть приложение, желающее вызывать какие-то сервисы, доступные в кластере. Добавляем специальные Lazy Gateway.

Технически это обычные манипуляции с Gateway, но мы будем называть их Lazy-игры с Gateway. Они будут программно-управляемые специальным оператором, который мы с вами создадим, настроив через Istio.

Мы просто будем делать Istio конфиги, чтобы этот Lazy Gateway был настроен корректно.

На Lazy Gateway мы загружаем полную таблицу маршрутизации на все сервисы, доступные в этом кластере. Этот egress Lazy Gateway видит всё — каждый маршрут, роут, кластер. А приложение, вызывающее сервисы, работает и конфигурируется другим способом. Так мы полностью закрываем сетевую видимость через Sidecar, о котором говорили в самом начале.

То есть мы говорим: «Дорогое приложение, ты не получаешь никаких маршрутов, кроме дефолтных системных, ведущих на Istio system, чтобы работал сам Istio». Через Envoy Filter мы подкидываем всего один маршрут. Его суть в том, что любой запрос нужно перенаправить на Lazy Gateway абсолютно по любому сервису. Это статический маршрут, он всегда уезжает на Lazy Gateway. Это можно подбросить через конфигурацию Envoy Filter.

Таким образом наше приложение получает практически нулевой пуш — не получает в моменте, не получает в динамике, никаких изменений в сервисах, подах и так далее. Эту функциональность мы выключаем. Первый запрос у нас проходит всегда через Lazy Gateway. Мы его никогда не теряем, более того, мы его даже нигде не храним. То есть, в отличие от серверных отслеживаний, например, Key Native, где запросы буферизируются, здесь это не нужно. Мы его просто отправляем по другому маршруту. Да, подрастёт latency, потому что есть дополнительный хоп, но незначительно по отношению к тому, сколько бы мы ждали конфигурацию.

Таким образом проходит первый вызов.

Этот вызов мы легко отслеживаем через Access-Log и формируем уже другую конфигурацию.

Фактически мы говорим: «ОК, теперь ты видишь прямой маршрут с сервиса 1 на сервис 2, теперь ты будешь ходить на сервис 2 прямым маршрутом».

Происходит обновление конфигурации на нашем прокси.

Маршрут уже случается прямой для сервиса 2.

https://github.com/aeraki-mesh/lazyxds

Трафик ходит напрямую, Lazy Gateway не работает в этом процессе, соответственно нет дополнительной задержки, дополнительного хопа, а sidecar потребляет минимальное количество памяти.

Это опенсорсное решение. Схему я специально не стал менять. Его можно использовать в своём ландшафте. Есть и альтернативные решения — легко найдёте их сами.

#5. External Lazy XDS – оценка

⮚       Эффективность — 4 sidecars

С точки зрения эффективности — четыре коляски. Потому что всё хорошо, но есть Lazy Gateway, который деплоится в каждый namespace, получает полный набор маршрутных конфигураций и его потребление надо учитывать, то есть система не идеальна.

⮚       Простота реализации — 2 sidecars

С точки зрения простоты реализации — тоже самое, что и с Policy Engine. Если делать это самим, то это долго и дорого, если взять готовое из open-source, то в целом это живое решение, которое можно применить в относительно сжатые сроки.

⮚       Простота масштабирования — 4 sidecars

Вы не делаете никаких изменений на приложении и на конфигурациях, всё запускается из коробки. Но есть дополнительный компонент Lazy Gateway, и это трейд-офф по отказоустойчивости и надёжности, это может быть трейд-офф по перформансу, потому что у вас есть дополнительный компонент в системе, и это влияет.

Итоговая оценка — 10 sidecars.

Можно ли улучшить этот подход? Мы долго думали над этим вопросом и оказалось, что да, можно.

#6. Internal Lazy XDS

Мы пришли к схеме Internal Lazy XDS Gateway.

Работает она так. Есть Istio, контрольная панель, и нам нужно как-то доставить конфигурацию на Envoy.

Для этого формируется gRPC запрос, где есть конфигурация для прокси. Она принимается фильтрами где-то в прокси. Дальше фильтруется, предобрабатывается.

Здесь мы можем встроиться в процесс и сказать, что у нас есть дополнительный модуль, который будет отслеживать только прикладную конфигурацию, то есть не системные маршруты на сервисе. Всё, что мы считаем системными, загружаем на Envoy напрямую, без каких-либо модификаций или изменений.

А дальше, если это не системная информация, мы начинаем работать в модуле отложенной загрузки, который написали сами, форкнув Envoy proxy и модифицировав цепочку обработки запросов.

Есть базовая функция, которая фильтрует ресурсы. Мы её немного модифицируем и говорим, что теперь ресурсы будут фильтроваться по схеме, которую предлагаем.

Ресурсы фильтруются те же, что мы обсуждали в начале доклада:

1)    Список маршрутов — это конфигурация роутинга.

 2)    Список кластеров — конфигурация бэкенда, которая нам доступна.

3)    Список Listeners, которые присутствуют в структурах Envoy и используются для того, чтобы перехватывать трафик.

4)    Cписок конечных точек.

В Envoy эта структура называется LoadAssignment, по факту это список конечных точек, на которые мы будем отправлять трафик в те самые XDS.

В итоге мы получаем две хитрых таблицы, которые заполняем через анализ этой конфигурации:

  • Таблица соответствия имени кластера кластеру.

  • Таблица соответствия адреса кластера кластеру.

Эти две таблицы регулярно обновляются и используются для обработки трафика, когда приложение формирует запрос. Это происходит так: клиент (наше приложение) хочет отправить запрос на сервер, этот запрос подходит через прокси.

Прокси анализирует запрос и понимает, что маршрутной информации для этого запроса нет, кластер не существует в конфигурации.

Таким образом, мы этот запрос сохраняем прямо в системной памяти Envoy и начинаем проверять, приходила ли информация об этом кластере и сохранили ли мы эту информацию в таблице соответствия имен кластеров и самих кластеров.

Если нет, то надо бы проверить адреса, нет ли соответствия в таблице адресов, потому что трафик может быть четвертого уровня.

Если мы кластер нашли, то понимаем, что по такому кластеру приходила информация, и мы её проигнорировали, потому что работаем в ленивом режиме. Значит, самое время эту информацию достать. Поэтому мы формируем запрос в специализированную контрольную панель и говорим; «дайте нам, пожалуйста, изменения по такому-то адресу, потому что будет запрос».

Контрольная панель честно этот запрос обрабатывает, маршрутная конфигурация доставляется в Envoy, пролетает мимо всех фильтров, загружается стандартным способом.

Запрос, естественно, отправляется по назначению, а мы подчищаем наши таблицы маршрутизации, чтобы следующий маршрут был уже прямым. Мы поняли, что запрос ходит напрямую, буфер нам не нужен.

Это полный аналог схемы Lazy XDS, который работает через внешний Gateway, но всё происходит сразу в прокси без дополнительных компонентов и трейд-оффов по надёжности.

#6. Internal Lazy XDS – оценка

Оценка здесь получается соответствующая:

⮚       Эффективность — 5 sidecars

⮚       Простота реализации — 1 sidecar

⮚       Простота масштабирования — 5 sidecars

Это чуть сложнее, нужно заморочиться с доработкой Envoy, как это делали мы. С точки зрения эффективности и масштабирования максимальные баллы, потому что это работает из коробки, не нужно настраивать и конфигурировать Service Mesh. В общем, полная свобода — вы просто поднимаете переменную окружения и радуетесь графику потребления памяти Data Plane в Istio, потому что он кратно снижается.

Здесь итого 11 колясочек.

***

Креативные решения

Мы рассмотрели стандартные способы. Все они проверены, так или иначе опробованы, по ним собраны и уже отработаны шишки. Но, помимо этого, есть и креативные решения, которые можно вывести умозрительно, использовав наше знание о том, как работает Istio.

#7. Отказ от сервисов в Kubernetes

Идея очень простая: если сервисы в Kubernetes так сильно влияют на Istio, мы создаём сервис, получаем кучу ненужных конфигураций, давайте просто не будем их делать вообще. Мы не будем делать ни одного сервиса в Kubernetes.

Возникает лишь один вопрос: а что, так можно было? Можно работать без сервисов в Kubernetes, ведь документация Kubernetes говорит, что нельзя, это основополагающий объект.

На самом деле, если хорошо подумать и почитать доку на Istio, то да, можно. Идея следующая: если Istio работает с двумя API-группами, мы выключаем одну и остаётся вторая.

Получается, что теперь мы должны использовать только вторую для того, чтобы управлять Service Discovery.

Можно ли так сделать? Да, можно. Ресурс в Istio называется ServiceEntry.

На самом деле он называется ServiceEntry ровно потому, что это запись в Istio Service Discovery о каком-то сервисе. То есть, это аналог сервисов обычного Kubernetes, который мы привыкли использовать, но только в API-группе Istio, который нужен для работы самого Istio. Мы привыкли использовать ServiceEntry как внешний сервис, чтобы сослаться на какой-то ресурс, который не входит в состав кластера и в состав Service Mesh.

Однако, здесь есть workloadSelector, и здесь можно сослаться на сущность, которая называется workloadEntry.

WorkloadEntry — это репрезентация какого-то endpoint в общепринятой терминологии, а в терминах Kubernetes endpoint’ом является под. Поэтому workloadEntry могут ссылаться на обычные поды в Kubernetes. Istio будет работать через сервис Entry напрямую без сервисов в Kubernetes, но вся маршрутизация сработает.

Когда эта мысль пришла мне в голову, я подумал, неужели кто-то так делает? Загуглил и оказалось, да, потому что есть 10 issues на GitHub, где люди говорят, что мы не используем сервисы в Kubernetes, но Istio у нас почему-то не работает. Там свой ворох проблем, но, тем не менее, такой подход имеет право на жизнь, и люди его используют.

#8. Переписать Istio. Частично

Многие рано или поздно приходят к идее, что Istio работает неэффективно, и нужно просто его переделать и сделать нормально — снизив потребление памяти. Это можно сделать, но частично. То есть, не переделать весь Istio, но сделать хорошо. Расскажу как.

Есть Service Mesh и Istio Proxy. Копнем, что происходит внутри контейнера Istio Proxy.

На самом деле там живут два бинаря:

  • Envoy Proxy, который обрабатывает сетевой трафик.

  • Pilot Agent. Потому что раньше XTD компонента, которая отвечала за маршрутизацию, тоже называлась Pilot. На самом деле этот Pilot Agent будстрапит Envoy, то есть все конфигурации, получаемые от Istio, проходят через внутренний прокси, который называется Pilot Agent.

Между агентом и Istio — собственный Istio API. Это gRPC с лимитом и собственной спецификацией. Классический XDS, который мы обсуждали в середине статьи, как раз прокидывается на Envoy через Pilot Agent. Мы можем форкнуть не сам Istio, но этого агента и подключить к нему дополнительный Control Plane, который будет отправлять конфигурацию в нужном режиме и по нужным сценариям.

Агент будет мержить эту конфигурацию с ресурсами, присланными Istio. В итоге эта конструкция будет жить и хочется верить, что ничего не сломается.

Возникает вопрос, пробовали ли мы у себя в организации такой подход? Да, попробовали. Правда, для другой задачи, не для оптимизации потребления памяти, а для хитрой поддержки кастомных протоколов.

#9. Решить вопрос одной командой

Наверняка многие хотели прочитать про какие-то магические ENV, или YAML-файлы, или флажки в Istio CTL, который можно применить, и всё заработает. То есть мы переключаем Istio в экономичный режим, и всё случается — задачи решаются одной командой.

Можно ли починить Istio одной командой? В целом, да. Такая команда существует:

linkerd install | kubectl apply -f -

Для тех, кто не знает, linkerd — это другой Service Mesh. Соответственно, вы можете установить другой Service Mesh, и потребление памяти кратно упадет. Если до этого вы попробовали предыдущие 8 способов из этой статьи, вам потребуется две команды, что логично:

istioctl uninstall --purge

linkerd install | kubectl apply -f

Сначала вы удаляете Istio, потом ставите linkerd и решаете проблему потребления памяти. При этом получаете десяток новых проблем, но об этом поговорим в следующих сериях.

Итоги

Мы рассмотрели много способов, попробуем их систематизировать и определиться, что и когда применять.

⮚       12/15 – Ambient Mesh (#3)

Максимальную оценку получил подход Ambient Mesh. Действительно, Ambient Mesh — это подход, который вы должны применять всегда, когда это возможно. Если вы можете поставить Ambient Mesh и всё работает, а все команды, пользующиеся инфраструктурой, довольны, нет никаких проблем и ничего не взрывается — это максимальный результат при минимальных вложениях. Этот подход действительно поможет серьёзно сэкономить память без лишних усилий.

Если Ambient Mesh вам не полностью подходит, потому что широта использования Istio очень велика, то дальше есть группа подходов, которые я бы назвал Lazy:

⮚       11/15 – Internal Lazy XDS (#6)

⮚       10/15 – External Lazy XDS (#5)

Это решения из коробки. Они позволяют точно так же решить проблему, и для пользователя это будет выглядеть, будто мы установили волшебный флаг, и всё заработало. Internal чуть лучше, потому что не нужен дополнительный компонент. External чуть хуже в плане эффективности и масштабируемости, но проще, потому что можно взять опенсорсное решение или обычный Egress Gateway и конфигурировать его вручную. Но в конечном итоге это может привести к конфигурационному аду, к которому почти всегда приводит ручное управление скоупами видимости.

⮚       9/15 – Scope (#1)

Здесь история точно такая же, как с Ambient Mesh. Это стандартные API Istio, они разрознены на аннотации, конфигурации и CRD. Но если вы можете это применять и это работает, делайте, это решает проблему. Просто имейте в виду, что рано или поздно вы придёте к слишком подвижному состоянию инфраструктуры для того, чтобы управлять скоупами видимости вручную. В этот момент вспомните, что есть ещё три способа, которые позволяют автоматизировать этот процесс.

⮚       8/15 – Resizable Resources (#4)

Этот подход не стоит ничего, а в плане тиражирования приносит пользу, поэтому я всегда говорю — используйте Policy Engine и придумывайте нестандартные кейсы, как его использовать. Его можно добавить даже в историю с потреблением ресурсов, что, как мне кажется, интересный подход в использовании Policy Engine.

⮚       6/15 – Proxyless (#2)

Это вещь в себе. Много надежд было на эту технологию, когда она появилась в 2021 году. В итоге нормальное воплощение она нашла только в решениях под Google в Google Cloud. Service Mesh там работает на базе Istio и называется Traffic Director. Это поддерживается, авторы заявляют, что он действительно экономит ресурсы, но принести это к себе в инфраструктуру достаточно сложно. Требует больших инвестиций и особой пользы не приносит, потому что вы перекладываете потребление ресурсов с Istio на приложение. Так вы просто маскируете проблему, но не решаете её. Поэтому этот подход на любителя. Но если у вас всё на gRPC, вам должно понравиться.

Приходите на следующий сезон Saint Highload++, чтобы узнать больше про оптимизацию памяти и эффективное распределение ресурсов.

Комментарии (0)