Postgres Operator от Zalando: разворачиваем PostgresCluster вместо ручных реплик
Операторный паттерн в Kubernetes взрослеет: берём Zalando Postgres Operator, сравниваем с Crunchy Data и разбираем что CRD реально берёт на себя в продакшне.
Операторный паттерн в Kubernetes зрелеет: крупные вендоры публикуют операторы для stateful-сервисов
С операторами в Kubernetes у нас были отношения по принципу «знаем, что существует, но в продакшн не трогаем». Паттерн красивый - берёшь CRD, описываешь желаемое состояние кластера баз данных как Kubernetes-ресурс, контроллер сам разруливает failover, реплики, бэкапы. На практике год назад всё это было в состоянии «работает, пока не надо ничего нестандартного». Но за последние месяцы несколько крупных вендоров выкатили достаточно зрелые операторы для stateful-сервисов, и мы решили посмотреть внимательнее - на конкретной задаче, а не в вакууме.
Задача: кластер PostgreSQL для одного из клиентов, который живёт в Kubernetes и до сих пор управляется руками. Три ноды, Patroni поверх etcd, всё поднято через Helm-чарт с горой кастомных values. Работает, но каждый раз когда надо добавить реплику или настроить бэкап - это ручная работа и знание о том как устроено хранится в головах, а не в Git.
Почему Zalando, а не что-то другое
Основных игроков сейчас два: Zalando Postgres Operator и Crunchy Data PGO (Postgres Operator). Есть ещё KubeDB, но это платный продукт с другой бизнес-моделью.
Zalando Operator - open source, активно поддерживается, написан на Go, CRD называется postgresql (в API-группе acid.zalan.do). Активно используется у самого Zalando в production с большим количеством кластеров.
Crunchy Data PGO - тоже open source, более enterprise-ориентированный, CRD pgclusters (группа crunchydata.com), v4 вышел недавно и изменил модель управления достаточно радикально по сравнению с v3. Crunchy делают certified PostgreSQL дистрибутив и в этом смысле экспертиза у них глубокая.
Выбрали Zalando для первого захода по нескольким причинам: документация лучше структурирована, примеры ближе к нашим сценариям, и оператор уже был знаком паре человек в команде.
Что CRD берёт на себя
Минимальный манифест кластера выглядит так:
apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
name: prod-db-cluster
namespace: databases
spec:
teamId: "adg"
volume:
size: 100Gi
storageClass: ceph-block
numberOfInstances: 3
postgresql:
version: "12"
users:
appuser:
- superuser
- createdb
databases:
appdb: appuser
resources:
requests:
cpu: 500m
memory: 2Gi
limits:
cpu: 2
memory: 8Gi
Что происходит после kubectl apply:
- Оператор разворачивает StatefulSet с нужным числом подов.
- Внутри каждого пода - Patroni, настроенный автоматически, с etcd или с Kubernetes-ом как distributed configuration store (мы выбрали Kubernetes endpoints как более простой вариант без внешней зависимости).
- Создаются Service-ы: один для primary (на который пишут), один для реплик (read-only), один headless для Patroni.
- Пользователи и базы из spec создаются автоматически при первом старте.
- Секреты с паролями генерируются и кладутся в Kubernetes Secrets.
Failover. При падении primary Patroni переключает роль на одну из реплик, оператор обновляет endpoint primary-сервиса. Приложение переподключается и продолжает работать. На тестах задержка переключения была в диапазоне 15-30 секунд - это Patroni-поведение, оператор тут не добавляет лишнего.
Масштабирование. Чтобы добавить реплику - меняешь numberOfInstances: 4 и делаешь apply. Оператор добавляет под в StatefulSet, дожидается когда Patroni введёт его в кластер как standby, всё. Чтобы убрать - уменьшаешь число. До этого добавление реплики вручную занимало добрые полчаса с pg_basebackup и настройкой recovery.conf.
Бэкап: Spilo + WAL-G
Образ, который Zalando Operator использует по умолчанию, - это Spilo, их собственный образ PostgreSQL со встроенным WAL-G для бэкапов. Настройка через аннотации на кластере:
annotations:
wal-g.zalando.org/wal-s3-bucket: "s3://backup-bucket/postgres"
wal-g.zalando.org/backup-schedule: "0 3 * * *"
WAL-G умеет в S3-совместимое хранилище, что в нашем случае - это Ceph Object Gateway. Полный бэкап раз в сутки, WAL-стриминг непрерывно, point-in-time recovery из коробки. Проверили восстановление на отдельном кластере - работает, время восстановления зависит от размера базы и объёма WAL за период.
Раньше бэкап был скриптом в cron на одной из нод. Если нода падала вместе со скриптом - бэкап молча не снимался. Теперь это забота оператора.
Что не так просто
Несколько мест где пришлось повозиться.
Connection pooling. Оператор умеет разворачивать pgBouncer рядом с кластером, но конфигурация через аннотации - не очень очевидная. Понадобилось время чтобы правильно настроить режим pooling и убедиться что приложение использует именно bouncer, а не напрямую primary.
Кастомные параметры PostgreSQL. Через spec.postgresql.parameters - работает, но изменение параметров требующих рестарта приводит до rolling restart кластера. Это правильно, но надо понимать заранее - в рабочее время лучше не трогать.
Мониторинг. Оператор сам по себе метрики в Prometheus не пушит - нужен postgres-exporter в качестве sidecar или отдельного деплоя. Документация это описывает, но подразумевает что вы сами это соберёте.
Crunchy Data PGO: пара слов для сравнения
Мы подняли тестовый кластер через PGO v4 параллельно. Основное отличие которое бросается в глаза: в Crunchy модель более декларативная и явная. Бэкап, мониторинг, пулер - всё описывается в одном CRD pgclusters с явными секциями, а не через аннотации. Это удобнее для ревью в Git - видно всё в одном месте.
С другой стороны, PGO v4 - достаточно свежий, и документация местами догоняет код. Мы несколько раз натыкались на поведение, которое не совпадало с тем что написано в docs. У Zalando Operator ощущение более обкатанного продукта - возможно потому что они его реально используют сами в нагруженном production.
Где сейчас
Клиентский кластер мигрирован на Zalando Operator в staging. Production пока стоит на старой схеме с Patroni вручную - ждём ещё пару недель наблюдений в staging перед переключением. Сама миграция потребовала pg_dump/restore, прямой in-place переход мы не стали делать - слишком много ручного состояния в старой конфигурации.
Общее впечатление: паттерн работает и для stateful-workload-ов. Главный выигрыш не в том что что-то стало быстрее, а в том что операционные процедуры - failover, добавление реплики, бэкап - теперь воспроизводимы и описаны в коде. Это как раз то что нам нужно в managed-сопровождении - не «у нас есть человек который это умеет», а «это работает так потому что так написано в манифесте».