Наш опыт с разработкой под CSI: релиз драйвера хранилища для Яндекс.Облака

1ytotnf73zepbwfalhzoropf9qa.png

Рады объявить, что компания «Флант» пополняет свой вклад в Open Source-инструменты для Kubernetes, выпустив альфа-версию драйвера CSI (Container Storage Interface) для Яндекс.Облака.

Но перед тем, как перейти к деталям реализации, ответим на вопрос, зачем это вообще нужно, когда у Яндекса уже есть услуга Managed Service for Kubernetes.

Введение


Зачем это?


Внутри нашей компании, ещё с самого начала эксплуатации Kubernetes в production (т.е. уже несколько лет), развивается собственный инструмент (deckhouse), который, кстати, мы тоже планируем в скором времени сделать доступным как Open Source-проект. С его помощью мы единообразно конфигурируем и настраиваем все свои кластеры, а в настоящий момент их уже более 100, причём на самых различных конфигурациях железа и во всех доступных облачных сервисах.

Кластеры, в которых используется deckhouse, имеют в себе все необходимые для работы компоненты: балансировщики, мониторинг с удобными графиками, метриками и алертами, аутентификацию пользователей через внешних провайдеров для доступа ко всем dashboard«ам и так далее. Такой «прокачанный» кластер нет смысла ставить в managed-решение, так как зачастую это либо невозможно, либо приведёт к необходимости отключать половину компонентов.

NB: Это наш опыт, и он довольно специфичен. Мы ни в коем случае не утверждаем, что всем стоит самостоятельно заниматься разворачиванием кластеров Kubernetes вместо того, чтобы пользоваться готовыми решениями. К слову, реального опыта эксплуатации Kubernetes от Яндекса у нас нет и давать какую-либо оценку этому сервису в настоящей статье мы не будем.

Что это и для кого?


Итак, мы уже рассказывали о современнем подходе к хранилищам в Kubernetes: как устроен CSI и как сообщество пришло к такому подходу.

В настоящее время многие крупные поставщики облачных услуг разработали драйверы для использования своих «облачных» дисков в качестве Persistent Volume в Kubernetes. Если же такого драйвера у поставщика нет, но при этом все необходимые функции предоставляются через API, то ничто не мешает реализовать драйвер собственными силами. Так и получилось у нас с Яндекс.Облаком.

За основу для разработки мы взяли CSI-драйвер для облака DigitalOcean и пару идей из драйвера для GCP, так как взаимодействие с API этих облаков (Google и Яндекс) имеет ряд сходств. В частности, API и у GCP, и у Yandex возвращают объект Operation для отслеживания статуса длительных операций (например, создания нового диска). Для взаимодействия с API Яндекс.Облака используется Yandex.Cloud Go SDK.

Результат проделанной работы опубликован на GitHub и может пригодиться тем, кто по какой-то причине использует собственную инсталляцию Kubernetes на виртуальных машинах Яндекс.Облака (но не готовый managed-кластер) и хотел бы использовать (заказывать) диски через CSI.

Реализация


Основные возможности


На текущий момент драйвер поддерживает следующие функции:

  • Заказ дисков во всех зонах кластера согласно топологии имеющихся в кластере узлов;
  • Удаление заказанных ранее дисков;
  • Offline resize для дисков (Яндекс.Облако не поддерживает увеличение дисков, которые примонтированы к виртуальной машине). О том, как пришлось дорабатывать драйвер, чтобы максимально безболезненно выполнять resize, см. ниже.


В будущем планируется реализовать поддержку создания и удаления снапшотов дисков.

Главная сложность и её преодоление


Отсутствие в API Яндекс.Облака возможности увеличивать диски в реальном времени — ограничение, которое усложняет операцию resize«а для PV (Persistent Volume): ведь в таком случае необходимо, чтобы pod приложения, который использует диск, был остановлен, а это может вызвать простой приложения.

Согласно спецификации CSI, если CSI-контроллер сообщает о том, что умеет делать resize дисков только «в offline» (VolumeExpansion.OFFLINE), то процесс увеличения диска должен проходить так:

If the plugin has only VolumeExpansion.OFFLINE expansion capability and volume is currently published or available on a node then ControllerExpandVolume MUST be called ONLY after either:
  • The plugin has controller PUBLISH_UNPUBLISH_VOLUME capability and ControllerUnpublishVolume has been invoked successfully.

OR ELSE
  • The plugin does NOT have controller PUBLISH_UNPUBLISH_VOLUME capability, the plugin has node STAGE_UNSTAGE_VOLUME capability, and NodeUnstageVolume has been completed successfully.

