Ещё пару лет назад «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 её не делает.
Что делает оператор
CloudNativePG — это контроллер, который расширяет Kubernetes ресурсом Cluster. Вы описываете желаемое состояние декларативно: сколько инстансов, какой образ, сколько storage, куда бэкапить — а оператор приводит кластер к этому состоянию и поддерживает его. Один Cluster на три инстанса разворачивается как один primary и две реплики с потоковой репликацией между ними.
Ключевое, что CNPG делает сам и постоянно:
- Выбирает и держит primary. Оператор знает, какой под — мастер, и публикует его через отдельный сервис
-rw(read-write). Реплики доступны через-ro(read-only) и-r(любой инстанс). - Делает автоматический failover. Если primary падает, оператор промоутит наиболее свежую реплику и переключает сервис
-rwна неё — приложению не нужно знать, кто теперь мастер, оно всегда ходит на<cluster>-rw. - Архивирует WAL и снимает бэкапы в объектное хранилище (S3, GCS, Azure) через Barman Cloud Plugin, а из этих же WAL умеет восстанавливаться на любую точку во времени (PITR).
- Обновляется без ручных плясок: rolling-рестарт реплик, затем switchover на primary — с контролируемым, коротким переключением.
Важно, что это не «ещё один способ запустить контейнер». Оператор кодирует знания DBA в контроллер: желаемое состояние в git, а согласование с реальностью — на CNPG. Голый Postgres в k8s по-прежнему плохая идея; Postgres под оператором — нормальная.
Голый StatefulSet против CloudNativePG
StatefulSet c Postgres | CloudNativePG 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.)
Ловушки, на которых спотыкаются
- StorageClass. Медленный или сетевой storage убивает Postgres: fsync каждого коммита превращается в сетевой round-trip. Берите локальные NVMe/SSD-тома с нормальным IOPS. И проверьте, что StorageClass поддерживает расширение (
allowVolumeExpansion: true) — иначе увеличить диск потом будет больно. - fsync — не выключать. Соблазн «ускорить» тестовый кластер, отключив fsync, регулярно перетекает в прод и заканчивается порчей данных при первом же сбое ноды. CNPG по умолчанию делает правильно — не переопределяйте это.
- PgBouncer в режиме transaction ломает сессионные фичи.
poolMode: transactionнесовместим с prepared statements «в лоб», advisory locks на сессию иSET-переменными на сессию. Либо приложение это учитывает, либо беритеsession-режим (и меньший выигрыш по коннектам). - requests=limits по памяти. Не давайте Postgres-поду шанс словить OOM из-за burst соседей. Память лучше жёстко фиксировать; CPU — можно оставить бёрстить.
Как проверить, что 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 под нагрузкой, проверенные бэкапы и мажорные апгрейды без даунтайма — про это следующая статья.