Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Как известно, системные администраторы делятся на 3 категории - кто еще не делает резервные копии, кто уже делает и кто уверен, что из них можно восстановиться. В нашу эпоху DevOps вопрос автоматизации управления резервным копирования стал еще более актуальным, поскольку каждая система предлагает свой уникальный способ создания (и восстановления) дампа и даже в пределах одной системы может быть множество разных способов хранения данных. В этой статье мы обсудим возможные стратегии и доступные технологические решения для создания резервных копий данных для развертываний в Kubernetes и поговорим о возможностях автоматизации и мониторинга процесса резервного копирования.
Перед тем, как мы перейдем к рассмотрению механизмов резервного копирования, нужно сказать несколько слов о терминологии Kubernetes в отношении внешних ресурсов для хранения данных и способах их подключения к запущенным контейнерам.
В простейшем варианте для хранения данных может использоваться локальный каталог на файловой системе сервера, но такое решение имеет множество недостатков:
контейнер может быть запущен только на сервере, где физически размещаются данные (а это снижает потенциальную устойчивость к сбоям при повреждении сервера или сетевого подключения к нему)
при необходимости масштабирования приложения необходимо контролировать исключение ситуации запуска нескольких контейнеров на одном узле (чтобы избежать потенциальное состояние гонки при доступе к одному каталогу с данными)
при необходимости хранения дополнительных копий (или реплик) данных необходимо либо использовать средства репликации разворачиваемого сервиса, либо использовать синхронизацию через сеть (например, rsync)
Основное достоинство такого решения - высокая скорость доступа к данным (поскольку исключаются дополнительные сетевые абстракции) и возможность создания резервных копий через сценарии, запущенные непосредственно на сервере.
Но все же, в большинстве развертываний на основе Kubernetes предполагается, что данные сервисов хранятся независимо от контейнеров. Долгое время эта задача была крайне нетривиальной и для ее решения использовались сценарии автоматического монтирования сетевых ресурсов на узел, где запускается приложение, с использованием flex-плагинов. Плагин устанавливался в каталог /usr/libexec/kubernetes/kubelet-plugins/volume/exec/<vendor~driver>/<driver> и обеспечивал подключение именованного тома из внешнего хранилища при размещении контейнера на узел (и последующее отключение при удалении). Для многих сетевых файловых систем (cephfs, lizardfs, glusterfs) до 2018 года это был единственный способ динамического монтирования томов, но с ним существовало множество проблем - начиная от банальных ошибок на этапе подключения из-за проблем с правами доступа и заканчивая "замиранием" тома, когда при переключении контейнера на другую машину он не отключался от предыдущей и не мог смонтироваться на новый узел, поскольку менеджер воспринимал диск как "уже примонтирован к единственно возможному узлу". Ну и кроме того, на разных системах оркестрации использовались различные подходы к управлению подключением хранилища и это добавляло сложностей к унифицированному управлению резервными копиями.
Ситуация в значительной степени улучшилась при появлении единого стандарта для подключения систем хранения к контейнерам (Container Storage Interface, про который замечательно было рассказано в статье на Хабре). CSI отделял концепции регистрации устройства, выделения необходимого объема хранения и монтирования системы хранения к узлу системы от непосредственно оркестратора и добавил новый уровень абстракции для исключения прямой зависимости от выбранной системы хранения. Это дало возможность подключать любые доступные системы сетевого хранения (независимо от их природы - как аппаратные системы на основе Storage Area Network, так и виртуальные облачные хранилища) унифицированным образом. Драйвер CSI связывается с одним или несколькими StorageClass в Kubernetes и решает задачи выделения указанного объема по запросу (Provisioner), изменения размера системы хранения (Resizer), поиска и подключения ресурса по идентификатору (Attacher), создание мгновенных снимков (Snapshotter). Частным случаем CSI может быть также flex-драйвер для совместимости с системами хранения, не предоставляющим CSI-драйвер, а также драйвер для локальной файловой системы (local-storage provisioner) и драйверы для поддержки существующих сетевых протоколов доступа к внешнему хранилищу, например CSI-Driver-NFS (подключение к NFS Share), CSI-Driver-SMB (для подключения к сетевым ресурсам Windows), CSI-Driver-iSCSI (для использования сетевых хранилищ на дисках iSCSI). CSI-драйвер обычно запускается в виде управляющего контейнера (контроллера) и DaemonSet-ресурса, который разворачивает на каждом узле системы агента, взаимодействующего с контроллером. Зарегистрированный CSI-драйвер может быть указан при создании класса системы хранения (StorageClass) как provisioner (с возможностью дополнительной конфигурации). Например, для поддержки системы хранения на основе NFS может быть применена следующая yaml-конфигурация:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
server: nfs-server.default.svc.cluster.local
share: /
csi.storage.k8s.io/provisioner-secret-name: "mount-options"
csi.storage.k8s.io/provisioner-secret-namespace: "default"
reclaimPolicy: Delete
volumeBindingMode: Immediate
mountOptions:
- nfsvers=4.1
Непосредственно взаимодействие с системой хранения начинается с создания PersistentVolume, однако обычно это выполняется не вручную, а с использованием CSI Provisioner через создание ресурса с типом PersistentVolumeClaim. Он может быть создан как отдельный ресурс, либо сгенерироваться на основе PersistentVolumeClaimTemplate при запуске развертываний StatefulSet с несколькими репликами (под каждую реплику Provisioner выделит из доступного пула на системе хранения запрошенный объем ресурсов и создаст PersistentVolume для доступа к выделенному ресурсу). И вот здесь наступает самая сложная часть по организации резервного копирования - практически всегда PersistentVolume предназначен для монтирования только к одному контейнеру. Значит нам придется встраивать сценарии резервного копирования непосредственно в контейнер с базой данных или в веб-приложение? И да и нет. Давайте будем разбираться дальше.
Посмотрим внимательно на содержание ресурса PersistentVolumeClaim:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
finalizers:
- kubernetes.io/pvc-protection
labels:
app: database
name: postgres-volume
namespace: postgres
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: yc-network-hdd
Наиболее важным для нас в этом описании являются количество запрашиваемого объема хранения (.spec.resources.requests.storage), название StorageClass (напомню, он напрямую связан с соответствующим provisioner от CSI-драйвера), а также режим доступа (accessModes). Для систем сетевого хранения с совместным конкурентным доступом режим доступа можно указывать в ReadWriteMany (может быть подключен одновременно ко множеству контейнеров на чтение-запись), но во многих случаях (в том числе для исключения ситуации одновременной модификации файлов на системе хранения) режим будет указываться как монопольный на запись к одному контейнеру (но с возможностью множественного чтения) - ReadWriteOnce. Именно эту особенность мы и будем использовать для организации резервного копирования подключенных разделов системы хранения для запущенных контейнеров.
Для настройки резервного копирования будем использовать кластер minikube. После установки выполним инициализацию кластера:
minikube start
Мы будем рассматривать все примеры на Kubernetes 1.23 и не будем использовать возможности более нового Kubernetes 1.24 (стабильная поддержка увеличения запрошенного размера PersistentVolume, возможность получать информацию о доступных ресурсах хранилища через CSIStorageCapacity). Для управления кластером нам также понадобится утилита kubectl (инструкция по установке есть здесь).
По умолчанию после установки нам будет доступен storageclass для локального хранения файлов на узлах с provisioner k8s.io/minikube-hostpath. Сейчас мы ограничимся использование только этого драйвера, но в реальных условиях здесь можно задействовать CSI драйверы для облачных провайдеров (например, pd.csi.storage.gke.io для Google Compute Engine или disk-csi-driver.mks.ycloud.io для подключения дисков Яндекс Облако), изменения будут затрагивать только класс хранилища.
Мы попробуем настроить резервное копирование базы данных PostgreSQL (запущенной из официального контейнера) и пройдем весь путь от ручного описания сценариев создания дампа и сохранения снимка до использования автоматизированных операторов для управления резервным копированием.
Начнем с установки PostgreSQL, для этого применим файл описания развертывания, включающий в себя PersistentVolumeClaim. Обратите внимание, что название StorageClass должно соответствовать выбранной системе хранения.
postgresql.yaml
apiVersion: v1
kind: Namespace
metadata:
name: postgres
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
namespace: postgres
name: postgres
spec:
selector:
matchLabels:
app: postgres
serviceName: postgres
replicas: 1
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:13
ports:
- name: postgres
containerPort: 5432
protocol: TCP
env:
- name: POSTGRES_USER
value: postgres
- name: PGUSER
value: postgres
- name: POSTGRES_DB
value: postgres
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
- name: POSTGRES_PASSWORD
value: password
- name: POD_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.podIP
livenessProbe:
exec:
command:
- sh
- -c
- exec pg_isready --host $POD_IP
failureThreshold: 6
initialDelaySeconds: 60
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
readinessProbe:
exec:
command:
- sh
- -c
- exec pg_isready --host $POD_IP
failureThreshold: 3
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 3
volumeMounts:
- mountPath: /var/lib/postgresql/data/pgdata
name: postgres
subPath: postgres-db
volumeClaimTemplates:
- metadata:
name: postgres
spec:
storageClassName: "standard"
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 4Gi
В реальных условиях, конечно же, пароль не должен храниться непосредственно в yaml, а должен извлекаться из секретов или из внешнего защищенного хранилища (например, Vault). После применения файла развертывания к кластеру можем убедиться, что контейнер с базой данных запустился и через local storage provisioner выделено 4Гб дискового пространства для хранения данных.
kubectl apply -f postgresql.yaml
kubectl get pod -n postgres
NAME READY STATUS RESTARTS AGE
postgres-0 1/1 Running 0 2m19s
kubectl get pvc -n postgres
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
postgres-postgres-0 Bound pvc-11501fc1-7d95-4a7c-a6c8-8138ac1f49e9 4Gi RWO standard 2m33s
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-11501fc1-7d95-4a7c-a6c8-8138ac1f49e9 4Gi RWO Delete Bound postgres/postgres-postgres-0 standard 2m46s
Теперь начнем настройку резервного копирования и рассмотрим несколько сценариев и инструментов:
Сохраняем состояние Persistent Volume "как есть"
Эта тактика хорошо работает для систем, которые не сохраняют промежуточные файлы (например, идентификаторы запущенного процесса или логи транзакций) и работают с относительно устойчивым каталогом с данными, который может быть сохранен при запущенной системе. Задача не выглядит сложной, но есть несколько важных нюансов:
раздел с данными (pvc-11501fc1-7d95-4a7c-a6c8-8138ac1f49e9) доступен изнутри контейнера (в действительности его также можно увидеть примонтированным в /var/lib/docker/containers/<id>/volumes на узле, где запущен контейнер) и нужно перенести данные изнутри контейнера на внешнюю систему
в образе postgresql не предусмотрено дополнительных инструментов для создания резервных копий (и управлением их устареванием и ротацией), поэтому их либо нужно будет установить в контейнер, либо сначала перенести данные на узел во временный каталог и оттуда уже создавать резервную копию
Начнем с первого варианта: создадим копию данных из контейнера на локальной машине (не обязательно на той же, где запущен контейнер) с последующей отправкой в централизованное хранилище резервных копий.
Для доступа к данным внутри контейнера можно использовать возможности переноса файлов через команду kubectl cp. В нашем случае команда может выглядеть следующим образом:
kubectl cp -n postgres postgres-0:/var/lib/postgresql/data/pgdata postgres
После выполнения команды мы получим текущий снимок каталога данных PostgreSQL и далее можем отправить его через систему управления резервными копиями (например, Bacula), сохранить во внешнее хранилище (например, через rclone можно сохранить полученные данные на облачного провайдера), либо использовать свободный инструмент restic для управления резервными копиями и их ротацией. Рассмотрим последний вариант чуть более подробно.
Restic является кроссплатформенным свободным решением для инкрементального резервного копирования с шифрованием резервных копий и возможностью ротации копий по количеству или сроку хранения. Для каждого набора данных создается отдельный репозиторий с паролем для доступа, в котором отслеживается появлением снимков (snapshot).
Для использования restic прежде всего начнем с инициализации репозитория для хранения резервных копий:
restic init -p password_file -r postgres_backup
В файл password_file необходимо ввести пароль для шифрования репозитория (также может быть получен из командной строки через --password-command echo 'password').
Инициализация репозитория выполняется однократно. Следующим действием выполним сохранение резервной копии в репозиторий.
restic backup -p password_file.txt -r postgres_backup postgres
И проверим появление нового снимка в репозитории:
restic snapshots -p password_file.txt -r postgres_backup
ID Time Host Tags Paths
-------------------------------------------------------------------------------------
1c705492 2022-05-24 16:47:58 demo /home/dmitrii/postgres
-------------------------------------------------------------------------------------
1 snapshots
А также статистику по размеру репозитория и количеству файлов и снимков:
restic stats -p password_file.txt -r postgres_backup
repository acafc202 opened (repo version 1) successfully, password is correct
scanning...
Stats in restore-size mode:
Snapshots processed: 1
Total File Count: 968
Total Size: 39.474 MiB
Для восстановления можно использовать любой из сохраненных ранее снимков или последний снимок:
restic restore -r postgres -p password_file.txt latest --target restore
Данные будут сохранены в каталог restore, откуда в дальнейшем они могут быть скопированы в запущенный контейнер:
cp restore && kubectl cp . -n postgres postgres-0:/var/lib/postgresql/data/pgdata
Однако использование снимка для системы, которая обновляет свое мгновенное состояние (к которым в том числе относится СУБД) может привести к созданию снимка, из которого будет невозможно восстановить работоспособное состояние базы данных (более того, файлы могут измениться во время копирования). Более правильным и каноничным способом создания копии будет создание дампа данных из запущенного процесса с последующим восстановлением штатными инструментами контейнера.
В случае с postgresql команда для создания дампа всех существующих баз данных будет выглядеть следующим образом:
kubectl exec -n postgres postgres-0 pg_dumpall >postgres.dump
В дальнейшем дамп может быть сохранен как снимок в restic (но тут нужно отметить, что в отличии от снимка диска копия будет всегда полной, а не инкрементальной) и восстановлен в новый (пустой) контейнер с базой данных:
cat postgres.dump | kubectl exec -n postgres postgres-0 psql databasename
Какие есть недостатки у ручного создания резервных копий:
необходимость создания дополнительных (временных) файлов с копией состояния системы или дампом содержания с последующем копированием их через bacula или restic
при копировании по расписанию нужно подключать системный планировщик (например, cron) и создавать сценарии под каждую систему в отдельности (поскольку расположение каталога с данными может отличаться)
мониторинг процесса осуществляется вручную (по сохраненным логам команд и проверки кодов возврата)
Рассмотрим другие возможные сценарии создания резервных копий.
Начиная с версии Kubernetes 1.17 стала доступна бета-версия поддержки функции CSI Snapshotter, позволяющей фиксировать мгновенное состояние раздела и, в дальнейшем, при необходимости возвращать систему в предыдущее состояние. Для поддержки CSI нам будет нужно дополнительно добавить драйвер hostpath CSI и установить контроллеры для поддержки возможностей CSI (Snapshot, Attacher, Resizer и др.).
Установим контроллер с адреса https://github.com/kubernetes-csi/csi-driver-host-path и установим CSI-драйвер и необходимые компоненты:
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-provisioner/v1.5.0/deploy/kubernetes/rbac.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-attacher/v2.1.0/deploy/kubernetes/rbac.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/v2.0.1/deploy/kubernetes/csi-snapshotter/rbac-csi-snapshotter.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-resizer/v0.4.0/deploy/kubernetes/rbac.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml
kubectl apply -f example/csi-storageclass.yaml
kubectl apply -f deploy/kubernetes-1.21-test/hostpath/csi-hostpath-plugin.yaml
kubectl apply -f deploy/kubernetes-1.21-test/hostpath/csi-hostpath-attacher.yaml
kubectl apply -f deploy/kubernetes-1.21-test/hostpath/csi-hostpath-driverinfo.yaml
kubectl apply -f deploy/kubernetes-1.21-test/hostpath/csi-hostpath-provisioner.yaml
kubectl apply -f deploy/kubernetes-1.21-test/hostpath/csi-hostpath-resizer.yaml
kubectl apply -f deploy/kubernetes-1.21-test/hostpath/csi-hostpath-snapshotclass.yaml
kubectl apply -f deploy/kubernetes-1.21-test/hostpath/csi-hostpath-snapshotter.yaml
Для использования снимков прежде всего нужно зарегистрировать новый VolumeSnapshotClass:
snapshot.yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: csi-hostpath-snapclass
driver: hostpath.csi.k8s.io
deletionPolicy: Delete
kubectl apply -f snapshot.yaml
kubectl patch storageclass standard -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
kubectl patch storageclass csi-hostpath-sc -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
а затем пересоздадим наше развертывание базы данных, заменив в yaml-файле выше storageClassName на csi-hostpath-sc
:
kubectl delete statefulset -n postgres --all
kubectl delete pvc -n postgres --all
kubectl apply -f postgresql.yaml
При развертывании из шаблона будет создан PersistentVolumeClaim на 4Гб, который будет обработан через Provisioner контроллер, который в свою очередь передает ресурс для Attacher и создает каталог по расположению /var/lib/kubelet/plugins/kubernetes.io/csi/pv/postgres-postgres-0/globalmount
. Далее после запуска контейнера создается точка монтирования /var/lib/kubelet/pods/<pod-uuid>/volumes/</pod-uuid>kubernetes.io~csi/postgres-postgres-0/mount/
и связывается с каталогом данных контейнера (в нашем случае/var/lib/postgresql/data/pgdata
). При удалении запрошенного развертывания (StatefulSet) согласно политике по умолчанию reclaimPolicy: Delete
запрошенные ресурсы также будут удалены из файловой системы (с использованием Provisioner, который наблюдает за обновлениями ресурсов PersistentVolumeClaim и вызывает необходимые действия со стороны драйвера). Более подробно о процессе выделения места и связывания ресурса с контейнером можно почитать в этой статье.
Теперь, когда у нас создан PersistentVolume с использованием CSI можем перейти к созданию резервных копий через механизм снимков. В первую очередь создадим описание ресурса для создания снимка (здесь нам пригодится VolumeSnapshotClass, созданный ранее).
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
namespace: postgres
name: postgres-snapshot
spec:
volumeSnapshotClassName: csi-hostpath-snapclass
source:
persistentVolumeClaimName: postgres-postgres-0
Получить список активных снимков можно в перечислении ресурсов типа volumesnapshots (или сокращенно vs), а информацию об их содержании - volumesnapshotcontents (или vsc).
Для восстановления данных из снимка требуется добавить секцию dataSource в описание PersistentVolumeClaimTemplate или PersistentVolumeClaim (при создании запроса ресурса вне StatefulSet), например следующим образом.
volumeClaimTemplates:
- metadata:
name: postgres
spec:
storageClassName: "csi-hostpath-sc"
dataSource:
name: postgres-snapshot
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 4Gi
После первоначального восстановления dataSource может быть удален.
При использовании любых других CSI-драйверов (например, при развертывании Kubernetes на облачных провайдерах) механизм создания снимков остается тем же, в общем случае изменяется только storageClassName и, иногда, конфигурация снимков. Список доступных драйверов CSI можно посмотреть на странице, там же есть информация о поддержке мгновенных снимков и возможности динамического выделения ресурсов и/или изменения выделенного объема системы хранения.
Сейчас мы рассмотрели вопросы создания резервных копий как в ручном сценарии с извлечением файлов и отправкой в управляемый репозиторий, так и с использованием возможностей CSI-драйвера по созданию мгновенных снимков. Во второй части статьи поговорим о возможностях автоматизации процесса конфигурирования резервного копирования, о стратегиях копирования распределенных систем и о sidecar-контейнерах, которые помогут нам получить доступ к разделам с режимом доступа ReadWriteOnce.
А прямо сейчас приглашаю всех читателей на бесплатный урок по теме: "Контроллеры репликации, сеты репликации и балансировка нагрузки". Регистрация на урок доступна по ссылке ниже.
Зарегистрироваться на бесплатный урок.