Привет, Хабр! 

Мы — команда Рег.Ру, направление облачных сервисов, — однажды решили создать сервис управляемых баз данных, он же DBaaS. На самом деле, это «однажды» не такое уж и спонтанное, сервис требовался в первую очередь для наших разработчиков, но эту историю мы расскажем в другой раз. А сегодня сосредоточимся на технических аспектах DBaaS: на каких технологиях основана услуга, какие инструменты использованы, а также с какими трудностями мы столкнулись при создании и запуске в продакшн. Будет интересно!

Ключевая цель сервиса DBaaS: предоставить пользователям возможность быстро, буквально в пару кликов, запустить н амые популярные СУБД (Система Управления Базами Данных) на рынке: MySQL и PostgreSQL (в интернете, куда ни посмотри, используется какая-то из них). Для обеспечения легковесности решения мы решили взять Kubernetes, так как для запуска любого приложения нет ничего проще. Ну, а раз Kubernetes, как же без операторов для него! Здесь выбор пал на Percona XtraDB Cluster для MySQL, и CloudNativePG для PostgreSQL, — зрелые, функциональные и, что немаловажно, работающие решения. А раз уж мы идем с операторами в полный рост, было бы неплохо и кластерами Kubernetes управлять как-то более-менее автоматизированно. Тут нам пригодился Cluster API.

Хотя до сих пор встречаются староверы, которые считают, что stateful-приложениям не место в кубере, мы в Рег.Ру думаем, что просто нужно уметь их готовить и с ними жить. На очевидные вопросы про stateful в Kubernetes хочется ответить сразу: «Плавали, знаем, умеем, данные не теряем, и нет, не страшно».

Cluster API как способ деплоймента

Cluster API — это инструмент, позволяющий описывать кластеры Kubernetes на базе практически любых облачных сервисов (мы используем OpenStack в качестве IaaS). Общую схему его работы можно изобразить так:

  1. Cluster API самостоятельно заказывает в OpenStack необходимое количество инстансов в нужной конфигурации, указывая все необходимые внутренние параметры инстасов через cloud-init. При этом можно выбрать как из банальной установки ванильного Kubernetes через kubeadm, так и поставить готовый дистрибутив, например, microk8s, Talos или k3s.

  2. Профит! Получаем готовый Kubernetes в нашем собственном облаке.

Нюанс в такой схеме только один: Cluster API тоже нужно где-то запустить. Мы для этого используем отдельный кластер Kubernetes, который называем bootstrap-кластером. Первые такие у нас были развернуты на базе KIND, затем мы начали использовать k0s, но это не принципиально, его можно развернуть чем угодно.

Важно пояснить, как работает Cluster API при обновлении конфигурации нод Kubernetes: он делает Rolling Update, то есть фактически ротирует инстансы, что может иметь определенные последствия при наличии на дисках инстансов стейта (например DATA_DIR’а нашей базы данных). Так что использовать его нужно крайне осторожно и аккуратно, предварительно проведя ряд экспериментов в «песочнице» для выработки целостного понимания, когда все удалится, а когда нет.

Запуск PostgreSQL

Kubernetes у нас теперь есть, попробуем развернуть кластеры БД. 

Начнем с оператора CloudNativePG (CNPG) для PostgreSQL, преимуществами которого являются:

  • Наличие плагина для kubectl

  • Поддержка pgBouncer

  • Поддержка всех видов репликации PostgreSQL

  • Встроенные бекапы через Barman

Интересно еще и то, что CNPG самостоятельно управляет подами в Kubernetes, не прибегая к помощи встроенных механизмов, таких как StatefulSet, умеет менять направление репликации (напомним, в PostgreSQL не бывает мультимастера) и отслеживать состояние и целостность кластера. 

Оператор можно установить через helm, как указано в его официальной документации:

helm repo add cnpg https://cloudnative-pg.github.io/charts
helm upgrade --install cnpg \
 --namespace cnpg-system \
 --create-namespace \
 cnpg/cloudnative-pg

Минималистичный пример того, как можно запустить простой PostgreSQL-кластер с двумя репликами:

---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
 name: mycluster
 namespace: mynamespace
spec:
 instances: 3
 imageName: ghcr.io/cloudnative-pg/postgresql:16.2-14
 storage:
   pvcTemplate:
     resources:
       requests:
         storage: 30Gi
 resources:
   requests:
     memory: "2Gi"
     cpu: 1
   limits:
     memory: "2Gi"
     cpu: 1

Обратите внимание на kind: Cluster — это один из ресурсов CNPG, такую спецификацию можно применить прямо через kubectl apply в наш целевой кластер для баз данных. В приведенном примере оператор создаст асинхронные реплики, а если мы хотим использовать синхронные, можно добавить параметры spec.minSyncReplicas и spec.maxSyncReplicas

