Всем привет! В этой статье поговорим про бэкапы PostgreSQL в Kubernetes через призму самого популярного опенсорс-оператора для этой СУБД — CloudNativePG. Мы расскажем о том, как внедрение нового решения на основе WAL-G позволило ускорить резервное копирование и восстановление больших баз данных и поделимся своим опытом доработки CloudNativePG. На связи Иван Архипов, ведущий разработчик в команде платформы данных в Yandex Cloud, и я приглашаю под кат всех, кому интересна эксплуатация PostgreSQL в Kubernetes!

Как мы пришли к CloudNativePG и почему пришлось его дорабатывать
Создавая новую платформу Stackland в Яндексе, мы стремились сделать продукт, который позволит клиентам запускать PaaS- и SaaS-сервисы Yandex Cloud на собственных мощностях. Вместо инфраструктуры публичного облака, рассчитанной на десятки миллионов пользователей, мы взяли более легковесный Kubernetes как фундамент, вместо виртуальных машин — контейнеры, а в качестве control plane для managed-решений — Kubernetes-операторы.
Одним из первых в каталоге PaaS-решений Stackland появился managed-сервис для PostgreSQL. За основу мы взяли оператор CloudNativePG (сокращенно CNPG) — нам понравился его kubernetes-native дизайн и крепкий опенсорс-фундамент (это единственный проект в CNCF для управления PostgreSQL). Но первые же тесты под нагрузками показали, что уже на кластерах от 50 ГБ встроенный механизм резервного копирования и непрерывной архивации на основе Barman Cloud работает не так хорошо, как нам бы хотелось.
На масштабах большой облачной платформы даже редкие проблемы встречаются достаточно часто, так что мы можем быстро их выявлять и системно исправлять. Для решения on-premises хотелось сохранить похожий опыт эксплуатации, несмотря на дополнительные факторы риска: нюансы оборудования и отсутствие контроля над инфраструктурой клиента.
В итоге мы попробовали перенести свой опыт работы с резервными копиями в публичном облаке на рельсы CNPG и хотим рассказать, как из этого получился наш опенсорс-плагин CloudNativePG для бэкапов.

Как устроено резервное копирование PostgreSQL в CloudNativePG
Традиционно для PostgreSQL есть два вида бэкапов:
логические дампы, создаваемые через
pg_dump, которые по сути являются выгрузкой всех данных в SQL-скрипт,физические бэкапы, создаваемые через
pg_basebackup, сохраняющие все бинарные файлы кластера (в том числе индексы, WAL-файлы последних примененных транзакций, конфигурацию кластера и прочие полезности).
У физических бэкапов есть важное преимущество — если в дополнение к ним постоянно выгружать WAL-файлы (они же журналы транзакций) в процессе работы кластера, мы получим возможность восстановиться на любой момент времени, начиная с момента завершения физического бэкапа, и заканчивая временем последнего выгруженного WAL-файла. Это Point-in-time recovery (или PITR), удобная штука, когда надо спасти коллег от нечаянного удаления какой-нибудь очень важной таблички.

