Всем привет! На связи Андрей Кундрюков, DevOps-инженер компании «Флант». За последние полгода мне дважды приходилось массово мигрировать данные с PVC из одного StorageClass в другой без потери данных и с минимальным простоем. В первый раз история была связана с выводом из эксплуатации зоны доступности ru-central1-c Yandex Cloud. А на этот раз у одного из клиентов мы мигрировали с Ceph на LINSTOR.
В ходе работы я не раз искал руководства по этому процессу, но ничего готового на русском языке не нашел. А на английском попадались или сухие советы по типу «наверное, можно примонтировать два PVC и запустить rsync», или совсем безумные варианты миграции через промежуточное S3-хранилище. Поэтому я решил написать небольшое руководство по данной теме.
В статье вы узнаете, как совершать такие операции без потери информации: мигрировать данные между PVC, а также между разными пространствами имён и StorageClass. Ещё разберём, как уменьшить или увеличить размер PVC, и бонусом поделюсь, как делать бэкапы PVC без снапшотов.
Хранение данных в K8s
Для начала кратко расскажу (для кого-то просто напомню), как выглядит схема хранения данных в Kubernetes.
В Kubernetes есть объект StatefulSet, который предназначен для управления состоянием приложений, таких как базы данных и распределённые системы, требующих постоянного хранения данных. Приложениям, работающим в StatefulSet, необходимо надёжное и устойчивое к ошибкам хранение данных для поддержания своего состояния в течение жизненного цикла подов. Поэтому они нуждаются в Persistent Volumes (PV) — выделенное пространство для хранения данных, которое может быть использовано приложениями.
Для запросов и доступа к PV StatefulSet использует Persistent Volume Claims (PVC) — самый верхний слой абстракции в дисковой подсистеме хранения постоянных данных в Kubernetes, которые сохраняются даже после перезапуска контейнера. Через создание PVC появляется запрос на выделение определённого объёма хранилища с заданными параметрами, такими как размер и режим доступа, а также область, из которой необходимо выделить дисковое пространство — StorageClass.
Когда PVC создаётся, Kubernetes ищет подходящий PV, который соответствует требованиям запроса. Если такой PV найден, он связывается с PVC, и приложение получает доступ к этому хранилищу. Если подходящего PV нет, Kubernetes может динамически создать новый PV, если это предусмотрено настройками. PV представляет собой более близкий уровень к физическому хранению данных на дисках, включая разделы, LVM или блочные устройства.
При этом, когда создается PVC, можно указать желаемый StorageClass, который определяет параметры PV. Это позволяет Kubernetes динамически создавать новый PV с заданными параметрами, если подходящее хранилище не найдено.
Пример PVC:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name:example-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: example-storage
resources:
requests:
storage: 5Gi
Самое главное, что нам нужно знать, — PVC можно отмонтировать, удалить или примонтировать к другому контейнеру. Данные на PV при этом останутся, если специально не настроено иное поведение.
Переезд на другой PVC без простоя
Перейдём к практике. Для начала рассмотрим простой вариант миграции, когда сохранность данных не важна, однако важно отсутствие простоя. Под этот кейс подходят кластерные системы, которые умеют синхронизировать данные между репликами.
Для примера возьмём кластер Redis из трёх реплик. Соответственно, у нас будет StatefulSet из трёх подов и трёх PVC. Мы можем сделать следующее:
Удалить один из подов.
Удалить PVC вместе с PV.
Создать новый PVC и запустить под обратно.
При таком порядке действий реплика Redis включится в кластер и синхронизируется с двумя действующими репликами.
Однако тут есть некоторые нюансы. В StatefulSet у нас есть такой блок для создания PVC и заказа PV:
volumeClaimTemplates:
- apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: ceph
Здесь поля storage
и storageClassName
— immutable (неизменяемые). Поэтому простым способом отредактировать StatefulSet не получится. Обойти это можно следующим образом: поскейлить StatefulSet до 2/3 подов и затем удалить PVC. Потом создать новый PVC с тем же именем, но с другим storageClassName
или объёмом, скейлить обратно и ждать синхронизации данных.
$ kubectl -n test scale sts example-sts –-replicas=2
$ kubectl -n test delete pvc example-pvc-2
$ kubectl -n test apply -f -<<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: example-pvc-2
spec:
storageClassName: linstor
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
EOF
$ kubectl -n test scale sts example-sts -–replicas=3
Но это только один PVC из трёх, а скейлить поды StatefulSet в произвольном порядке мы не можем, так как при уменьшении количества реплик удаляется последний созданный под. Можно рискнуть и повторить операцию, только попробовать уменьшить количество реплик до одной, но это увеличивает риск простоя. К тому же всё равно останется последний под, скейл которого гарантированно приведёт к простою.
Здесь нам на помощь придёт магия Kubernetes. Мы можем удалить StatefulSet, не удаляя поды, а затем удалить произвольный под:
$ kubectl -n test get sts example-sts -> saved-sts.yaml
$ kubectl -n test delete sts example-sts –-cascade=orphan # Этот параметр удалит StatefulSet, но не сами поды.
$ kubectl -n test delete pod example-sts-0
$ kubectl -n test delete pvc example-pvc-0
$ kubectl -n test apply -f -<<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: example-pvc-0-new
spec:
storageClassName: linstor
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
EOF
$ kubectl -n test apply -f saved-sts.yaml # Возвращаем на место StatefulSet, не забыв отредактировать storageClass.
Контроллер StatefulSet не будет поднимать удалённый под заново, и мы сможем изменить PVC. После этого вернём StatefulSet на место, не забыв изменить StorageClass или размер дисков в сохраненном манифесте StatefulSet.
В итоге на протяжении всего времени работ у нас были доступны две реплики из трёх. Таким образом удалось мигрировать без простоя сервиса.
Переезд на другой PVC без потери данных
Теперь пример сложнее. У нас StatefulSet c PVC, которые хранят данные без синхронизации между репликами, или одинокий deployment. Допустим, потеря данных в любом из PVC критична. Один из очевидных способов решения проблемы — создать новый PVC, примонтировать его, запустить rsync для копирования данных и затем сменить PVC.
Однако этот процесс выглядит сложным и долгим, поэтому я начал искать варианты ускорить его и нашёл. Как гласит инженерная мудрость: «Возможно, кто-то уже придумал это до нас». Я говорю про утилиту pv-migrate, которая автоматизирует шаги выше.
Итоговый план такой:
Создаём новый PVC с постфиксом
-new
.Скейлим sts в 0
kubectl -n test scale sts example-sts --replicas=0
.Запускаем утилиту
pv-migrate
.Переименовываем старый PVC в
pvc*.-old
.Переименовываем новый PVC, убрав постфикс new.
Возвращаем изначальное количество реплик:
kubectl -n test scale sts example-sts --replicas=[1|2|3] (вставить нужное количество исходных реплик)
.Если что-то идёт не так, возвращаем старый PVC, переименовав
pvc*.-old
обратно вpvc*
.Сохраняем манифест StatefulSet.
Удаляем StatefulSet c опцией
--cascade-orphan
(это сохранит нам поды).Правим StorageClass в StatefulSet.
Возвращаем исправленный StatefulSet.
Удаляем
pvc*.-old
.
В плане есть пункты про переименование PVC. Это можно сделать вручную, но это долго и неудобно, поэтому используем ещё одну утилиту — rename-pvc.
Кстати, обе утилиты (pv-migrate и rename-pvc) легко ставятся через krew — пакетный менеджер плагинов и дополнений к kubectl. Теперь у меня есть больше инструментов для работы с kubectl.
Финальный набор инструкций:
export NAMESPACE=test
export PVC_NAME=test-pvc-0
export STS_NAME=test-sts
export STORAGE_SIZE=1Gi
kubectl -n $NAMESPACE apply -f -<<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: $PVC_NAME-new
spec:
storageClassName: linstor
accessModes:
- ReadWriteOnce
resources:
requests:
storage: $STORAGE_SIZE
EOF
kubectl -n $NAMESPACE scale sts $STS_NAME --replicas=0
pv-migrate --source-namespace=$NAMESPACE --source=$PVC_NAME --dest-namespace=$NAMESPACE --dest=$PVC_NAME-new
rename-pvc -n $NAMESPACE $PVC_NAME $PVC_NAME-old
rename-pvc -n $NAMESPACE $PVC_NAME-new $PVC_NAME
kubectl -n $NAMESPACE scale sts $STS_NAME --replicas=1
#kubectl -n $NAMESPACE delete pvc $PVC_NAME-old
Дальнейшие шаги по автоматизации: написать скрипт, который соберёт все PVC в старом StorageClass и сам их перевезёт, но мы не будем рассматривать такой шаг в рамках данной статьи, чтобы не уйти от основной её мысли.
Таким образом удалось минимизировать время простоя и не заниматься ненужной рутиной, например снимать и заливать дампы БД.
Теперь, когда мы разобрались с основами миграции данных, обсудим дополнительные операции с PVC в Kubernetes — такие, как бэкапы, изменение размера и перенос между пространствами имён, — которые помогут лучше управлять данными.
Другие операции с PVC в Kubernetes
Бэкап PVC
Чтобы обновить версию софта в контейнере с постоянными данными и предусмотреть возможность отката, нужно создать новый PVC и мигрировать данные так, как описано выше. Если после обновления что-то пойдёт не так, необходимо удалить новый PVC и переименовать старый.
Изменение размера PVC
Чтобы увеличить размер PVC, можно отредактировать поле storage
у PVC, и storage provisioner дальше сам обо всём позаботится. Например, в Deckhouse Kubernetes Platform это работает для кластеров и дисков в Yandex Cloud. А вот уменьшить размер PVC уже сложнее.
В этом случае также можно использовать утилиту pv-migrate: создаём новую PVC меньшего размера, мигрируем данные и переключаемся. Поскольку миграция выполняется пофайлово с помощью rsync, а не блочно, проблем не возникнет.
Перенос PVC в другое пространство имён
Для переноса PVC в другое пространство имён используем следующую команду:
pv-migrate --source-namespace=$NAMESPACE1 --source=$PVC_NAME --dest-namespace=$NAMESPACE2 --dest=$PVC_NAME-new
Заключение
Я поделился своими знаниями и опытом работы с хранилищами данных в Kubernetes, а также двумя утилитами, которые могут облегчить описанные в статье процессы. Надеюсь, если вы не знали об этом ранее, они помогут вам в работе. Удачи!
P. S.
Читайте также в нашей главе:
trublast
Спасибо большое за статью!
Открыл для себя такие отличные утилиты, как pv-migrate/pv-rename.
Добавил бы еще, что так как pv-migrate выполняет rsync, то для уменьшения времени простоя, для больших pvc можно выполнить несколько итераций.
Сначала "прогнать" pv-migrate несколько раз с ключами --ignore-mounted --source-mount-read-only=false --dest-delete-extraneous-files при рабочем исходном поде (первый проход может занять много времени, если данных сотни гигабайт). Потом сделать бэкап манифеста пода и удалить под. Прогнать pv-migrate еще раз, "начисто". После чего переименовать pvc и восстановить бэкап манифеста пода, он подключит новую pv.
Почему --source-mount-read-only=false ? Если опции монтирования исходного пода и пода-мигратора различаются (pv-migrate создает под, в который подключает 2 вольюма, и между ними копирует данные), то мультимаунт вольюма для некоторых CSI провайдеров не срабатывает и под мигратора не запускается.
Mounting arguments: -t ext4 -o _netdev,ro /dev/drbd1010 /var/lib/kubelet/pods/7894f1d9-cb3b-4caa-b5dd-94ed94363458/volumes/kubernetes.io~csi/pvc-8e6c39d5-30fd-4a01-9867-64d152abe597//dev/drbd1010 already mounted on /var/lib/kubelet/pods/ee7487b4-dc5c-4d40-b89d-df2396e8bff8/volumes/kubernetes.io~csi/pvc-8e6c39d5-30fd-4a01-9867-64d152abe597/mount.