OR ELSE
  • The plugin does NOT have controller PUBLISH_UNPUBLISH_VOLUME capability, nor node STAGE_UNSTAGE_VOLUME capability, and NodeUnpublishVolume has completed successfully.


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

Однако, к сожалению, реализация спецификации CSI через sidecar«ы не соответствует этим требованиям:

  • В sidecar-контейнере csi-attacher, который и должен отвечать за наличие нужного промежутка между монтированиями, при offline-ресайзе попросту не реализован этот функционал. Дискуссию об этом инициировали здесь.
  • Что вообще такое sidecar-контейнер в данном контексте? Сам CSI-плагин не занимается взаимодействием с Kubernetes API, а лишь реагирует на gRPC-вызовы, которые посылают ему sidecar-контейнеры. Последние разрабатываются сообществом Kubernetes.


В нашем случае (CSI-плагин) операция увеличения диска выглядит следующим образом:

  1. Получаем gRPC-вызов ControllerExpandVolume;
  2. Пытаемся увеличить диск в API, но получаем ошибку о невозможности выполнения операции, так как диск примонтирован;
  3. Сохраняем идентификатор диска в map, содержащий диски, для которых необходимо выполнить операцию увеличения. Далее для краткости будем называть этот map как volumeResizeRequired;
  4. Вручную удаляем pod, который использует диск. Kubernetes при этом перезапустит его. Чтобы диск не успел примонтироваться (ControllerPublishVolume) до завершения операции увеличения при попытке монтирования, проверяем, что данный диск всё ещё находится в volumeResizeRequired и возвращаем ошибку;
  5. CSI-драйвер пытается повторно выполнить операцию resize«а. Если операция прошла успешно, то удаляем диск из volumeResizeRequired;
  6. Т.к. идентификатор диска отсутствует в volumeResizeRequired, ControllerPublishVolume проходит успешно, диск монтируется, pod запускается.


Всё выглядит достаточно просто, но как всегда есть подводные камни. Увеличением дисков занимается external-resizer, который в случае ошибки при выполнении операции использует очередь с экспоненциальным увеличением времени таймаута до 1000 секунд:

func DefaultControllerRateLimiter() RateLimiter {
  return NewMaxOfRateLimiter(
  NewItemExponentialFailureRateLimiter(5*time.Millisecond, 1000*time.Second),
  // 10 qps, 100 bucket size.  This is only for retry speed and its only the overall factor (not per item)
  &BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
  )
}


Это может периодически приводить к тому, что операция увеличения диска растягивается на 15+ минут и, таким образом, недоступности соответствующего pod«а.

Единственным вариантом, который достаточно легко и безболезненно позволил нам уменьшить потенциальное время простоя, стало использование своей версии external-resizer с максимальным ограничением таймаута в 5 секунд:

workqueue.NewItemExponentialFailureRateLimiter(5*time.Millisecond, 5*time.Second)


Мы не посчитали нужным экстренно инициировать дискуссию и патчить external-resizer, потому что offline resize дисков — атавизм, который вскоре пропадёт у всех облачных провайдеров.

Как начать пользоваться?


Драйвер поддерживается в Kubernetes версии 1.15 и выше. Для работы драйвера должны выполняться следующие требования:

  • Флаг --allow-privileged установлен в значение true для API-сервера и kubelet;
  • Включены --feature-gates=VolumeSnapshotDataSource=true,KubeletPluginsWatcher=true,CSINodeInfo=true,CSIDriverRegistry=true для API-сервера и kubelet;
  • Распространение монтирования (mount propagation) должно быть включено в кластере. При использовании Docker«а демон должен быть сконфигурирован таким образом, чтобы были разрешены совместно используемые объекты монтирования (shared mounts).


Все необходимые шаги по самой установке описаны в README. Инсталляция представляет собой создание объектов в Kubernetes из манифестов.

Для работы драйвера вам понадобится следующее:

  • Указать в манифесте идентификатор каталога (folder-id) Яндекс.Облака (см. документацию);
  • Для взаимодействия с API Яндекс.Облака в CSI-драйвере используется сервисный аккаунт. В манифесте Secret необходимо передать авторизованные ключи от сервисного аккаунта. В документации описано, как создать сервисный аккаунт и получить ключи.


В общем — попробуйте, а мы будем рады обратной связи и новым issues, если столкнетесь с какими-то проблемами!

Дальнейшая поддержка


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

Кроме того, наверное, у Яндекса в managed-кластере Kubernetes есть собственная реализация CSI-драйвера, которую можно выпустить в Open Source. Такой вариант развития для нас также видится благоприятным — сообщество сможет пользоваться проверенным драйвером от поставщика услуг, а не от сторонней компании.

P.S.


Читайте также в нашем блоге:

© Habrahabr.ru