Перейти к содержимому
Hogin Hogin
Назад

CloudNativePG: prod-ready Postgres в Kubernetes без шаманства

7 мин чтения

Ещё пару лет назад «Postgres в Kubernetes» звучало как антипаттерн: кластер эфемерный, поды переезжают, storage — сетевой, а база хочет ровно противоположного. CloudNativePG (CNPG) меняет ответ. Это Kubernetes-оператор, который берёт на себя всё, чего боялись руками: выбор primary, потоковую репликацию, автоматический failover, бэкапы и восстановление. В 2025 проект получил статус CNCF Incubating, а крупные банки и SaaS публично рассказали о переезде продовых баз на него. Ниже — что именно он даёт и как поднять рабочий кластер на три пода.

Содержание

Открыть содержание

Почему StatefulSet — это ещё не база

Соблазн «просто взять StatefulSet с образом postgres» понятен: он даёт стабильные имена подов и персистентные тома. Но StatefulSet ничего не знает про саму Postgres. Он не отличает primary от реплики, не умеет продвинуть реплику при падении мастера, не архивирует WAL и не проверяет, что бэкап вообще восстанавливается. Всё это — операционная работа, которую при ручной схеме кто-то должен делать руками или сотней строк bash в init-контейнерах.

Итог предсказуем: при падении primary кластер встаёт, потому что никто не решил, кого повышать; при переезде пода на другую ноду приложение час не может найти мастер; а «бэкап» оказывается дампом, который последний раз проверяли на восстановление никогда. Проблема не в том, что Postgres плохо живёт в k8s, а в том, что между «база запущена» и «база переживёт инцидент» лежит вся работа DBA, и StatefulSet её не делает.

StatefulSet даёт поды и тома, но не failover, не бэкап и не выбор primary

Что делает оператор

CloudNativePG — это контроллер, который расширяет Kubernetes ресурсом Cluster. Вы описываете желаемое состояние декларативно: сколько инстансов, какой образ, сколько storage, куда бэкапить — а оператор приводит кластер к этому состоянию и поддерживает его. Один Cluster на три инстанса разворачивается как один primary и две реплики с потоковой репликацией между ними.

Ключевое, что CNPG делает сам и постоянно:

Оператор ведёт Cluster CR: primary + реплики, сервисы -rw/-ro, WAL в S3

Важно, что это не «ещё один способ запустить контейнер». Оператор кодирует знания DBA в контроллер: желаемое состояние в git, а согласование с реальностью — на CNPG. Голый Postgres в k8s по-прежнему плохая идея; Postgres под оператором — нормальная.

Голый StatefulSet против CloudNativePG

StatefulSet c PostgresCloudNativePG Cluster
Выбор primaryвручную / внешний скриптоператор, автоматически
Failover при падении мастеранет, встаёт до вмешательстваавтопромоут реплики за секунды
Точка подключения приложениянадо самому знать, кто primaryстабильный сервис <cluster>-rw
Бэкап и WAL-архивруками (cron + скрипты)Barman Cloud Plugin в S3/GCS
Восстановление на точку (PITR)собирать самомуbootstrap.recovery + recoveryTarget
Реплики для чтениянастраивать вручнуюсервис -ro из коробки
Обновление minor-версииручной рестарт с рискомrolling + switchover оператором
Источник конфигурацииразрозненные манифестыодин декларативный Cluster CR

Что нужно, чтобы поднять кластер

Минимально нужны установленный оператор CloudNativePG и StorageClass, отдающий блочные тома (лучше локальный SSD, а не медленный сетевой NFS). Оператор ставится одним манифестом:

kubectl apply --server-side -f \
  https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.27/releases/cnpg-1.27.0.yaml

Дальше — сам кластер на три инстанса (1 primary + 2 реплики) с синхронной репликацией и выделенным томом под WAL:

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg
  namespace: prod