Запуск MySQL

Теперь к Percona XTraDB Cluster for MySQL. Это аналогичный CNPG инструмент, но со своими особенностями:

  • Для репликации применяется Galera

  • Можно балансировать соединения с помощью ProxySQL или HAProxy

  • Встроена поддержка бекапов через xtrabackup 

Описание простого кластера тут будет не сложнее, чем для CNPG, но несколько объемнее, потому что содержит описания сразу нескольких компонентов в одном ресурсе:

---
apiVersion: pxc.percona.com/v1
kind: PerconaXtraDBCluster
metadata:
 name: mydb
 namespace: mynamespace
spec:
 allowUnsafeConfigurations: false
 backup:
   allowParallel: false
   image: percona/percona-xtradb-cluster-operator:1.14.0-pxc8.0-backup-pxb8.0.35
   schedule:
   - keep: 5
     name: daily-backup
     schedule: 55 7 * * *
     storageName: s3-swift
   storages:
     s3-swift:
       s3:
         bucket: backup-bucket
         credentialsSecret: pxe-db-backup-secret
         endpointUrl: http://<s3-endpoint>
       type: s3
       verifyTLS: true
 crVersion: 1.14.0
 haproxy:
   affinity:
     antiAffinityTopologyKey: kubernetes.io/hostname
   enabled: true
   image: percona/percona-xtradb-cluster-operator:1.14.0-haproxy
   size: 2
   volumeSpec:
     emptyDir: {}
 initContainer:
   resources:
     requests:
       cpu: "0"
       memory: "0"
 initImage: percona/percona-xtradb-cluster-operator:1.14.0
 logcollector:
   enabled: false
   image: percona/percona-xtradb-cluster-operator:1.14.0-logcollector
 pause: false
 pmm:
   enabled: false
 proxysql:
   enabled: false
 pxc:
   affinity:
     antiAffinityTopologyKey: "kubernetes.io/hostname"
   autoRecovery: true
   envVarsSecret: pxc-db-secret
   image: percona/percona-xtradb-cluster:8.0.35-27.1
   size: 3
 secretsName: pxc-db-superuser-secret
 sslSecretName: pxc-db-cert-secret
 updateStrategy: SmartUpdate
 upgradeOptions:
   apply: disabled

Так как тут используется Galera, мы сразу получаем синхронную репликацию, которую еще иногда называют мультимастер. Сам оператор позволяет добавлять асинхронные реплики, в том числе и использовать в качестве таковой другой кластер на базе Galera.

Вот так легко и непринужденно мы развернули два кластера БД: MySQL и PostgreSQL, по 3 инстанса СУБД в каждом. Давайте разберем, что еще можно подкрутить.

Про ресурсы

Начнем с ресурсов. 

Это крайне важно для корректного планирования нагрузки внутри кластера Kubernetes, в котором развернут сервис DBaaS, так как по сути это единственное, что позволяет ему правильно выбирать ноды для запуска подов. При этом в приведенных примерах у нас указана статическая (или гарантированная) конфигурация для ресурсов, то есть описанное количество CPU и памяти зарезервировано для подов наших СУБД и не может быть выделено для кого-то другого. 

Ключевым преимуществом использования Kubernetes под капотом DBaaS является механизм выделения ресурсов, так как большинство CRI используют cgroups для выделения ресурсов, и мы можем менять их крайне быстро и практически без накладных расходов.

Про бэкапы

Поддержка резервного копирования существует у обоих операторов. Стандартный современный набор бэкендов для хранения резервных копий: S3, GCS, Azure Blob Storage — нам лишь надо задать расписание и путь к хранилищу. 

Вот пример для CNPG с S3:

---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
 name: mycluster
 namespace: mynamespace
spec:
 . . .
 backup:
   barmanObjectStore:
     destinationPath: "s3://<bucket_name>"
     endpointURL: https://s3.example.com
     endpointCA:
       name: ca-certs
       key: ca.crt
     s3Credentials:
       accessKeyId:
         name: cnpg-backup-creds
         key: ACCESS_KEY_ID
       secretAccessKey:
         name: cnpg-backup-creds
         key: ACCESS_SECRET_KEY
   retentionPolicy: "30d"

А вот расписание задается отдельным ресурсом:

---
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
 name: mybackup
 namespace: mynamespace
spec:
 schedule: "0 30 */3 * * *"
 backupOwnerReference: self
 cluster:
   name: mycluster

Создать резервную копию вручную можно посредством  использования ресурса Backup (или плагина для kubectl). А с учетом возможности задать расписание решение становится вполне самодостаточным. Кроме того, дополнительно мы можем  включить сбор метрик с нашего кластера БД и замониторить их создание на случай, если что-либо сломается.

