Спокойный сон и крепкие нервы. Резервное копирование для Kubernetes. Часть 1

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру 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.

А прямо сейчас приглашаю всех читателей на бесплатный урок по теме: "Контроллеры репликации, сеты репликации и балансировка нагрузки". Регистрация на урок доступна по ссылке ниже.

Зарегистрироваться на бесплатный урок.

Источник: https://habr.com/ru/company/otus/blog/667644/


Интересные статьи

Интересные статьи

По мере того, как народ пересаживается с зеркалок и беззеркалок на профессиональные видеокамеры, все чаще встает вопрос безграмотной экспозиции. Люди снимают в LOG и RAW, просто потому что могут, не о...
Эта текст покрывает ответы на некоторые совсем базовые вопросы и вместе с тем сразу погружает в проблематику получения ответа на вопрос: "как работать лучше? однопоточно, многопоточно или многопоточно...
Мы продолжаем серию публикаций адаптированного и дополненного перевода "Карманной книги по TypeScript". Другие части: Часть 1. Основы Часть 2. Типы на каждый день Часть...
Привет всем в этом блоге! В предыдущем посте мы рассмотрели базовые концепции жизненного цикла приложения в Red Hat Advanced Cluster Management (ACM) и показали, как их применять на приме...
Барт Хендрикс, понедельник, 2 ноября 2020 г.Первоисточник:[Часть 1 была опубликована на прошлой неделе] Читать далее