lenec ru

← все посты

Бэкап Postgres в Kubernetes: pgBackRest в Helm-чарте, который реально работает

17K

Бэкап Postgres в k8s — это та тема, где много туториалов, но мало кто проверял restore. Я три раза попадал на ситуацию, когда бэкап был, скрипт pg_dump отрабатывал по cron, файлы лежали в S3, а восстановиться было нельзя: дамп не консистентный, версии не сходятся, или вообще файл повреждён. После третьего раза я перестал использовать pg_dump для prod-бэкапа Postgres и перешёл на pgBackRest.

Расскажу, как поднять pgBackRest в k8s через CloudNativePG-оператор и регулярно проверять, что бэкапы работают. Версии — k8s 1.30, CloudNativePG 1.24, Postgres 16.

Почему не pg_dump

pg_dump делает логический дамп: SQL-инструкции, которые потом надо psql накатить на пустую базу. Это нормально для маленьких баз и миграций между мажорами, но не для бэкапа в проде:

  • Долго восстанавливать. БД на 100 ГБ из дампа — несколько часов.
  • Нет PITR (point-in-time-recovery). Дамп — это снимок «во столько-то», между ними можно потерять данные.
  • На большой базе сам dump блокирует таблицы (даже с --no-locks бывают сюрпризы).

Физический бэкап через WAL и базовые файлы — единственный нормальный путь для prod. Это и делает pgBackRest.

Что такое pgBackRest и почему он

pgBackRest — инструмент для бэкапа Postgres от Crunchy Data. Делает full / differential / incremental бэкап + непрерывный архив WAL. Поддерживает шифрование, параллельную загрузку, S3/GCS/Azure backends.

Альтернативы — Barman, WAL-G, простые wal-archive скрипты. WAL-G тоже хорош и часто используется в k8s, но pgBackRest в комбинации с CloudNativePG-оператором — самый «доковыряный» путь без ручного скриптинга.

CloudNativePG: оператор для Postgres в k8s

Существует несколько операторов: Zalando postgres-operator, Crunchy PGO, CloudNativePG. Я остановился на CloudNativePG (CNPG), потому что:

  • Чистый CRD-first подход, без лишних обвязок.
  • Встроенная поддержка pgBackRest.
  • Активная разработка.
  • Hot standby и failover из коробки.

Ставится одной командой:

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

Появляется namespace cnpg-system с оператором.

Кластер с бэкапом в S3

Сначала создаём bucket для WAL и base backups. Можно один — pgBackRest сам разделит по prefix.

aws s3 mb s3://company-postgres-backup --region eu-central-1

Доступы. Создаёшь IAM user (или IRSA service account на EKS) с правами read/write на bucket. Кладёшь creds в Secret:

apiVersion: v1
kind: Secret
metadata:
  name: postgres-backup-creds
  namespace: db
type: Opaque
stringData:
  ACCESS_KEY_ID: AKIA...
  ACCESS_SECRET_KEY: secret...

На EKS лучше IRSA: создаёшь IAM role, аннотируешь ServiceAccount, и pod без явных credentials получает доступ к S3 через STS-токен. Безопаснее, без секретов в кластере.

Теперь сам кластер:

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgres-prod
  namespace: db
spec:
  instances: 3
  postgresql:
    parameters:
      shared_buffers: "256MB"
      max_connections: "200"
      work_mem: "16MB"
  storage:
    size: 100Gi
    storageClass: gp3
  backup:
    barmanObjectStore:
      destinationPath: s3://company-postgres-backup/postgres-prod
      s3Credentials:
        accessKeyId:
          name: postgres-backup-creds
          key: ACCESS_KEY_ID
        secretAccessKey:
          name: postgres-backup-creds
          key: ACCESS_SECRET_KEY
      wal:
        compression: gzip
        maxParallel: 8
      data:
        compression: gzip
        jobs: 4
    retentionPolicy: "30d"

3 instance — primary + 2 replica с автоматическим failover. WAL уезжают в S3 непрерывно, базовый бэкап делается по расписанию.

Расписание бэкапов

Отдельный CRD ScheduledBackup:

apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
  name: postgres-prod-daily
  namespace: db
spec:
  schedule: "0 2 * * * *"  # каждый день в 02:00
  backupOwnerReference: self
  cluster:
    name: postgres-prod
  method: barmanObjectStore