Включить опрос кластера CNPG через Prometheus можно, например, так (требуется Prometheus Operator или VictoriaMetrics Operator с включенной конвертацией ресурсов Prometheus Operator), при этом создастся ресурс PodMonitor, который сконфигурирует Prometheus или VMAgent:

---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
 name: mycluster
 namespace: mynamespace
spec:
 . . .
 monitoring:
   enablePodMonitor: true

Через тернии…

Напоследок хочется рассказать о нескольких проблемах, с которыми мы столкнулись, пока разрабатывали, тестировали и эксплуатировали наш сервис DBaaS:

  1. Limits/Requests. Все знают, что они существуют для CPU и Memory, что ими не стоит пренебрегать, и вообще, они крутые и полезные. Но в какой-то момент эксплуатации нам пришлось описать requests и limits к объему занимаемого подом места. Необходимость их использования показала практика и настроенный мониторинг, который указывал на выселение подов с нод Kubernetes кластера. Случилось это при загрузке большого дампа в MySQL, видимо, слишком большого. resources.requests/limits.ephemeral-storage позволили решить эту проблему еще на этапе планирования размещения пода.

  2. initContainers PXC. Еще один не самый очевидный случай с ресурсами. В Kubernetes есть так называемый effective request/limit — это сумма всех request/limit’ов для контейнеров. Аналогичный параметр есть и для init-container’ов, — effective init request/limit, он формируется как наибольший из заданных для init-container’ов. Оба параметра участвуют в планировании размещения подов и аллокации ресурсов. Теперь, зная это, давайте вспомним Percona XtraDB Operator v13, который для пода c Haproxy в init-контейнер наследовал request/limit от контейнера с СУБД, и получаем ноды, забитые подами с сервисом Haproxy для балансировки.

  3. CNPG и Fencing. В ходе проектирования и разработки сервиса мы добрались до реализации этапа, как должен вести себя кластер в ситуации, когда клиент перестал оплачивать услугу. Удалять его кластер БД вместе с данными, очевидно, не самая удачная  бизнес-идея, и тут нам пригодился Fencing. В CNPG это механизм защиты от повреждения данных, позволяющий гарантированно остановить основной процесс PostgreSQL — postmaster, не останавливая сам под (это нам также сыграло на руку для сохранения аллокации ресурсов на конкретной ноде Kubernetes). Изначально этот механизм создавался с целью получить гарантию того, что PostgreSQL не изменит данные в поде пока происходит какое-либо изучение состояния PostgreSQL по данным на диске. Фича, безусловно, удобная, но проверки на живость конкретного инстанса оказались уж слишком прожорливые. В итоге, вместо Fencing’а стали использовать гибернацию: у этого механизма нет таких особенностей, и он нам тоже вполне подходил, а с точки зрения того, что там происходит внутри — нас устраивает вариант остановить под и при необходимости снова его запустить.

…к звездам

В заключение хочется немного пофантазировать о будущем нашей услуги: рассказать, какие фичи у нас просят пользователи, и одновременно немного спойлернуть наш роадмэп :)

  1. Очень важное продолжение очень важной фичи — реализовать управление бэкапами. Сейчас мы делаем бэкапы незаметно для пользователя, но многие хотели бы иметь возможность самостоятельно управлять временными слотами создания резервных копий, или вовсе отключить резервное копирование. Для них мы собираемся отобразить соответствующий интерфейс в личном кабинете.

  2. Внедрить TLS для соединений с БД. За счет шифрования и обеспечения возможности аутентификации сервера БД, с которым устанавливается соединение, безопасная работа с нашими базами данных может быть организована из любого места.

  3. Добавить расширения  для PostgreSQL. Мы все их регулярно используем, и потому хотим пополнить набор доступных из коробки расширений наиболее популярными и часто запрашиваемыми.

  4. Добавить локали. По результатам наших исследований очень часто приложения могут использовать свои собственные локали, так что мы постепенно расширяем их набор для обеспечения максимальной совместимости с приложениями

  5. Завести новые типы stateful приложений как сервис. MySQL и PostgreSQL весьма популярны, но в мире есть огромное количество других популярных приложений, баз данных, очередей, KV-хранилищ. Так что мы уже начали разрабатывать несколько решений.


Вот такие дела. 

Пишите в комментариях, что вы думаете об архитектуре нашего решения, чего вам не хватило в этой статье, мысли, предложения, — мы будем рады любому фидбеку. 

А про DBaaS в Рег.ру вам сегодня рассказывал Макар Кунгуров, DevOps-инженер из команды облачных сервисов. Как говорится, stay tuned for the next episode!

Комментарии (0)