Если вы используете Kubernetes, то, наверное, уже слышали про sidecar-контейнеры. Эта концепция лежит в основе нескольких важных строительных блоков облачной экосистемы, прежде всего service mesh. При этом удивительно, что в самом Kubernetes нет собственного понятия sidecar-контейнеров — по крайней мере, не было до сих пор. Наконец-то грядут долгожданные изменения: предложение по добавлению sidecar-контейнеров (sidecar KEP) войдёт в релиз Kubernetes 1.28, и в API Kubernetes они официально появятся.

Команда VK Cloud перевела руководство по sidecar-контейнерам Kubernetes: что это такое, для чего они существуют и что изменилось в Kubernetes 1.28

Поды, контейнеры и init-контейнеры


Прежде чем поговорить о sidecar-контейнерах, давайте вспомним, как в Kubernetes организованы компоненты приложения.

Основной блок выполнения и адресации в Kubernetes это под. Он содержит один или несколько контейнеров. Каждому поду присваивается уникальный IP-адрес, одинаковый для всех контейнеров в поде. Каждый контейнер может слушать разные порты, но все контейнеры в поде привязаны к этому IP-адресу и входят в состав этого пода — когда «умирает» под, «умирают» и они.

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

Свойства у разных подов Kubernetes бывают разные, а вот на разные типы поды не подразделяются. Поды это поды. Зато про контейнеры так не скажешь. В Kubernetes выделяют два типа контейнеров: обычные и init-контейнеры.

Как ясно из названия, init-контейнеры запускаются в момент инициализации пода, ещё до того, как он становится готов к работе. Через некоторое время init-контейнеры завершаются. Когда завершаются все init-контейнеры пода, запускаются обычные, non-init, контейнеры. Они обычно работают до тех пор, пока не завершится сам под. Есть и некоторые исключения. Наиболее распространённый паттерн — это рабочие нагрузки вроде Jobs, которые завершаются после выполнения указанной задачи.


Порядок контейнеров в подах Kubernetes. В этой последовательности init-контейнеры запускаются первыми, после них обычные контейнеры, которые работают одновременно друг с другом до завершения

Это отличие init-контейнеров от обычных становится ключевым моментом в функциональных возможностях sidecar, представленных в Kubernetes 1.28. Но прежде чем перейти к этой теме, давайте, наконец, рассмотрим sidecar-контейнеры.

Так что же такое sidecar-контейнер


До сих пор за всю историю Kubernetes sidecar было просто названием паттерна, а не собственно функцией Kubernetes. Sidecar — это контейнер, который работает «рядом» с основным контейнером приложения, примерно как люлька, прикреплённая к мотоциклу. Это была просто условность — единственное различие между sidecar- и любым другим контейнером было в том, что он делает.

Но чем так интересна идея, что контейнер может работать рядом с другим контейнером? Почему она удостоилась отдельного термина? Потому что паттерн sidecar предоставляет нечто действительно мощное: 

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

Это определение — ключ к пониманию эффективности sidecar-контейнеров и, думаю, самой модели подов в Kubernetes.

«‍Возможность добавить что-то к приложению во время его работы без необходимости изменения кода». С помощью sidecar-контейнеров можно добавить функциональность в приложение без необходимости перекомпилировать его, подключать новые библиотеки или изменить в нём ещё что-то. Мало того что для этого не требуется участие разработчиков, sidecar-контейнеры могут ещё работать с приложениями, написанными на любом языке.

«В том же операционном контексте и контексте безопасности, что и у самого приложения». Этот момент заслуживает особого внимания. Поскольку sidecar-контейнеры работают в тех же подах (а не в какой-то другой части системы), с ними можно эффективно обращаться как с составной частью приложения. Сбой sidecar-контейнера равнозначен сбою пода, в котором он работает. Если у sidecar-контейнера брешь в системе безопасности, её масштабы ограничены подом. Если sidecar-контейнер потребляет немыслимое количество ресурсов, это то же самое, как если бы их потребляло само приложение. Все механизмы Kubernetes для устранения этих ситуаций подходят и для приложений с sidecar-контейнерами.

Сочетание «могу дополнить приложение, не задействуя разработчиков» и «могу обращаться с этими дополнениями так же, как с самим приложением» — ценнейший бонус для владельцев платформ, особенно с учётом того, что большинство дополнений, которые предоставляют проекты с sidecar-контейнерами вроде service mesh, это скорее функции платформы, а не бизнес-логика.

