Изучаем контроллеры и операторы Kubernetes

Kubernetes использует контроллеры, которые выполняют рутинные задачи, чтобы гарантировать, что желаемое состояние кластера соответствует наблюдаемому состоянию. Например, контроллер ReplicaSet поддерживают правильное количество подов, запущенных в кластере. Контроллер узлов (Node Controller) проверяет состояние серверов и реагирует, когда серверы выходят из строя. По сути, каждый контроллер отвечает за определенный ресурс в мире Kubernetes. Чтобы пользователи могли управлять своим кластером, важно, чтобы пользователи понимали роль каждого контроллера в Kubernetes. Более того, каждый может написать свой контроллер для определенных задач. Все блоки кода, которые я использую в этом посте, взяты из текущей реализации контроллеров Kubernetes, которые написаны на Golang и основаны на библиотеке client-go.

Определение контроллера

Контроллер Kubernetes — это любой объект, который отслеживает ресурсы и определяет, соответствует ли текущее состояние настроенному состоянию. Если этого не происходит, контроллер пытается привести ресурсы в соответствие с желаемым состоянием.

Kubernetes предлагает набор встроенных контроллеров, которые по умолчанию устанавливаются в каждое окружение Kubernetes. Встроенные контроллеры управляются kube-controller-manager — демоном, встроенным в Kubernetes control plane.

Примеры встроенных контроллеров:

  • Deployment. Этот контроллер отслеживает состояние деплойментов Kubernetes — наиболее распространенного подхода к развертыванию рабочей нагрузки в Kubernetes.
  • StatefulSet. Этот контроллер обеспечивает хранилище с отслеживанием состояния (stateful storage) для stateful приложений.
  • CronJob. Этот контроллер запускает задания — компоненты рабочей нагрузки Kubernetes, которые выполняют определенные задачи — в соответствии с установленным расписанием.

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

Например, администратору может регулярно потребоваться добавлять определенные типы лейблов (меток, labels) для подов в своей среде Kubernetes. Написание собственного контроллера позволяет администратору создавать собственные лейблы для подов способами, которые не поддерживаются встроенными методами контроллера, такими как Deployments.

Операторы vs. контроллеры

Операторы — это пользовательские (кастомные) контроллеры, которые используют расширение API Kubernetes, также известные как custom resources. Оператор — это всего лишь один тип контроллера в Kubernetes.

Однако между операторами и другими типами контроллеров есть два важных различия:

  • Custom resources. В то время как операторы используют собственные кастомные ресурсы, другие контроллеры Kubernetes работают, не полагаясь на собственные ресурсы или расширения API Kubernetes.
  • Назначение. Большинство операторов предназначены для запуска приложений определенного типа. Это отличает их от других контроллеров, которые служат общей цели, не привязанной к конкретному типу. Например, контроллер развертывания управляет всеми развертываниями Kubernetes, а не только теми, которые связаны с определенным типом приложения.

Репозитории операторов, такие как OperationHub, содержат операторы, предназначенные для работы с определенными приложениями или платформами (например, инструментами мониторинга Prometheus), но не для состояния базовых ресурсов Kubernetes. Это связано с тем, что базовых ресурсы, такие как deployments и pods, управляются встроенными контроллерами с использованием нативных возможностей Kubernetes control plane, а не расширений API.

Компоненты контроллера

Существует два основных компонента контроллера: Informer/SharedInformer и Workqueue. Informer/SharedInformer отслеживает изменения текущего состояния объектов Kubernetes и отправляет события в Workqueue, где события затем подхватываются воркерами для обработки.

Informer

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

Однако повторное получение информации с сервера API может оказаться накладным. Таким образом, чтобы получать и перечислять объекты несколько раз в коде, разработчики Kubernetes используют кеш, который уже предоставлен библиотекой client-go. Кроме того, контроллер на самом деле не хочет постоянно отправлять запросы. Его интересуют только события, когда объект был создан, изменен или удален. Библиотека client-go предоставляет интерфейс Listwatcher, который выполняет первоначальный список и начинает просмотр определенного ресурса:

lw := cache.NewListWatchFromClient(
      client,
      &v1.Pod{},
      api.NamespaceAll,
      fieldSelector)

Все эти элементы используются в Informer. Общая структура Информера описана ниже:

store, controller := cache.NewInformer {
    &cache.ListWatch{},
    &v1.Pod{},
    resyncPeriod,
    cache.ResourceEventHandlerFuncs{},

Хотя Informer не так часто используется в текущей версии Kubernetes (вместо него используется SharedInformer, о котором я расскажу ниже), это по-прежнему важная концепция, которую нужно понимать, особенно когда вы хотите написать собственный контроллер. Ниже приведены три шаблона, использованные для создания Информера:

ListWatcher

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

cache.ListWatch {
    listFunc := func(options metav1.ListOptions) (runtime.Object, error) {
        return client.Get().
            Namespace(namespace).
            Resource(resource).
            VersionedParams(&options, metav1.ParameterCodec).
            FieldsSelectorParam(fieldSelector).
            Do().
            Get()
    }
    watchFunc := func(options metav1.ListOptions) (watch.Interface, error) {
        options.Watch = true
        return client.Get().
            Namespace(namespace).
            Resource(resource).
            VersionedParams(&options, metav1.ParameterCodec).
            FieldsSelectorParam(fieldSelector).
            Watch()
    }
}

Resource Event Handler

Обработчик событий ресурса — это место, где контроллер обрабатывает уведомления об изменениях в конкретном ресурсе:

type ResourceEventHandlerFuncs struct {
    AddFunc    func(obj interface{})
    UpdateFunc func(oldObj, newObj interface{})
    DeleteFunc func(obj interface{})
}
  • AddFunc вызывается при создании нового ресурса.
  • UpdateFunc вызывается при изменении существующего ресурса. oldObj — это последнее известное состояние ресурса. UpdateFunc также вызывается, когда происходит повторная синхронизация, и он вызывается, даже если ничего не меняется.
  • DeleteFunc вызывается при удалении существующего ресурса. Он получает окончательное состояние ресурса (если оно известно). В противном случае он получает объект типа DeletedFinalStateUnknown. Это может произойти, если наблюдение закрыто и пропускает событие удаления, а контроллер не замечает удаления до последующего повторного получения списка.

ResyncPeriod

ResyncPeriod определяет, как часто контроллер просматривает все элементы, оставшиеся в кеше, и снова запускает UpdateFunc. Это обеспечивает своего рода конфигурацию для периодической проверки текущего состояния и приведения его в желаемое состояние.

Это чрезвычайно полезно в случае, если контроллер пропустил обновления или предыдущие действия не удались. Однако, если вы создаете собственный контроллер, вы должны быть осторожны с загрузкой ЦП, если время периода слишком короткое.

SharedInformer

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

В этом случае SharedInformer помогает создать единый общий кэш между контроллерами. Это означает, что кэшированные ресурсы не будут дублироваться, и за счет этого уменьшаются затраты памяти. Кроме того, каждый SharedInformer создает только одно наблюдение на вышестоящем сервере, независимо от того, сколько нижестоящих потребителей читают события из информера. Это также снижает нагрузку на вышестоящий сервер. Это характерно для kube-controller-manager, у которого очень много внутренних контроллеров.

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

lw := cache.NewListWatchFromClient(…)
sharedInformer := cache.NewSharedInformer(lw, &api.Pod{}, resyncPeriod)

Workqueue

SharedInformer не может отслеживать, в каком состоянии находится каждый контроллер и чем занят (поскольку он является общим), поэтому контроллер должен предоставить свой собственный механизм организации очереди и повторния попыток (если требуется). Следовательно, большинство Resource Event Handler просто помещают события в Workqueue для каждого потребителя.

Всякий раз, когда ресурс изменяется, обработчик событий ресурса помещает ключ в Workqueue. Ключ использует формат <resource_namespace>/<resource_name>, если только неймспейс не пустой, тогда это просто <имя_ресурса>. При этом события объединяются по ключу, поэтому каждый потребитель может использовать воркеры для чтобы открыть ключ, и эта работа выполняется последовательно. Это гарантирует, что никакие два воркера не будут работать с одним и тем же ключом одновременно.

Workqueue предоставляется в библиотеке client-go по адресу client-go/util/workqueue. Поддерживается несколько типов очередей, включая очередь с задержкой, очередь по времени и очередь с ограничением скорости.

Ниже приведен пример создания очереди с ограничением скорости:

queue :=
workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())

Workqueue предоставляет удобные функции для управления ключами. В случае сбоя при обработке события контроллер вызывает функцию AddRateLimited (), чтобы вернуть ключ обратно в рабочую очередь для дальнейшей работы с заранее определенным количеством повторов. В противном случае, если процесс прошел успешно, ключ можно удалить из рабочей очереди, вызвав функцию Forget (). Однако эта функция только не позволяет рабочей очереди отслеживать историю событий. Чтобы полностью удалить событие из рабочей очереди, контроллер должен вызвать функцию Done ().

Key's life-cycle in Workqueue

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

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

Псевдокод ниже описывает эту идею:

controller.informer = cache.NewSharedInformer(...)
controller.queue = workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())

controller.informer.Run(stopCh)

if !cache.WaitForCacheSync(stopCh, controller.HasSynched)
{
    log.Errorf("Timed out waiting for caches to sync"))
}
// Now start processing
controller.runWorker()

Был ли наш пост полезен?

Нажмите на звезду, чтобы оценить мои труды!

Средний рейтинг: 5 / 5. Количество голосов: 1

Пока голосов нет. Проголосуй первым!

Мне жаль, что пост вам не помог 🙁

Позвольте мне исправиться.

Поделитесь, что можно улучшить?

Похожие посты