В CNPG для резервного копирования используются физические бэкапы вкупе с непрерывной архивацией WAL-файлов с помощью инструмента Barman Cloud от тех же разработчиков, что и сам CloudNativePG. Настроить резервное копирование и создать первую копию можно в три шага:
с помощью
kubectl applyсоздаём специальный ресурс с типомObjectStoreдля настроек хранилища бэкапов
манифест ObjectStore
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
name: my-minio-store
spec:
configuration:
destinationPath: s3://backups/
endpointURL: http://minio:9000
s3Credentials:
accessKeyId:
name: minio
key: ACCESS_KEY_ID
secretAccessKey:
name: minio
key: ACCESS_SECRET_KEY
wal:
compression: gzip
«прикрепляем» настройки хранилища к кластеру (необходимо использовать разные префиксы S3
destinationPathдля разных кластеров, чтобы бэкапы не перезаписывали друг друга)
прикрепляем ObjectStore к ресурсу Cluster
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-example
spec:
#<...> настройки кластера, дисков, ресурсов, etc.
plugins:
- name: barman-cloud.cloudnative-pg.io # Указываем стандартный плагин BarmanCloud
isWALArchiver: true # Включить непрерывную архивацию WAL-логов в S3
parameters:
barmanObjectName: my-minio-store # Указываем имя ресурса ObjectStore с настройками хранилища бэкапов.
создаём ресурс
Backupдля создания резервной копии
манифест ресурса Backup
apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata:
name: backup-example
spec:
cluster:
name: cluster-example
method: plugin
pluginConfiguration:
name: barman-cloud.cloudnative-pg.io
И хотя это решение широко используется и долгое время являлось единственным инструментом резервного копирования в CNPG, оно имело ряд существенных для нас недостатков:
Скорость создания и выгрузки бэкапов в S3: однопоточное сжа��ие данных на больших базах данных становилось бутылочным горлышком, забивая одно ядро процессора и радикально замедляя создание резервной копии даже при наличии свободных вычислительных мощностей.
Отсутствие инкрементальных резервных копий, когда в хранилище записываются только те данные, которые изменились с момента создания предыдущей полной резервной копии. Это серьёзно увеличивало потребление дискового пространства и скорость создания новых бэкапов.
Нехватка «умного» retention, автоматически очищающего старые автоматические бэкапы, но сохраняющего пользовательские.
В итоге стало понятно, что просто «включить» и использовать стандартный механизм бэкапов нам не хватит. Мы пошли искать способы прикрутить свой механизм создания и управления бэкапами через WAL-G, опенсорс-инструмент, развиваемый Яндексом и отлаженный многолетним опытом обслуживания десятков тысяч баз данных в Yandex Cloud.
Как дорабатывать CloudNativePG, не меняя его код
Одной из сильных сторон CloudNativePG является его расширяемость через механизм плагинов. По сути плагин — это любой внешний по отношению к CNPG сервис, который поддерживает определённый gRPC API.
О существовании плагина контроллер CNPG узнаёт через наличие ресурса Service, у которого указаны такие метаданные:
лейбл
cnpg.io/pluginName: <имя плагина>аннотация
cnpg.io/pluginPort: "<порт, на котором работает GRPC API плагина>"аннотация
cnpg.io/pluginClientSecret: "<ресурс Secret с клиентским TLS-сертификатом>"аннотация
cnpg.io/pluginServerSecret: "<ресурс Secret с серверным TLS-сертификатам>"
В силу специфики gRPC для взаимодействия необходимо генерировать TLS-сертификаты для сервера и для клиента, мы это делаем с помощью cert-manager на этапе установки плагина через helm-чарт.
Контроллер CloudNativePG обращается к плагину при выполнении основных действий, таких как создание/изменение кластера, создание инстансов PostgreSQL, создание бэкапов или архивация WAL-логов. Плагин, в свою очередь, может повлиять на результат выполнения действия, например, изменить на лету спеку пода PostgreSQL, добавив туда сайдкар-контейнер для мониторинга. Либо он может вообще самостоятельно выполнить запрашиваемое действие (в случае создания бэкапов или архивации WAL-логов).
API вызовов и обёртки для них на Golang реализованы в отдельном репозитории CNPG Interface, а их описание — в файле protocol.md
Основные вызовы, которые определяются интерфейсом CNPG-I
BackupRequest — запрос на создание полной резервной копии. Позволяет полностью переложить процесс создания бэкапа на плагин.
RestoreRequest — запрос на восстановление кластера из резервной копии. Аналогично предыдущему, перекладывает процесс восстановления на плагин.
WALArchiveRequest — запрос на архивацию WAL-файла. Вызывается, когда мастер закончил формирование очередного WAL-файла и его можно выгрузить в S3.
WALRestoreRequest — запрос на восстановление WAL-файла из хранилища. Вызывается, например, когда отставшей реплике необходимо дополучить WAL-файлы, чтобы «догнать» изменения мастера.
CollectMetricsRequest — запрос на сбор метрик с кластера. Можем собрать какие-нибудь дополнительные метрики и передать их экземпляру CNPG, а тот уже экспортирует их в Prometheus-формате.
OperatorMutateClusterRequest вызывается, когда произошли любые изменения с ресурсом Cluster и позволяет дополнительно модифицировать его «на лету», перед применением. Идейно похож на Mutating admission webhook в Kubernetes.
OperatorValidateClusterCreateRequest и OperatorValidateClusterChangeRequest — аналогично предыдущему вызываются при изменениях/созданиях ресурса Cluster и позволяют отклонить операцию, если какие-то параметры выбраны неверно (например, параметры, специфичные для плагина).
OperatorLifecycleRequest — один из самых интересных запросов, вызывается при создании любых Pod'ов и Job'ов, связанных с инициализацией и работой кластера, и позволяет «на лету» изменять их (например, добавить какие-нибудь переменные среды, Volume или сайдкар-контейнеры).
EnrichConfigurationRequest — позволяет вносить свои изменения в параметры PostgreSQL (
postgresql.conf), сгенерированные CloudNativePG.
По сути этот набор вызовов позволяет внедрить собственную логику почти на всех этапах жизненного цикла кластера без необходимости менять кодовую базу CloudNativePG, что очень удобно для нашей ситуации.
Мы разделили реализацию плагина на два компонента:
Сайдкар-контейнер, сосуществующий рядом с каждым экземпляром PostgreSQL. Его задача — выполнять «на земле» все операции, связанные с бэкапами и WAL-логами.
Контроллер, отвечающий на запросы жизненного цикла (OperatorLifecycleRequest). Он существует как отдельный ресурс Deployment в кластере и выполняет задачи по модификации подов PostgreSQL для подстановки сайдкар-контейнера, назначению ролей и прав для доступа к ресурсам, связанным с бэкапами (например, Secret с ключами S3), а также определением и поддержкой собственного ресурса Kubernetes
BackupConfig, который, аналогичноObjectStoreот Barman, содержит все настройки бэкапов для кластера.
Схематично эта реализация выглядит так (серым выделены компоненты плагина):

Для пользователя взаимодействие с плагином WAL-G практически не отличается от существующего решения: для описания конфигурации бэкапов используется схожий ресурс BackupConfig (полный пример можно посмотреть в репозитории).
А настройка и создание резервной копии аналогично выполняются в три шага:
с помощью
kubectl applyсоздаём специальный ресурс с типомBackupConfigдля настроек хранилища бэкапов
Пример манифеста ресурса BackupConfig
apiVersion: cnpg-extensions.yandex.cloud/v1beta1
kind: BackupConfig
metadata:
name: example-backup-config
spec:
storage:
type: s3
s3:
prefix: s3://<BUCKET_NAME>/<PREFIX_NAME> # Префикс S3 для хранения бэкапов. Не должен совпадать с префиксами для других кластеров для избежания коллизий и перезаписи бэкапов.
region: ru-central1 # Регион S3-хранилища
endpointUrl: https://storage.yandexcloud.net # Эндпоинт S3-хранилища
forcePathStyle: false # Использовать формат с поддоменами (false, по-умолчанию) или с суффиксами (устаревший, но необходим для хранилищ без поддоменов, например - для хранилищ на основе Minio)
accessKeyId:
name: "example-s3-credentials" # Имя ресурса Secret с ключом ACCESS_KEY
key: "accessKey" # Поле в ресурсе Secret с ключом ACCESS_KEY
accessKeySecret:
name: "example-s3-credentials" # Имя ресурса Secret с ключом SECRET_KEY
key: "secret" # Поле в ресурсе Secret с ключом SECRET_KEY
retention: # Политики авто-удаления резервных копий спустя время
minBackupsToKeep: 5 # Минимальное кол-во бэкапов, которые нужно удерживать от удаления (имеет приоритет над настройкой deleteBackupsAfter)
deleteBackupsAfter: 1d # Срок, после которого бэкап можно удалить
encryption: # Настройки шифрования бэкапов
method: libsodium # Метод шифрования (на данный момент поддерживается libsodium)
encryptionSecret: example-encryption-secret # Имя ресурса Secret с ключом libsodium
«прикрепляем» настройки хранилища к кластеру
прикрепляем BackupConfig к ресурсу Cluster
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-example
spec:
#<...> настройки кластера, дисков, ресурсов, etc.
plugins:
- name: cnpg-extensions.yandex.cloud # Указываем стандартный плагин BarmanCloud
isWALArchiver: true # Включить непрерывную архивацию WAL-логов в S3
parameters:
backupConfig: example-backup-config # Указываем имя ресурса BackupConfig с настройками хранилища бэкапов.
создаём ресурс
Backupдля создания резервной копии
манифест ресурса Backup
apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata:
name: backup-example
spec:
cluster:
name: cluster-example
method: plugin
pluginConfiguration:
name: cnpg-extensions.yandex.cloud
Таким образом, минимальными изменениями в манифестах, мы получаем:
Более производительные резервные копии с WAL-G.
Поддержку инкрементальных бэкапов, что кратно сокращает потребление места в хранилище.
Возможность автоматического удаления только периодических бэкапов (создаваемых через ресурс
ScheduledBackup), оставляя управление ручными бэкапами пользователю.
Для наглядности мы замерили время создания и восстановления, а также потребление места в S3 с помощью плагина Barman и плагина WAL-G на кластерах разных размеров:

Графики наглядно демонстрируют, насколько быстрыми и эффективными в плане потребляемого места могут быть резервные копии в CloudNativePG с использованием WAL-G, а использование инкрементальных бэкапов может дополнительно кратно сократить потребление места, хоть и ценой небольшого увеличения времени восстановления.
Детали бенчмарков и как воспроизвести результаты
Для проведения тестов использовалась однонодовая инсталляция kubernetes в Yandex Cloud на 1 ВМ в зоне доступности ru-central1-a со следующими характеристиками:
16vCPU поколения Ice Lake (100% Guaranteed)
64GB RAM
744GB DISK, Non-replicated SSD, 75000 / 44800 IOPS read/write, 880 MB/s / 656 MB/s Max. bandwidth (read / write), no encryption
Версии основных компонентов:
Kubernetes:
1.33.6+k3s1CloudNativePG:
1.27.1Barman Cloud Plugin:
0.7.0CNPG Plugin WAL-G:
0.2.2
Для хранения бэкапов использовалось S3-совместимое хранилище Yandex Object Storage класса STANDARD.
Конфигурация кластера CNPG и хранилища бэкапов для плагина Barman:
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
name: barman-object-store
spec:
configuration:
destinationPath: s3://cnpg-test-backups-barman/
endpointURL: https://storage.yandexcloud.net
s3Credentials:
region:
name: s3-credentials
key: region
accessKeyId:
name: "s3-credentials"
key: "accessKey"
secretAccessKey:
name: "s3-credentials"
key: "secret"
data:
compression: snappy # используем сжатие snappy, см. примечание после манифестов
wal:
compression: snappy
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-example
spec:
instances: 1
plugins:
- name: barman-cloud.cloudnative-pg.io
isWALArchiver: false
parameters:
barmanObjectName: barman-object-store
storage:
size: 700Gi
---
apiVersion: v1
kind: Secret
metadata:
name: s3-credentials
type: Opaque
stringData:
region: ru-central1
accessKey: ******
secret: ******
Доступные методы сжатия для Barman описаны в документации, из них осознанно был выбран snappy как наиболее быстрый при приемлемом качестве сжатия, для других методов сжатия результаты по занимаемому месту могут отличаться в меньшую сторону, однако скорость создания и восстановления может также уменьшаться в разы.
Конфигурация кластера CNPG и хранилища бэкапов для плагина WAL-G:
apiVersion: cnpg-extensions.yandex.cloud/v1beta1
kind: BackupConfig
metadata:
name: walg-backup-config
spec:
deltaMaxSteps: 7
uploadDiskRateLimitBytesPerSecond: 0 # снято ограничение на скорость чтения с диска и выгрузки в S3
storage:
type: s3
s3:
prefix: s3://endevir-backups-walg-64/
region: ru-central1
endpointUrl: https://storage.yandexcloud.net
forcePathStyle: false
storageClass: STANDARD
accessKeyId:
name: "s3-credentials"
key: "accessKey"
accessKeySecret:
name: "s3-credentials"
key: "secret"
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-example
spec:
instances: 1
plugins:
- name: cnpg-extensions.yandex.cloud
isWALArchiver: true
parameters:
backupConfig: walg-backup-config
storage:
size: 700Gi
---
apiVersion: v1
kind: Secret
metadata:
name: s3-credentials
type: Opaque
stringData:
region: ru-central1
accessKey: ******
secret: ******
Для генерации тестовых данных использовался инструмент pgbench с разным scale factor, запускался с пода postgres:
Для БД размером 64ГБ:
pgbench -i -s 4384 -d appДля БД размером 128ГБ:
pgbench -i -s 8769 -d appДля БД размером 256ГБ:
pgbench -i -s 17537 -d app
Перед созданием инкрементальных копий с плагином WAL-G для БД дополнительно генерировался 1 млн. транзакций-изменений. Этим мы делали условия для создания более приближенными к реальности — инкрементальная копия создавалась на изменённых случайным образом данных относительно родительской копии.
Случайные транзакции генерировались следующей командой: pgbench -j 10 -c 50 -b tpcb-like --transactions=20000 --progress=30 -d app
Собранные грабли и впечатления от CNPG
Одной из самых нетривиальных задач оказалась поддержка инкрементальных бэкапов: CloudNativePG не разделяет бэкапы на «полные» и «инкрементальные». Поэтому нам пришлось добавить дополнительные лейблы и аннотации для того, чтобы понимать, какой бэкап, полный или инкрементальный, и какие бэкапы от него зависят.
Не обошлось и без сложностей с контролем удаления бэкапов: мы не можем удалять полный бэкап, пока существует хотя бы один инкрементальный, зависящий от него. Чтобы решить эту проблему, реализовали finalizer, удерживающий бэкап от удаления до момента, пока существует хоть один зависящий от него бэкап.
Кроме этого, в первых релизах протокола CNPG-I отсутствовала поддержка передачи метрик, связанных с бэкапами и для того, чтобы отдавать метрики, приходилось реализовывать отдельный http-сервер метрик на стороне плагина, что не очень удобно. Благо с релизом 1.27 реализовали эту функциональность в протоколе, и вскоре мы планируем поддержать её в нашем плагине.

CloudNativePG в целом очень порадовал нас возможностями для расширения и отличной пользовательской документацией, но в силу своей молодости сохраняет ещё некоторое количество подводных камней, которые могут обернуться проблемами при эксплуатации. Тем не менее, надо отдать должное разработчикам CNPG, в последних релизах 1.26 и 1.27 уделено очень много внимания стабилизации и защите от потери данных.
Наш плагин для бэкапов WAL-G доступен на GitHub под лицензией Apache 2.0. Будем рады любым предложениям по улучшению и замечаниям, а если хочется попробовать надежное решение для крупных production-баз данных на своей инфраструктуре — приглашаем также оценить Yandex Cloud Stackland =)