Распространённые примеры sidecar-контейнеров в Kubernetes — это агенты метрик, которые измеряют параметры работы контейнеров приложения и передают данные в центральное хранилище метрик, или агрегаторы логов, которые регистрируют логи контейнеров и передают их дальше. Но возможно, самый яркий пример sidecar-контейнеров в действии — это service mesh.

Service mesh использует модель sidecar, добавляя в каждый под прокси-сервер. Эти sidecar-контейнеры перехватывают все TCP-соединения с приложением и из него и обрабатывают запросы к этим соединениям, реализуя впечатляющий список функций — эти прокси поддерживают такие функции, как Mutual TLS между подами, детальные метрики HTTP по каждому компоненту, повторные запросы, прозрачное взаимодействие между кластерами и многое другое. Что именно представляют собой эти прокси, зависит, собственно, от специфики реализации. Многие service mesh используют популярный балансировщик Envoy, но, например, в Linkerd используются ультралёгкие «микропрокси», написанные на языке Rust, — подход, который повышает производительность и эффективность, значительно сокращая сложность и потребление ресурсов. (Подробнее о нашем решении перейти на Rust.)

В чём выражается эффективность модели sidecar? Service mesh вроде Linkerd может не только прозрачно добавлять эти функции, но и масштабировать их в приложении и при этом не вводить в кластер единые точки отказа.

Действительно ли нам нужны sidecar-контейнеры? Нельзя ли всё это делать в сети?


Несмотря на свою популярность, sidecar-контейнеры всегда оставались неоднозначной темой. Для тех, кто приходит к service mesh, скажем, с бэкграундом владельца платформы или приложения, ценность модели sidecar очевидна: получаешь преимущества общей библиотеки практически без единого её недостатка. Я исхожу именно из этой точки зрения.

Для тех же, кто считает, что service mesh — это networking layer, идея, что сетевая логика L7 должна размещаться ближе к приложению, а не «в ядре» или по крайней мере «где-то, где её не видно, когда смотришь на приложение», противоречит традиционным отношениям между приложением и сетью. В конечном счёте каждый раз, когда вы смотрите на под, sidecar-контейнер service mesh смотрит вам в глаза! С «сетевой» точки зрения это странное и не очень приятное нарушение.

Фактически это разделение укоренилось с появлением service mesh без sidecar-контейнеров. Давайте оставим за скобками безжизненный маркетинг этого подхода в стиле мы «реализуем service mesh с помощью волшебных технологий под названием eBPF или HBONE». В конечном счёте все эти service mesh перемещают логику ядра в прокси скорее на уровне ноды или пространства имён, а не sidecar-контейнера. Иными словами, прокси никуда не исчезли, просто теперь они больше не в подах.

Мы в Linkerd тщательно изучили эти подходы и в итоге не приняли ни один из них. Здесь можно много говорить о компромиссах, плюсах и минусах; если вам нужно хорошее средство от бессонницы, у меня есть детальная оценка eBPF и её пользы для service mesh. Достаточно будет сказать, что трафик L7 отличается от трафика L4 и что вы теряете критически важные гарантии изоляции и безопасности, которые получаете с sidecar-контейнерами. Думаю, в конечном счёте вы отступаете далеко назад в плане безопасности и качества эксплуатации.

Но в рамках этой статьи эти мои взгляды можно и проигнорировать. Мы здесь обсуждаем sidecar-контейнеры! И не всё гладко в Датском королевстве. У sidecar-контейнеров есть действительно серьёзные недостатки. Давайте выясним, в чём дело.

Побочные эффекты sidecar-контейнеров


