Наш опыт с разработкой под CSI: релиз драйвера хранилища для Яндекс.Облака
Рады объявить, что компания «Флант» пополняет свой вклад в 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 onlyVolumeExpansion.OFFLINE
expansion capability and volume is currently published or available on a node thenControllerExpandVolume
MUST be called ONLY after either:
- The plugin has controller
PUBLISH_UNPUBLISH_VOLUME
capability andControllerUnpublishVolume
has been invoked successfully.
OR ELSE
- The plugin does NOT have controller
PUBLISH_UNPUBLISH_VOLUME
capability, the plugin has nodeSTAGE_UNSTAGE_VOLUME
capability, andNodeUnstageVolume
has been completed successfully.
OR ELSE
- The plugin does NOT have controller
PUBLISH_UNPUBLISH_VOLUME
capability, nor nodeSTAGE_UNSTAGE_VOLUME
capability, andNodeUnpublishVolume
has completed successfully.
По сути это означает необходимость отсоединить диск от виртуальной машины перед тем, как его увеличивать.
Однако, к сожалению, реализация спецификации CSI через sidecar«ы не соответствует этим требованиям:
- В sidecar-контейнере
csi-attacher
, который и должен отвечать за наличие нужного промежутка между монтированиями, при offline-ресайзе попросту не реализован этот функционал. Дискуссию об этом инициировали здесь. - Что вообще такое sidecar-контейнер в данном контексте? Сам CSI-плагин не занимается взаимодействием с Kubernetes API, а лишь реагирует на gRPC-вызовы, которые посылают ему sidecar-контейнеры. Последние разрабатываются сообществом Kubernetes.
В нашем случае (CSI-плагин) операция увеличения диска выглядит следующим образом:
- Получаем gRPC-вызов
ControllerExpandVolume
; - Пытаемся увеличить диск в API, но получаем ошибку о невозможности выполнения операции, так как диск примонтирован;
- Сохраняем идентификатор диска в map, содержащий диски, для которых необходимо выполнить операцию увеличения. Далее для краткости будем называть этот map как
volumeResizeRequired
; - Вручную удаляем pod, который использует диск. Kubernetes при этом перезапустит его. Чтобы диск не успел примонтироваться (
ControllerPublishVolume
) до завершения операции увеличения при попытке монтирования, проверяем, что данный диск всё ещё находится вvolumeResizeRequired
и возвращаем ошибку; - CSI-драйвер пытается повторно выполнить операцию resize«а. Если операция прошла успешно, то удаляем диск из
volumeResizeRequired
; - Т.к. идентификатор диска отсутствует в
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.
Читайте также в нашем блоге: