Kubernetes: продолжаем говорить о контейнерах и архитектуре
Каждый раз залипаю на анимацию работы Raft-протокола
В прошлый раз мы говорили о контейнерах, механизмах Namespace и о том, как это всё работает.
Сегодня мы пойдём дальше — к системам оркестрации, так как нам мало просто иметь контейнер. Нам нужно управлять сетью, распределять нагрузку и вовремя поднимать упавшие контейнеры и свои собственные компоненты.
Попробуем немного отойти от классического «yaml-программирования» и заглянуть под капот.
Коснёмся CAP-теоремы, важности ETCD и причинах его устойчивости к split-brain-проблеме. А ещё посмотрим, почему Kubernetes API можно распределить на кучу инстансов, работающих одновременно, а Controller Manager может работать только в одном экземпляре за раз.
Вот о том, как всё это работает в Kubernetes, кто и зачем тыкает в API, мы сегодня и поговорим.
CAP-теорема и Raft-протокол
Перед тем как начать, давайте для начала коснёмся такой важной темы, как обеспечение консистентности данных в распределённых системах. Одна из ключевых ценностей Kubernetes — это его способность к самостоятельному решению проблем как с отдельными контейнерами, так и с собственными компонентами. Чтобы построить подобную архитектуру, мы с самого начала предполагали, что роутер может зависнуть, fiberfinder (трактор) порвать сеть, а СХД с критичными метаданными задымиться в стойке. При этом у нас одновременно стоит задача гарантировать то, что все ключевые запросы из разных сегментов инфраструктуры будут отдавать один и тот же результат. Если пользователю admin_01 разрешено выполнять одну группу API-запросов, но нельзя другую, то это правило должно выполняться при запросах к узлам в любом дата-центре единой инфраструктуры.
Выбрать можно только два свойства из трёх
Проблема доступности и консистентности очень хорошо иллюстрируется теоремой Брюйера, она же — CAP-теорема. Коротко она формулируется следующим образом: у распределённых систем хранения данных есть три фундаментальных взаимопротиворечивых свойства, из которых система может одномоментно реализовать только два:
- Consistency — консистентность данных. Каждое чтение возвращает самую последнюю версию данных или ошибку.
- Availability — доступность. Каждый запрос возвращает ответ без ошибки, но без гарантий того, что в ответе будет самая последняя версия данных.
- Partition tolerance — устойчивость к расщеплению. Каждый запрос вернёт ответ или ошибку в ситуации сетевого расщепления, когда узел или группа узлов потеряли связь с остальными. Всё это — без гарантий того, что в ответе будет самая последняя версия данных.
На практике системы бывают двух типов — CA и CP. Просто потому, что система AP-типа, которая не может гарантировать устойчивости к расщеплению, должна работать на базе инфраструктуры, где каждый пакет гарантированно доставляется, а любые её узлы никогда не падают. На практике это просто невозможно. Поэтому в менее строгом варианте эта теорема формулируется следующим образом:
«В случае расщепления распределённой системы на изолированные сегменты она может быть либо доступной, либо консистентной».
В случае Kubernetes для нас допустимы, хоть и нежелательны, ошибки при ответе на запрос к распределённому key-value-хранилищу, а неконсистентность данных совершенно недопустима.
Например, если мы отозвали права на какие-то действия у пользователя, то ожидаем, что API в любом случае не разрешит выполнения запроса, даже если особо старательный экскаваторщик внезапно рассечёт кластер на несколько несвязанных сегментов.
Именно поэтому в самой основе его архитектуры лежит ETCD — консистентное key-value-хранилище CP-типа на базе протокола Raft. По своим свойствам ETCD сильно напоминает Consul от Hashicorp.
Этап выбора лидера кластера в симуляторе протокола
Для хорошего понимания лучше всего попробовать разобрать протокол в интерактивном виде на thesecretlivesofdata.com/raft. Отмечу несколько ключевых его особенностей:
- В кластере возможен только один лидер.
- Лидер может только добавлять новые записи в свой лог, но не может ни перезаписывать, ни удалять их.
- Ни один узел не может внести запись в лог, если другой узел уже внёс в него свои правки.
- В случае распада кластера на сегменты иметь лидера и писать сможет только сегмент, содержащий как минимум N/2+1 узлов, где N — общее число узлов в кластере.
Эти свойства достигаются следующим образом:
- Узел не может добавлять записей в лог, если он не лидер.
- Если узел оказался изолированным от большинства, то он не сможет стать лидером и не сможет ничего записать.
- Чтобы участвовать в выборах, получить право стать лидером и писать, узлу нужно синхронизировать своё состояние с остальным кластером.
В результате мы получаем то самое отказоустойчивое, непротиворечивое и консистентное хранение данных нашего Kubernetes в ETCD.
Декларативность как базовый принцип
Ключевая идея, лежащая в основе работы с Kubernetes, — декларативный подход. Для того чтобы наше приложение работало как задумано, оно должно запускаться определённым образом внутри нашего кластера, для него должны прокидываться сети, обеспечиваться запуск нужных инстансов в нужном порядке и тому подобное. Самая большая ценность такой архитектуры в том, что мы ничего не выполняем вручную, а лишь декларативно описываем желаемое состояние.
Начинаем мы с того, что создаём контейнер с нашим приложением. Как мы уже говорили в прошлых постах, image, из которого собирается контейнер, — это маленький слепок операционной системы с минималистичным окружением, необходимым для запуска приложения.
Всё, что нужно этому слепку для работы, — это ядро хост-системы, на котором будет стартовать контейнер. Все остальные зависимости и ресурсы уже упакованы внутрь.
Какие-то конфигурационные данные могут меняться от среды к среде, но всё остальное будет уже изолировано внутри образа.
Теперь я, например, хочу, чтобы у меня было 10 контейнеров Redis и пять контейнеров с бэкендом приложения. А ещё пусть всё это будет доступно из внешнего мира через Ingress.
Дальше Kubernetes сам должен решить, на какие узлы будет более оптимально распределить нагрузку, как прокинуть сети и как следить за работоспособностью всех контейнеров. Например, он сам будет решать, что на каких-то инфраструктурных узлах не хватает ресурсов и лучше бы развернуть новые контейнеры в другом месте. Разумеется, мы можем сконфигурировать наше приложение с более жёсткими требованиями к развёртыванию и всячески требовать от Kubernetes соблюдения каких-то мелочей и что-то ему запрещать. Тем не менее в идеальном мире оркестратор справляется со всем этим самостоятельно.
Если перестараться, то можно прийти к концепции, близкой к той, что я видел у пожилых знакомых. Сын подарил им робота-пылесоса, чтобы снять с них скучные рутинные задачи по ежедневной уборке. Как оказалось, у хозяйки дома и робота были разные представления об оптимальном порядке уборки. В результате каждый запуск пылесоса превращался в сложную систему поочерёдного открывания и закрывания дверей, нежного подталкивания шваброй в нужную сторону и строительства перегородок в стратегических проходах. Всё это для того, чтобы запуск и работа проходили в том порядке, который хозяйка считала правильным. В контексте ПО такой подход скорее антипаттерн.
Промышленного скота не жалко
Просто так загнать наше приложение в оркестратор не получится. Для начала мы должны быть уверены, что оно изначально отвечает принципам Cloud native ПО и готово работать в том окружении, которое обеспечивает Kubernetes.
В случае если ваше приложение — stateful, а каждый узел заботливо настраивается вручную, то оно относится скорее к категории питомца (pet), нежели cattle (скот). Например, вы создали четыре инстанса вашего приложения, тщательно его сконфигурировали и своими собственными руками бережно запускаете. За работоспособностью каждого узла вы внимательно следите и чутко реагируете на каждый чих мониторинга. Это классический pet-подход. Если у собаки начала протекать будка, то вы расстраиваетесь и бежите немедленно её ремонтировать. Если животное заболело, то вы бегаете с ним по ветеринарам и лечите до последнего: потеря питомца — большая потеря.
В случае облачного подхода мы реализуем концепцию взаимодействия, аналогичного промышленному крупному рогатому скоту. Все наши инстансы — это безликие и безымянные номерные сущности. Нам совершенно неважны судьба каждой отдельной особи и номер загона, где она обитает. В любой непонятной ситуации нормальным подходом будет переработка в котлеты вместе с будкой и заменой на новый экземпляр. Микросервисная архитектура, хоть и даёт свой оверхед, позволяет прибивать пачками приунывшие узлы, поднимать их обратно и наращивать их число при необходимости. Потеря нескольких инстансов не должна оказывать никакого влияния на конечного пользователя.
Чтобы всё это корректно работало, а множество микросервисов могло взаимодействовать друг с другом, все компоненты кластера должны иметь полноценную сетевую связность друг с другом.
Для этого используется дополнительный уровень абстракции в виде overlay-сетей, предоставляющих сетевую инфраструктуру в удобном масштабируемом виде. Оверлейные сети в Kubernetes абстрагируют сетевую конфигурацию и детали реализации от подов и контейнеров. Они предоставляют виртуальную сетевую среду, в которой контейнеры могут обмениваться данными, не беспокоясь о том, на каком физическом хосте они размещены или каким образом реализована сеть между хостами.
Kubelet. Принципы работы
Ключевые архитектурные элементы кластера
Давайте пройдёмся по ключевым инфраструктурным элементам. Начнём с Kubelet. Это ключевой сервис, который отвечает за управление жизненным циклом подов и контейнеров.
Основные функции Kubelet:
- Контейнеризация. Kubelet отвечает за запуск, остановку и управление контейнерами на узле в соответствии с указаниями из API-сервера Kubernetes. Он следит за состоянием подов и управляет контейнерами, чтобы поддерживать желаемое состояние, определённое в объектах ReplicaSet, Deployment и других контроллерах развёртывания.
- Взаимодействие с API-сервером. Kubelet взаимодействует с API-сервером Kubernetes для получения информации о планирующихся к деплою подах, а также для отчёта о состоянии и доступности узла. Это позволяет API-серверу отслеживать состояние кластера и координировать работу Kubelet на разных узлах.
- Мониторинг ресурсов. Kubelet мониторит ресурсы узла, такие, как использование CPU, память, дисковое пространство и т. д. Он передаёт эти метрики в API-сервер, чтобы система планирования знала, на каком узле размещать новые поды.
- Обработка Health Checks. Kubelet проверяет здоровье контейнеров, выполняя заранее определённые Health Checks. Если контейнер не отвечает на Health Check или его состояние становится некорректным, то Kubelet может перезапустить контейнер, чтобы восстановить его работоспособность.
- Работа с Volume. Kubelet обрабатывает монтирование Volume (томов) в контейнеры. Он управляет связыванием томов, определённых в манифестах подов, с соответствующими файловыми системами или другими хранилищами узла.
- Работа с сетью. Kubelet работает с CNI-плагином (Container Network Interface), чтобы обеспечить сетевую изоляцию и обмен данными между контейнерами в различных подах.
Kubernetes API
Давайте перейдём ещё к одному ключевому компоненту, без которого кластер Kubernetes не будет работать. Это Kubernetes API-сервер.
Упрощённо стандартный workflow выглядит так:
Мы делаем запрос в API → Запрос проходит валидацию, ряд проверок и сохраняется в ETCD — key-value-хранилище → Другие сервисы, например, тот же Kubelet, стучатся в API и получают список задач из этого хранилища.
Сразу возникает вопрос:, а зачем вообще нужен этот API, если сервисы могли бы напрямую ходить в ETCD? У подхода с API есть несколько ключевых преимуществ:
- Валидация. API-сервер Kubernetes выполняет валидацию данных, которые приходят от клиентов, прежде чем они будут записаны в ETCD. Это включает проверку синтаксиса, правильности значений и других ограничений, определённых в спецификации API.
- Безопасность. Обращение к ETCD через API-сервер позволяет контролировать доступ к данным и обеспечивать безопасность. API-сервер выполняет аутентификацию и авторизацию запросов от компонентов кластера, чтобы предотвратить несанкционированный доступ к данным ETCD.
Этапы обработки API-запроса
Кроме того, в API есть ещё один очень важный компонент, о котором мы поговорим чуть позже. Admission Controllers — расширения API-сервера, которые позволяют внедрять дополнительные проверки и манипуляции перед тем, как запросы будут приняты или отклонены.
Я бы хотел обратить внимание на очень важный момент: API при всей его критичности не хранит никаких данных. Он реализует только логику и занимается исключительно обработкой запросов от пользователей и компонентов Kubernetes. Именно поэтому этот компонент не кластеризуется. У вас может быть одна нода API, может быть несколько. Конфликт между ними невозможен, так как за консистентное хранение состояния отвечают ETCD-бэкенд и Raft-протокол.
Как мы уже разобрали ранее, в Raft-протоколе писать может только лидер. Если лидеру станет плохо, то его быстро пометят как уставшего и выберут нового. API может запросить любой узел ETCD и попросить внести какие-то изменения в конфигурацию. Если ближайшая нода окажется read-only, то она просто проксирует этот запрос к лидеру, и тот внесёт нужные изменения.
Кстати, есть один полезный момент, который можно использовать в работе. Если зайти в ~/.kube/cache/discovery/<ИМЯ КЛАСТЕРА С КОТОРЫМ РАБОТАЛИ Kubectl>, то можно выдернуть в виде JSON описание всех API-групп, историю deployment и прочую полезную информацию. Только надо прогнать через jq, чтобы было удобнее смотреть.
28290@w-msk00-vdi252e MINGW64 ~/.kube/cache/discovery/mycoolcluster_443/security.istio.io/v1beta1
$ cat serverresources.json | jq
{
"kind": "APIResourceList",
"apiVersion": "v1",
"groupVersion": "security.istio.io/v1beta1",
"resources": [
{
"name": "peerauthentications",
"singularName": "peerauthentication",
"namespaced": true,
"kind": "PeerAuthentication",
"verbs": [
"delete",
"deletecollection",
"get",
"list",
"patch",
"create",
"update",
"watch"
],
"shortNames": [
"pa"
],
"categories": [
"istio-io",
"security-istio-io"
],
"storageVersionHash": "0IW8zQBF4Qc="
},
{
"name": "peerauthentications/status",
"singularName": "",
"namespaced": true,
"kind": "PeerAuthentication",
"verbs": [
"get",
"patch",
"update"
]
},
{
"name": "requestauthentications",
"singularName": "requestauthentication",
"namespaced": true,
"kind": "RequestAuthentication",
"verbs": [
"delete",
"deletecollection",
"get",
"list",
"patch",
"create",
"update",
"watch"
],
"shortNames": [
"ra"
],
"categories": [
"istio-io",
"security-istio-io"
],
"storageVersionHash": "mr+dytYVwr8="
},
{
"name": "requestauthentications/status",
"singularName": "",
"namespaced": true,
"kind": "RequestAuthentication",
"verbs": [
"get",
"patch",
"update"
]
},
{
"name": "authorizationpolicies",
"singularName": "authorizationpolicy",
"namespaced": true,
"kind": "AuthorizationPolicy",
"verbs": [
"delete",
"deletecollection",
"get",
"list",
"patch",
"create",
"update",
"watch"
],
"categories": [
"istio-io",
"security-istio-io"
],
"storageVersionHash": "djwd/cy0e/E="
},
{
"name": "authorizationpolicies/status",
"singularName": "",
"namespaced": true,
"kind": "AuthorizationPolicy",
"verbs": [
"get",
"patch",
"update"
]
}
]
}
Admission Controller
Как мы уже разобрали, API выступает в качестве важной прослойки между компонентами и распределённым бэкендом в ETCD. Его функциональность можно существенно дополнить ещё одной прослойкой — Admission Controller.
Admission Controller — это расширение API-сервера Kubernetes, которое позволяет выполнять дополнительные проверки, модификации и манипуляции с объектами Kubernetes перед тем, как они будут созданы, изменены или удалены в кластере. Он предоставляет механизм для внедрения пользовательской логики на этапе входящего запроса к API-серверу, что обеспечивает дополнительные уровни безопасности и валидации.
Admission Controllers могут быть запущены как встроенные (built-in) или пользовательские (custom). Встроенные Admission Controllers уже включены в API-сервер, и их можно просто включать при необходимости. Вот некоторые типовые встроенные варианты:
- NamespaceLifecycle. Управляет жизненным циклом пространств имён, позволяя разрешить или запретить их создание и удаление.
- ResourceQuota. Контролирует количество ресурсов (CPU, память) и объектов (поды, сервисы) в пространствах имён.
- SecurityContextDeny. Запрещает создание объектов без установленных Security Context, что повышает безопасность.
- NamespaceExists. Проверяет существование пространства имён, прежде чем разрешить создание объектов в нём.
Admission Webhook — ещё один очень полезный компонент, который может дополнительно улучшить безопасность, проверяя допустимость тех или иных API-запросов перед их выполнением. Он представляет собой HTTP-сервер, который используется в Admission Controller для выполнения дополнительных проверок и манипуляций с объектами Kubernetes перед их созданием, изменением или удалением.
Например, мы хотим что-то задеплоить, но при этом у нас обязательно нужно описывать Network Policy, чтобы ограничить возможности пода взаимодействовать с другими компонентами. Но мы, негодяи, взяли и отправили запрос без этого описания.
Admission Controller увидит несоответствие и вернёт отказ через Admission Webhook в формате AdmissionReview API Kubernetes. Этот ответ содержит информацию о том, разрешён запрос или нет, а также может включать в себя какие-то дополнительные данные.
Controller Manager
Ещё один критически важный компонент. У нас есть «стадо приложений» — тот самый cattle. В нашем манифесте мы декларативно описали, что нам нужно, чтобы численность «стада» была ровно 100 голов. Соответственно, нужен какой-то пастух-администратор, который будет это самое стадо постоянно пересчитывать, при необходимости отстреливая избыточных бурёнок и пополняя недостающих.
Наше целевое состояние было пропущено через API и сохранено в ETCD, который гарантирует консистентность данных по всему кластеру. Controller Manager будет периодически тыкать в API и запрашивать из ETCD целевое состояние системы, сравнивать с текущим и при необходимости его корректировать.
Вот некоторые типы контроллеров, за которыми следит Controller Manager:
- ReplicaSet Controller. Контролирует количество запущенных реплик подов для объектов ReplicaSet. Он поддерживает желаемое количество реплик и может запускать или останавливать поды, чтобы поддерживать этот желаемый уровень.
- Deployment Controller. Управляет развёртываниями (Deployments) и обеспечивает желаемое количество реплик, включая возможность обновления версий приложений.
- StatefulSet Controller. Обрабатывает StatefulSet, предоставляющий уникальные идентификаторы для подов в режиме управления состоянием.
- DaemonSet Controller. Управляет DaemonSet, который обеспечивает запуск по одной реплике пода на каждом узле кластера.
- Job Controller. Управляет заданиями (Jobs), обеспечивая выполнение задачи и отслеживание статуса выполнения.
- Service Controller. Следит за объектами Service и обновляет Endpoints для сервисов, чтобы обеспечить правильную маршрутизацию трафика к подам.
- Endpoint Controller. Обновляет Endpoints для сервисов на основе изменений в Pod и Service.
- Namespace Controller. Обрабатывает пространства имён (Namespaces), создавая и удаляя их по запросу.
Ещё один важный момент. По аналогии с инстансами API мы можем запустить сколько угодно Controller Manager для отказоустойчивости. Но в отличие от API активным может быть только один Controller Manager. Например, два инстанста работают активно одновременно и увидели новый deployment. Тут же кинулись создавать Replica Set и получили его задвоение, хотя в основе был консистентный и единый ETCD. Именно поэтому Controller Manager тоже использует механизм голосования и выбирает лидера, чтобы этого избежать. Именно лидер уже и принимает нужные решения по поддержанию «численности стада». Все остальные будут ждать в резерве для отказоустойчивости.
Scheduler
Scheduler (планировщик) в Kubernetes — это компонент системы управления кластером, который отвечает за размещение подов на доступных узлах в кластере. Он отвечает за то, чтобы кластер не перекосило от нагрузки, а все инстансы запускались максимально равномерно.
Scheduler распределяет поды по узлам кластера так, чтобы нагрузка была равномерно распределена. Он учитывает доступные ресурсы на каждом узле, такие, как CPU, память и дисковое пространство, чтобы предотвратить перегрузку или неэффективное использование ресурсов.
При необходимости он будет отвечать за автоматическое масштабирование на основе текущей нагрузки. Если кластер перегружен, то планировщик может поднять дополнительные поды для того, чтобы сгладить пиковую нагрузку. Кроме того, Scheduler учитывает требования к георезервированию и предотвращает размещение нескольких реплик одного пода на одном узле.
Это обеспечивает высокую доступность, так как при сбое узла другие реплики подов продолжают работать на других нодах.
Результатом работы Scheduler является скоринг. То есть он находит оптимальное место для размещения ресурса и делает пометку в ETCD через API. Потом в API стучится Kubelet, считывает информацию и видит, что ему нужно запустить вон тот под на этом узле.
Инструмент крайне гибкий, и его можно настроить под свои нужды как угодно. Если у вас пока нет понимания, чего вы от него хотите, то разумным будет оставить те самые sane defaults «из коробки».
Для упрощения настройки могу порекомендовать такую штуку, которая называется Kube Scheduler simulator. В ней можно погонять в GUI различные сценарии и подобрать более оптимальные варианты.
Kubernetes не прибит гвоздями к Docker
Если я сейчас покажу вам свой домашний Kubernetes-кластер, то вы внезапно не увидите там Docker. Потыкаем в утилиту lsns для точности и получим кучу namespace, которые принадлежат кубернетовским сервисам, но самого Docker нет.
Основная идея в том, что Kubernetes — это конструктор. Причём он изначально создавался как максимально слабосвязанный и модульный, чтобы была возможность заменять один его компонент другим. Docker по-прежнему — один из наиболее популярных рантаймов, но в некоторых дистрибутивах он вытесняется другими альтернативами:
- Containerd. Containerd — это индустриальный стандарт контейнерного рантайма, который был инициирован из проекта Docker. Со временем Docker реорганизовал свою архитектуру, и большая часть функциональности, ранее включённая в Docker, была перенесена в Containerd.
- Container Runtime Interface (CRI) с runc. Container Runtime Interface (CRI) — это спецификация, предоставляющая стандартизированный интерфейс между Kubernetes и контейнерным рантаймом. runc — это инструмент, реализующий стандарт OCI (Open Container Initiative) для запуска контейнеров. Многие Kubernetes-дистрибутивы используют CRI и runc вместо Docker.
- Containerd с CRI. Containerd также поддерживает CRI-интерфейс, что делает его совместимым с Kubernetes. Kubernetes может взаимодействовать с Containerd через CRI, и это даёт возможность использовать Containerd в качестве контейнерного рантайма вместо Docker.
- CRI-O. CRI-O — это реализация CRI, которая предоставляет простой и легковесный контейнерный рантайм, оптимизированный для использования в Kubernetes. Он предоставляет только функциональность, необходимую для работы с Kubernetes, и является альтернативой Docker для развёртывания контейнеров.
В итоге Docker всё чаще остаётся как удобный инструмент для запуска и подготовки образов на локалхосте и в CI-системах, а в кластере эти образы крутит уже совместимый рантайм, который умеет с ними работать.
Сетевая часть
Давайте напоследок ещё немного коснёмся сетевой части. Как я уже писал ранее, для полноценной работы наших узлов мы должны организовать сетевую связность между отдельными компонентами нашего кластера.
Есть много различных инструментов, которые обеспечивают overlay-сеть, но один из самых распространённых — Flannel. Этот инструмент деплоит маленький бинарник flanneld на каждом хосте. Этот бинарник отвечает за корректное выделение подсетей на каждом хосте из большего, заранее определённого пространства адресов. Flannel, как и другие компоненты, использует API, чтобы хранить данные о выделении адресов в ETCD, связывая всю инфраструктуру в одну Layer3-сеть.
Низкоуровневый дизайн сетевой подсистемы
По сути, под капотом нашего overlay работают те же самые классические Linux-инструменты.
Используя пакет iproute2, создаётся виртуальное сетевое окружение с необходимой конфигурацией.
Интерфейс Virtual Ethernet в контейнерном сетевом пространстве подключается к виртуальному bridge (виртуальный коммутатор) в хостовой машине. Виртуальный bridge объединяет все Virtual Ethernet-адаптеры, что позволяет контейнерам на одной ноде взаимодействовать друг с другом, как будто они взаимодействуют через коммутатор.
Netfilter + iptables
Но нам мало обеспечить связность подов между собой. Нам надо ещё опубликовать наше приложение и сделать его глобально доступным. Доступ во внешний мир идёт с помощью старых классических механизмов на базе модуля ядра Netfilter. Именно он обеспечивает функциональность firewall в Linux. Тот же iptables — это имплементация интерфейса, взаимодействующего с этим модулем и настраивающего сеть.
Что умеет firewall? Firewall, кроме того, чтобы кому-то прикрыть доступ, зарезать пакеты, умеет эти пакеты немножко модифицировать, менять заголовки. На базе этого механизма и работает NAT. Когда на внешний адрес вылетает какой-то пакет из внутренней сети, то по дороге происходит подмена, и для внешнего мира он вылетел уже не от имени контейнера, а от адреса Kubernetes-ноды.
Полноценный же сетевой доступ обеспечивается за счёт отдельной абстрации — Service.
Service предоставляет стабильный виртуальный IP-адрес и DNS-имя для доступа к приложению.
Это позволяет клиентам взаимодействовать с приложением. Причём неважно, на какой ноде оно запущено.
Service распределяет трафик между подами, работающими в составе одного сервиса. Это обеспечивает балансировку нагрузки между подами, что позволяет распределить запросы равномерно и предотвратить перегрузку определённых подов.
Кроме того, этот компонент автоматически обнаруживает появление новых подов и соответствующим образом обновляет свою конфигурацию. Это обеспечивает отказоустойчивость и непрерывную доступность приложения при масштабировании или перезапуске подов.
Задачу по проксированию сетевого трафика, фильтрации и настройке iptables выполняет ещё одна прослойка между Service и Pods — Kube-proxy.
Механизм eBPF в сетевой подсистеме
У Netfilter есть одна важная проблема. Он создавался в древние времена, когда целый интернет-провайдер мог иметь аплинк в 100 мегабит. Сейчас, когда уже и 40 гигабит между серверами не такая уж редкость, Netfilter в чистом виде перестаёт справляться. Для того чтобы обойти эту проблему, внедрили механизм eBPF (extended Berkeley Packet Filter) — это технология, которая позволяет встраивать и исполнять пользовательский код (байт-код) в ядре Linux. Этот механизм позволяет более гибко и оптимально обрабатывать сетевые запросы, чтобы обеспечивать нужный уровень быстродействия для типовых задач в Kubernetes.
Наш итоговый workflow
Давайте ещё раз пробежимся по основному workflow с нашими компонентами.
Вечер пятницы, DevOPS Иван скучает и внезапно решает, что сейчас просто идеальное время ещё для одного деплоя.
- Он готовит манифест, описывающий желаемое состояние приложения, которое он хочет развернуть в Kubernetes-кластере. Манифест содержит описание ресурсов (например, подов, сервисов, деплойментов и т. д.) и их конфигурации.
- Kubernetes API-сервер проводит аутентификацию девопса и проверяет его права доступа, чтобы удостовериться, что он имеет право развернуть приложение в кластере.
- Манифест, представленный Иваном, проходит через Admission Controller, который выполняет дополнительные проверки и валидацию. Это может быть проверка политик безопасности, обязательных полей, ограничений ресурсов и т. д.
- Если манифест проходит успешно все проверки, то он сохраняется в ETCD, что является распределённым хранилищем данных Kubernetes.
- Controller Manager обнаруживает новые сущности (поды, сервисы и т. д.), добавленные в ETCD, и начинает работу по созданию подов для приложения.
- Scheduler обнаруживает созданный под, который ещё не назначен на узел кластера. Он анализирует доступные ресурсы на узлах и проводит скоринг для выбора наилучшей ноды для размещения пода. Scheduler выбирает оптимальную ноду, к которой будет назначен под, и помечает это решение в манифесте пода.
- Kubelet на выбранной ноде обнаруживает под, который нужно деплоить. Он связывается с Kubernetes API-сервером, чтобы узнать, какой контейнер нужно запустить.
- Kubelet отправляет запрос в репозиторий образов (например, Nexus) и выкачивает необходимый образ контейнера.
- После получения образа Kubelet запускает процесс контейнера и обеспечивает его работу на выбранной ноде.
В результате выполнения всей цепочки мы получаем деплой контейнера со всеми промежуточными проверками, квотами и прочим. При правильной конфигурации вся система будет работать максимально отказоустойчиво, соблюдая принципы георезервирования и равномерно распределяя нагрузку на инфраструктуру.