Мощная модель подов в Kubernetes является в то же время и источником некоторых неприятностей с sidecar-контейнерами. Вот основные из них:

  1. Чтобы обновить sidecar-контейнеры, нужно перезапустить под целиком. Поскольку поды Kubernetes — вещь неизменяемая, нельзя изменить один контейнер, не перезапустив весь под вместе с приложением. По идее, приложения Kubernetes должны справляться с произвольными перезапусками и сбоями пода, и в Kubernetes есть всевозможные механизмы вроде непрерывного перезапуска, которые обеспечивают нулевое время простоя рабочей нагрузки в целом. Но всё равно это раздражает.
  2. Sidecar-контейнеры могут помешать завершению заданий. Помните, что я писал про задания выше? Задание — это тип рабочей нагрузки Kubernetes, которая должна самостоятельно завершаться после выполнения. К сожалению, в Kubernetes не поддерживается способ как-то сигнализировать sidecar-контейнеру, что ему следует завершаться, то есть по умолчанию поды с заданиями должны работать вечно. Есть специальные рабочие нагрузки, позволяющие это исправить (наша называется linkerd-await), но для них в контейнер приложения нужно вносить изменения, нарушающие прозрачность service mesh.
  3. Sidecar-контейнеры могут конкурировать с приложением при запуске. Хотя Kubernetes запускает init-контейнеры в определённом порядке, он не гарантирует очерёдность выполнения обычных контейнеров. Это значит, что во время запуска приложению не гарантируют, что sidecar-контейнер готов к работе. Это может вызывать проблемы, если sidecar-контейнер нужен, например, для подключения по сети к внешнему миру. (Есть пара трюков, с помощью которых мы в Linkerd это обходим.)

Конечно, не то чтобы это критические моменты, но всё это раздражает: периодически нужно находить лайфхаки или обходные пути.

Что за лайфхаки? Для ознакомления: оказывается, перехватчик postStart действительно предотвращает race condition для контейнеров, которые, хотя этого и нет в спецификациях, не дают запуститься следующему контейнеру, пока перехватчик не завершится. Так что Linkerd добавляет в под свой прокси как первый контейнер и использует перехватчик postStart, чтобы запускать его перед другими приложениями. Победа над race condition! Но поскольку сейчас в спецификации пода первый контейнер это Linkerd, инструменты вроде логов kubectl выдают по умолчанию логи прокси Linkerd, а не логи приложения. Это очень раздражает пользователей. Но не стоит беспокоиться: в Kubernetes 1.27 есть понятие контейнера по умолчанию, который можно указать как контейнер приложения. Таким образом можно заставить kubectl делать то, что нужно. Но не для каждого кластера в Kubernetes 1.27… и т. п. Вся система какая-то хрупкая и беспорядочная, но таково уж положение дел на сегодня.

До сих пор именно так обстояли дела с использованием sidecar-контейнеров в Kubernetes: в основном всё работает, но кое-где торчат гвозди, которые постоянно задеваешь. И вот, наконец, Kubernetes 1.28 спешит на помощь!

Что изменится в 1.28


Давайте, наконец, поговорим о Kubernetes 1.28 и sidecar KEP. Это предложение Kubernetes Enhancement Proposal появилось в 2019 году — ему предстояли годы дополнений, стагнации, редизайна и угрозы закрытия проекта. В мире Kubernetes он превратился в локальный мем!


Реакция сообщества Kubernetes в Reddit на то, что KEP снова не попал в очередной релиз Kubernetes

Похоже, четыре года спустя KEP для sidecar-контейнеров наконец попал в число новых функций ожидаемого релиза 1.28. Это заметно меняет работу с init-контейнерами. 

KEP вводит новое поле RestartPolicy в спецификации init-контейнеров. Вот его описание:

RestartPolicy определяет, как выполняется перезапуск отдельных контейнеров в поде. Это поле предназначено только для init-контейнеров, и единственное допустимое значение в нём — Always. Для non-init-контейнеров или когда значение этого поля не задано, характер перезапуска определяется политикой перезапуска пода и типом контейнера. Если задать значение Always в поле RestartPolicy для init-контейнера, он будет постоянно перезапускаться после выхода, пока не завершат работу все обычные контейнеры. После завершения всех обычных контейнеров все init-контейнеры со значением Always в поле RestartPolicy будут остановлены. У таких контейнеров жизненный цикл отличается от работы стандартных init-контейнеров, и их часто называют sidecar-контейнерами. Хотя этот init-контейнер всё так же запускается в последовательности init-контейнеров, он не ждёт, пока контейнер закончит работу, чтобы после этого перейти к следующему init-контейнеру. Вместо этого следующий init-контейнер запускается сразу же после этого init-контейнера или после успешного завершения любого startupProbe.

Давайте повторим:

  1. Теперь для init-контейнера можно задать новую конфигурацию RestartPolicy:Always.
  2. Если добавить эту новую конфигурацию, получается sidecar-контейнер.
  3. Sidecar-контейнер запускается раньше всех обычных контейнеров (потому что это init-контейнер) и — это самое важное — завершается после завершения всех обычных контейнеров.
  4. Если по какой-то причине sidecar-контейнер перестаёт работать, пока остальные контейнеры ещё выполняются, он перезапускается автоматически. За это отвечает настройка Always.
  5. Наконец, в отличие от обычных init-контейнеров, которые запускаются по очереди, после завершения предыдущего, другие init-контейнеры запускаются, не дожидаясь завершения sidecar-контейнеров. И это хорошо, потому что они завершаются нескоро.

В сущности, это KEP вводит режим, очень похожий на «фоновый контейнер» (или, как его называли раньше, «контейнер TSR»). Эти новые sidecar-контейнеры запускаются до обычных контейнеров и завершаются после них. Одним лёгким движением руки исчезают пункты 2 и 3 из нашего списка недостатков sidecar-контейнеров: теперь мы можем автоматически завершать их, когда завершается задание, и у нас больше нет состояния гонки при запуске, которое теснит обычные контейнеры. А ещё наконец можно избавиться от проблем с перехватчиками postStart и контейнерами по умолчанию.


Последовательность контейнеров в поде Kubernetes с новыми sidecar-контейнерами в версии Kubernetes 1.28. Sidecar-контейнеры не блокируют non-sidecar init-контейнеры и завершаются после завершения обычных контейнеров

Победа! С этой новой функцией sidecar-контейнеры снова стали классными. Или нет?

Славное будущее, которое ждёт нас в Kubernetes 1.28


Я назвал эту статью «Sidecar-контейнеры возвращаются», но это, конечно, явное преувеличение. Предложение KEP для sidecar-контейнеров устраняет два самых серьёзных недостатка Kubernetes в использовании sidecar-контейнеров: проблемы с поддержкой заданий и race condition при запуске контейнера. Сегодня для каждой из этих проблем есть обходные пути, но с неприятными побочными эффектами для владельца mesh; в Kubernetes 1.28 и более поздних версий от таких решений можно будет полностью отказаться.

Значит ли это, что теперь в Датском королевстве всё спокойно? Лично я думаю, что некоторые сложности для признания sidecar-контейнеров широкими массами всё ещё остаются. Наиболее серьёзная из них (что весьма иронично) связана с видимостью: поскольку sidecar-контейнеры видны в каждом поде, a) это не вписывается в наше традиционное представление о сети; и б) влияние sidecar-контейнеров на использование ресурсов очень заметно. Если общая библиотека использовала 200 Мбайт памяти и 5% CPU, вы, возможно, вообще этого не заметите; если столько же ресурсов израсходовал sidecar-контейнер, это очень бросается в глаза. (К счастью, для Linkerd характерно очень экономное расходование ресурсов, отчасти за счёт отличных средств распределения ресурсов памяти в Rust.)

В прошлом году я в шутку заявил, что мы собираемся удалить sidecar-контейнеры из Linkerd… скрыв их из результатов kubectl (с помощью eBPF, вот так-то)! К несчастью для меня, подписчики отреагировали с таким бурным энтузиазмом, что мне пришлось выступить с опровержением. Мы на самом деле не собирались разветвлять kubectl. Но в каждой хорошей шутке (?) есть доля правды. Мне вот интересно, насколько сошли бы на нет рассуждения в стиле «нужно избавиться от sidecar-контейнеров», если бы они не исчезли, а просто были не видны или скрыты настройками.

С учётом всего сказанного цель нашей работы в Linkerd — предоставлять самую быструю, легковесную и простую в обращении service mesh в мире (то есть service mesh с минимальным TCO). И пока что ничто нас не убедило, что в решении этой задачи что-то может сравниться с sidecar-контейнерами. 

Вы прямо сейчас можете воспользоваться Kubernetes от VK Cloud. Для тестирования мы начисляем новым пользователям 3 000 бонусных рублей и будем рады вашей обратной связи.

Stay tuned

Присоединяйтесь к телеграм-каналу «Вокруг Kubernetes», чтобы быть в курсе новостей из мира K8s: регулярные дайджесты, полезные статьи, а также анонсы конференций и вебинаров.

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


  1. shurup
    16.11.2023 03:31
    +1

    предложение по добавлению sidecar-контейнеров (sidecar KEP) войдёт в релиз Kubernetes 1.28,

    Здесь и в других местах статьи про v1.28 говорится в будущем времени, но это релиз уже состоялся (в августе), а скоро выходит v1.29. В то же время важно помнить, что пока эта фича имеет статус Alpha.