Cron-формат CNPG — 6 полей (с секундами). После apply раз в сутки запускается полный бэкап. Каждые 5 минут (по умолчанию) уезжает текущий WAL-файл.

Как происходит restore

Этот пункт обязателен к проверке. Я делаю его раз в месяц на тестовом кластере. Без проверки бэкап — это не бэкап.

Recovery нового кластера из бэкапа

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgres-restored
  namespace: db-test
spec:
  instances: 1
  storage:
    size: 100Gi
  bootstrap:
    recovery:
      source: postgres-prod-cluster
      recoveryTarget:
        targetTime: "2026-05-15 14:30:00+00"
  externalClusters:
    - name: postgres-prod-cluster
      barmanObjectStore:
        destinationPath: s3://company-postgres-backup/postgres-prod
        s3Credentials:
          accessKeyId:
            name: postgres-backup-creds
            key: ACCESS_KEY_ID
          secretAccessKey:
            name: postgres-backup-creds
            key: ACCESS_SECRET_KEY
        wal:
          compression: gzip

Указал targetTime — оператор найдёт последний бэкап до этого времени и докатит WAL до нужного момента. Это и есть PITR — можно восстановиться на любую секунду в пределах хранения WAL.

После apply создаётся новый Cluster, оператор поднимает init-pod, скачивает бэкап, накатывает WAL, и через несколько минут у тебя рабочая копия БД.

Проверка: что бэкап реально работает

Я каждый месяц делаю на тестовом неймспейсе:

  1. Apply Cluster с recovery на конкретный targetTime.
  2. Жду готовности (kubectl get cluster postgres-restored -n db-test покажет phase: Cluster in healthy state).
  3. Подключаюсь и делаю SELECT count(*) FROM critical_table; — сравниваю с тем, что было в проде в тот момент.
  4. Удаляю восстановленный кластер.

На прод-объёме (200 ГБ) восстановление занимает 15-30 минут. На больших БД — час-два.

Мониторинг бэкапов

Бэкап без алёртинга — это бэкап только до первого сбоя. CNPG отдаёт метрики Prometheus, нужные:

  • cnpg_pg_stat_archiver_last_archived_wal — имя последнего заархивированного WAL.
  • cnpg_pg_replication_lag — лаг репликации.
  • Возраст последнего бэкапа (через CRD Backup.status.completionTime).

Алёрты:

- alert: PostgresBackupTooOld
  expr: |
    time() - max by (cluster) (
      cnpg_collector_last_available_backup_timestamp
    ) > 86400 * 2
  for: 30m
  annotations:
    summary: "Бэкап Postgres старше 2 дней"

- alert: PostgresWALArchivingStuck
  expr: cnpg_collector_pg_stat_archiver_failed_count > 0
  for: 15m
  annotations:
    summary: "WAL archiving падает с ошибками"

Без таких алертов backup может «тихо» сломаться: креды S3 ротировались, оператор не заархивировал WAL за неделю, а узнаешь только при попытке восстановиться.

Что я делаю в проде

Сноска для контекста: e-commerce, БД 800 ГБ, RPO 5 минут, RTO 30 минут.

  • Полный бэкап раз в день в 02:00 UTC.
  • Differential раз в 6 часов (для быстрого восстановления).
  • WAL архивируется непрерывно с интервалом 5 минут.
  • Retention 30 дней (compliance требует 14, я с запасом).
  • Раз в месяц — drill восстановления на тестовый namespace, ручной grep по SELECT count(*) ключевых таблиц.
  • Раз в полгода — полное восстановление в отдельный кластер, со снятием нагрузки от приложений и сверкой checksums.

Что запомнить

pg_dump — для миграций и маленьких БД, не для prod-бэкапа. pgBackRest через CloudNativePG-оператор — рабочий путь без скриптинга. WAL непрерывно в S3, base backup по расписанию, retention 30 дней. Главное — регулярно проверять restore. Если ты не восстанавливал бэкап последние полгода, считай что у тебя бэкапа нет.

Куда копать: документация CloudNativePG, сайт pgBackRest, и обязательно — пройди руками сценарий PITR на тестовой БД, прежде чем доверять ему в проде.

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

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.