Во времена, когда Kubernetes был ещё v1.0.0, существовали плагины для томов (volume plugins). Нужны они были для подключения к Kubernetes систем для хранения персистентных (постоянных) данных контейнеров. Количество их было невелико, а в числе первых — такие провайдеры хранилищ, как GCE PD, Ceph, AWS EBS и другие.
Поставлялись плагины вместе с Kubernetes, за что и получили своё название — in-tree. Однако многим существующего набора таких плагинов оказалось недостаточным. Умельцы добавляли простенькие плагины в ядро Kubernetes при помощи патчей, после чего собирали свой собственный Kubernetes и ставили его на свои серверы. Но со временем разработчики Kubernetes поняли, что рыбой проблему не решить. Людям нужна удочка. И в релизе Kubernetes v1.2.0 она появилась…
Плагин Flexvolume: удочка на минималках
Разработчиками Kubernetes был создан плагин FlexVolume, который являлся логической обвязкой из переменных и методов для работы с реализуемыми сторонними разработчиками Flexvolume-драйверами.
Остановимся и подробнее рассмотрим, что представляет собой драйвер FlexVolume. Это некий исполняемый файл (бинарный файл, Python-скрипт, Bash-скрипт и т.п.), который при выполнении принимает на вход аргументы командной строки и возвращает сообщение с заранее известными полями в JSON-формате. Первым аргументом командной строки по соглашению всегда является метод, а остальные аргументы — его параметры.
Схема подключения CIFS Shares в OpenShift. Драйвер Flexvolume — прямо по центру
Минимальный набор методов выглядит так:
flexvolume_driver mount # отвечает за присоединение тома к pod'у
# Формат возвращаемого сообщения:
{
"status": "Success"/"Failure"/"Not supported",
"message": "По какой причине был возвращен именно такой статус",
}
flexvolume_driver unmount # отвечает за отсоединение тома от pod'а
# Формат возвращаемого сообщения:
{
"status": "Success"/"Failure"/"Not supported",
"message": "По какой причине был возвращен именно такой статус",
}
flexvolume_driver init # отвечает за инициализацию плагина
# Формат возвращаемого сообщения:
{
"status": "Success"/"Failure"/"Not supported",
"message": "По какой причине был возвращен именно такой статус",
// Определяет, использует ли драйвер методы attach/deatach
"capabilities":{"attach": True/False}
}
Использование методов attach
и detach
определит сценарий, по которому в будущем kubelet будет действовать при вызове драйвера. Также существуют специальные методы expandvolume
и expandfs
, которые отвечают за динамическое изменение размера тома.
В качестве примера изменений, которые добавляет метод expandvolume
, а вместе с ним — и возможность выполнять изменение размера томов в реальном времени, можно ознакомиться с нашим pull request'ом в Rook Ceph Operator.
А вот пример реализации Flexvolume-драйвера для работы с NFS:
usage() {
err "Invalid usage. Usage: "
err "t$0 init"
err "t$0 mount <mount dir> <json params>"
err "t$0 unmount <mount dir>"
exit 1
}
err() {
echo -ne $* 1>&2
}
log() {
echo -ne $* >&1
}
ismounted() {
MOUNT=`findmnt -n ${MNTPATH} 2>/dev/null | cut -d' ' -f1`
if [ "${MOUNT}" == "${MNTPATH}" ]; then
echo "1"
else
echo "0"
fi
}
domount() {
MNTPATH=$1
NFS_SERVER=$(echo $2 | jq -r '.server')
SHARE=$(echo $2 | jq -r '.share')
if [ $(ismounted) -eq 1 ] ; then
log '{"status": "Success"}'
exit 0
fi
mkdir -p ${MNTPATH} &> /dev/null
mount -t nfs ${NFS_SERVER}:/${SHARE} ${MNTPATH} &> /dev/null
if [ $? -ne 0 ]; then
err "{ "status": "Failure", "message": "Failed to mount ${NFS_SERVER}:${SHARE} at ${MNTPATH}"}"
exit 1
fi
log '{"status": "Success"}'
exit 0
}
unmount() {
MNTPATH=$1
if [ $(ismounted) -eq 0 ] ; then
log '{"status": "Success"}'
exit 0
fi
umount ${MNTPATH} &> /dev/null
if [ $? -ne 0 ]; then
err "{ "status": "Failed", "message": "Failed to unmount volume at ${MNTPATH}"}"
exit 1
fi
log '{"status": "Success"}'
exit 0
}
op=$1
if [ "$op" = "init" ]; then
log '{"status": "Success", "capabilities": {"attach": false}}'
exit 0
fi
if [ $# -lt 2 ]; then
usage
fi
shift
case "$op" in
mount)
domount $*
;;
unmount)
unmount $*
;;
*)
log '{"status": "Not supported"}'
exit 0
esac
exit 1
Итак, после подготовки собственно исполняемого файла необходимо выложить драйвер в Kubernetes-кластер. Драйвер должен находиться на каждом узле кластера согласно заранее оговоренному пути. По умолчанию был выбран:
/usr/libexec/kubernetes/kubelet-plugins/volume/exec/имя_поставщика_хранилища~имя_драйвера/
… но при использовании различных дистрибутивов Kubernetes (OpenShift, Rancher…) путь может быть другим.
Проблемы Flexvolume: как правильно закидывать удочку?
Выкладывать Flexvolume-драйвер на узлы кластера оказалось нетривиальной задачей. Проделав операцию однажды вручную, легко столкнуться с ситуацией, когда в кластере появятся новые узлы: из-за добавления нового узла, автоматического горизонтального масштабирования или — что страшнее — замены узла из-за неисправности. В этом случае работу с хранилищем на данных узлах производить невозможно, пока вы всё так же в ручном режиме не добавите на них Flexvolume-драйвер.
Решением данной проблемы послужил один из примитивов Kubernetes — DaemonSet
. При появлении нового узла в кластере на нем автоматически оказывается pod из нашего DaemonSet'a, к которому присоединяется локальный том по пути для нахождения Flexvolume-драйверов. При успешном создании pod копирует необходимые файлы для работы драйвера на диск.
Вот пример такого DaemonSet'а для выкладывания Flexvolume-плагина:
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: flex-set
spec:
template:
metadata:
name: flex-deploy
labels:
app: flex-deploy
spec:
containers:
- image: <deployment_image>
name: flex-deploy
securityContext:
privileged: true
volumeMounts:
- mountPath: /flexmnt
name: flexvolume-mount
volumes:
- name: flexvolume-mount
hostPath:
path: <host_driver_directory>
… и пример Bash-скрипта для выкладывания Flexvolume-драйвера:
#!/bin/sh
set -o errexit
set -o pipefail
VENDOR=k8s.io
DRIVER=nfs
driver_dir=$VENDOR${VENDOR:+"~"}${DRIVER}
if [ ! -d "/flexmnt/$driver_dir" ]; then
mkdir "/flexmnt/$driver_dir"
fi
cp "/$DRIVER" "/flexmnt/$driver_dir/.$DRIVER"
mv -f "/flexmnt/$driver_dir/.$DRIVER" "/flexmnt/$driver_dir/$DRIVER"
while : ; do
sleep 3600
done
Важно не забыть, что операция копирования не является атомарной. Велика вероятность, что kubelet начнет использовать драйвер до того, как процесс его подготовки будет завершен, что вызовет ошибку в работе системы. Правильным подходом будет сначала скопировать файлы драйвера под другим именем, после чего использовать атомарную операцию переименования.
Схема работы с Ceph в операторе Rook: драйвер Flexvolume на схеме находится внутри агента Rook
Следующей проблемой при использовании Flexvolume-драйверов является то, что для большинства хранилищ на узле кластера должен быть установлен необходимый для этого софт (например, пакет ceph-common для Ceph). Изначально плагин Flexvolume не был задуман для реализации настолько сложных систем.
Оригинальное решение для этой проблемы можно увидеть в реализации Flexvolume-драйвера оператора Rook:
Сам драйвер выполнен в виде RPC-клиента. IPC-сокет для общения лежит в том же каталоге, что и сам драйвер. Мы с вами помним, что для копирования файлов драйвера хорошо бы использовать DaemonSet, который в качестве тома подключает себе директорию с драйвером. После копирования необходимых файлов драйвера rook этот pod не умирает, а подключается к IPC-сокету через присоединенный том как полноценный RPC-сервер. Пакет ceph-common уже установлен внутри контейнера pod’а. IPC-сокет дает уверенность, что kubelet будет общаться именно с тем pod'ом, который находится с ним на одном узле. Всё гениальное просто!..
До свидания, наши ласковые… плагины in-tree!
Разработчики Kubernetes обнаружили, что количество плагинов для хранилищ внутри ядра равняется двадцати. И изменение в каждом из них так или иначе проходит через полный релизный цикл Kubernetes.
Оказывается, чтобы использовать новую версию плагина для хранилища, нужно обновить весь кластер. В дополнение к этому вы можете удивиться, что новая версия Kubernetes вдруг станет несовместимой с используемым ядром Linux… А посему вы вытираете слезы и скрипя зубами согласовываете с начальством и пользователями время обновления ядра Linux и кластера Kubernetes. С возможным простоем в предоставлении услуг.
Ситуация более чем комичная, не находите? Всему сообществу стало ясно, что подход не работает. Волевым решением разработчики Kubernetes объявляют, что новые плагины для работы с хранилищами более не будут приниматься в ядро. Ко всему прочему, как мы уже знаем, в реализации Flexvolume-плагином был выявлен ряд недоработок…
Раз и навсегда закрыть вопрос с персистентными хранилищами данных был призван последний добавленный плагин для томов в Kubernetes — CSI. Его альфа-версию, более полно называемую как Out-of-Tree CSI Volume Plugins, анонсировали в релизе Kubernetes 1.9.
Container Storage Interface, или спиннинг CSI 3000!
Первым делом хотелось бы отметить, что CSI — это не просто volume plugin, а самый настоящий стандарт по созданию пользовательских компонентов для работы с хранилищами данных. Предполагалось, что системы оркестрации контейнерами, такие как Kubernetes и Mesos, должны «научиться» работе с компонентами, реализованными по этому стандарту. И вот Kubernetes уже научился.
Каково же устройство CSI-плагина в Kubernetes? CSI-плагин работает со специальными драйверами (CSI-драйверами), написанными сторонними разработчиками. CSI-драйвер в Kubernetes минимально должен состоять из двух компонентов (pod’ов):
- Controller — управляет внешними персистентными хранилищами. Релизуется в виде gRPC-сервера, для которого используется примитив
StatefulSet
. - Node — отвечает за монтирования персистентных хранилищ к узлам кластера. Тоже реализуется в виде gRPC-сервера, но для него используется примитив
DaemonSet
.
Схема работы CSI-плагина в Kubernetes
О некоторых других подробностях работы CSI вы можете узнать, например, из статьи «Understanding the CSI», перевод которой мы публиковали год назад.
Плюсы такой реализации
- Для базовых вещей — например, для регистрации драйвера для узла — разработчики Kubernetes реализовали набор контейнеров. Больше не нужно самим формировать JSON-ответ с capabilities, как это делалось для плагина Flexvolume.
- Вместо «подсовывания» на узлы исполняемых файлов мы теперь выкладываем в кластер pod’ы. Этого мы изначально и ждем от Kubernetes: все процессы происходят внутри контейнеров, развернутых при помощи примитивов Kubernetes.
- Для реализации сложных драйверов больше не нужно разрабатывать RPC-сервер и RPC-клиент. Клиент за нас реализовали разработчики Kubernetes.
- Передача аргументов для работы по протоколу gRPC гораздо удобнее, гибче и надежнее, чем их передача через аргументы командной строки. Для понимания, как добавить в CSI поддержку метрик по использованию тома при помощи добавления стандартизированного gRPC-метода, можно ознакомиться с нашим pull request'ом для драйвера vsphere-csi.
- Общение происходит через IPC-сокеты, чтобы не путаться, тому ли pod'у kubelet отправил запрос.
Этот список вам ничего не напоминает? Преимущества CSI — это решение тех самых проблем, что не были учтены при разработке плагина Flexvolume.
Выводы
CSI как стандарт реализации пользовательских плагинов для взаимодействия с хранилищами данных был принят сообществом очень тепло. Более того, благодаря своим преимуществам и универсальности, CSI-драйверы создаются даже для таких хранилищ, как Ceph или AWS EBS, плагины для работы с которыми были добавлены ещё в самой первой версии Kubernetes.
В начале 2019 года плагины in-tree были объявлены устаревшими. Планируется продолжать поддержку плагина Flexvolume, но разработки новых функциональных возможностей для него не будет.
Сами мы уже имеем опыт использования ceph-csi, vsphere-csi и готовы пополнять этот список! Пока что CSI с возложенными на него задачами справляется на ура, а там поживем-увидим.
Не забывайте, что всё новое — это хорошо переосмысленное старое!
P.S.
Читайте также в нашем блоге:
- «Понимаем Container Storage Interface (в Kubernetes и не только)»;
- «Rook или не Rook — вот в чём вопрос»;
- «Знакомимся с альфа-версией снапшотов томов в Kubernetes».
Автор: nabokihms