spec:
  instances: 3
  imageName: ghcr.io/cloudnative-pg/postgresql:17.2

  # synchronous replication: хотя бы одна реплика подтверждает коммит
  postgresql:
    synchronous:
      method: any
      number: 1

  storage:
    size: 50Gi
    storageClass: fast-ssd
  walStorage:            # WAL на отдельном томе — меньше конкуренция за IO
    size: 10Gi
    storageClass: fast-ssd

  resources:
    requests:
      cpu: "1"
      memory: 2Gi
    limits:
      memory: 2Gi        # requests=limits по памяти: без OOM-сюрпризов

Приложение подключается не к поду, а к сервису pg-rw (запись, всегда указывает на текущий primary) и, для read-реплик, pg-ro. Это и есть главный выигрыш: при failover DNS-имя не меняется, меняется только под за ним.

Перед кластером почти всегда ставят пул соединений. Postgres тяжело переносит тысячи коротких коннектов от приложения — на каждый форкается процесс. Pooler поднимает PgBouncer как отдельный слой:

apiVersion: postgresql.cnpg.io/v1
kind: Pooler
metadata:
  name: pg-pooler-rw
  namespace: prod
spec:
  cluster:
    name: pg
  instances: 2
  type: rw
  pgbouncer:
    poolMode: transaction     # для веб-нагрузки почти всегда transaction
    parameters:
      max_client_conn: "1000"
      default_pool_size: "25"

Теперь приложение ходит на сервис pg-pooler-rw, PgBouncer держит небольшой пул реальных коннектов к pg-rw, а тысячи клиентских соединений мультиплексируются в эти 25. Это снимает главный источник боли Postgres под нагрузкой.

А вот бэкап в S3 — сначала описываем хранилище через ObjectStore (ресурс из Barman Cloud Plugin), потом включаем плагин в кластере:

apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
  name: pg-backups
  namespace: prod
spec:
  configuration:
    destinationPath: "s3://my-bucket/pg"
    s3Credentials:
      accessKeyId:
        name: s3-creds
        key: ACCESS_KEY_ID
      secretAccessKey:
        name: s3-creds
        key: ACCESS_SECRET_KEY
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg
  namespace: prod
spec:
  # ...остальное как выше...
  plugins:
    - name: barman-cloud.cloudnative-pg.io
      isWALArchiver: true            # WAL едет в S3 непрерывно
      parameters:
        barmanObjectName: pg-backups

С включённым WAL-архивом у вас есть непрерывная защита: не только ночной снапшот, но и все транзакции между снапшотами — база из которых собирается PITR. (Как из этого восстановиться на конкретную секунду — тема статьи про day-2.)

Ловушки, на которых спотыкаются

Как проверить, что failover работает

Смысл оператора — в том, что вы можете убить primary и ничего не сломается. Проверьте это до прода, а не во время инцидента. Посмотрите статус кластера плагином kubectl-cnpg и грохните текущий мастер:

kubectl cnpg status pg -n prod          # кто primary, кто реплики, лаг
kubectl delete pod pg-1 -n prod         # убиваем текущий primary
kubectl cnpg status pg -n prod -w       # наблюдаем промоут новой реплики

За считанные секунды оператор промоутит свежую реплику, а сервис pg-rw переключится на неё. Приложение, ходящее на pg-rw, переживёт это как короткий разрыв коннекта — и переподключится уже к новому primary. Если у вас перед базой PgBouncer, разрыв ещё короче: клиентские соединения к пулу вообще не рвутся.

Итог

Postgres в Kubernetes без оператора — по-прежнему плохая идея: слишком много операционной работы, которую легко сделать наполовину. CloudNativePG эту работу кодирует в контроллер: Cluster CR даёт primary с репликами, автоматический failover, стабильную точку подключения, бэкапы в S3 и PITR — декларативно, в том же git, что и остальное приложение. Плата — время на понимание оператора и дисциплина со storage. Выигрыш — база живёт рядом с приложением, без отдельной команды DBA и без страха, что «поды переедут». Первый шаг сделали. Дальше начинается day-2: HA под нагрузкой, проверенные бэкапы и мажорные апгрейды без даунтайма — про это следующая статья.


Поделиться:

Предыдущая статья
CloudNativePG в проде: HA, бэкапы и мажорные апгрейды без даунтайма
Следующая статья
Progressive delivery: canary с автоматическим откатом на Flagger