Рецепт 24. Миграция из Crunchy PGO в PostgreSQL 18 с CloudNativePG

Пошаговое руководство по миграции кластера PostgreSQL 17 под управлением Crunchy PGO v6 на PostgreSQL 18 под управлением CloudNativePG. Рассматриваются два пути: офлайн-миграция через pg_dump и онлайн-миграция через логическую репликацию.
Опубликовано:

Руководство по миграции кластера PostgreSQL 17 из Crunchy PGO v6 в PostgreSQL 18 под управлением CloudNativePG. Мы рассмотрим два пути: полностью декларативную офлайн-миграцию с использованием встроенного импорта pg_dump и онлайн-миграцию через нативную логическую репликацию PostgreSQL для минимизации времени простоя (near-zero downtime).

С точки зрения CloudNativePG, источник миграции — это просто эндпоинт PostgreSQL, доступный по сети. Неважно, управляется ли он Crunchy PGO, Zalando, Patroni, RDS или чем-то ещё. Главное — доступность базы, наличие пользователя с достаточными правами и (для онлайн-пути) поддержка логической репликации на источнике.

Этот рецепт опирается на Рецепт 5 и декларативную логическую репликацию из Рецепта 15. Кластер Crunchy PGO здесь рассматривается как «чёрный ящик»: мы разворачиваем его манифестом, фиксируем эндпоинт сервиса и секрет с учетными данными, и передаём эти данные в CloudNativePG. Знание внутренних механизмов PGO не требуется.

Офлайн-путь проще: вся миграция описывается в одном манифесте Cluster, полностью декларативно. Требуется окно обслуживания, пропорциональное объёму данных.

Онлайн-путь использует логическую репликацию для непрерывной передачи данных от источника к целевому кластеру. Это сокращает окно переключения (cutover) до нескольких секунд независимо от объёма данных, но требует дополнительных шагов по настройке и удалению объектов репликации.

Если окно pg_dump допустимо для вашей нагрузки — используйте офлайн-путь. В противном случае переходите к онлайн-миграции.

Пререквизиты

Для локального развёртывания используйте инструменты из Рецепта 1:

Настройка локального окружения

Создайте Kind-кластер и установите CloudNativePG:

BASH
kind create cluster --name cnpg-migration
kubectl config use-context kind-cnpg-migration
Нажмите, чтобы развернуть и увидеть больше

Установите оператор CloudNativePG из официального манифеста:

BASH
kubectl apply --server-side -f \
  https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.29/releases/cnpg-1.29.1.yaml
Нажмите, чтобы развернуть и увидеть больше

Дождитесь готовности оператора и установите каталог образов расширений:

BASH
kubectl rollout status deployment -n cnpg-system cnpg-controller-manager

kubectl apply -f \
  https://raw.githubusercontent.com/cloudnative-pg/artifacts/refs/heads/main/image-catalogs-extensions/catalog-minimal-trixie.yaml
Нажмите, чтобы развернуть и увидеть больше

Каталог предоставляет расширения (например, pgaudit) в виде OCI-образов, которые монтируются в поды через функцию Kubernetes ImageVolume (доступно по умолчанию в K8s 1.35+).

Развёртка исходного кластера PostgreSQL 17

Если вы уже используете Crunchy PGO, пропустите этот раздел. Для примера установим PGO v6:

BASH
git clone https://github.com/CrunchyData/postgres-operator.git
cd postgres-operator
git checkout v6.0.1
kubectl apply -k config/namespace
kubectl apply --server-side -k config/default
cd ..
Нажмите, чтобы развернуть и увидеть больше

Развернём исходный PostgresCluster. Мы создаём базу app с обычным пользователем (app) и специальным пользователем для миграции (cnpg), которого будет использовать CloudNativePG. Параметр wal_level: logical необходим для онлайн-пути.

[postgres-cluster-source.yaml]

YAML
apiVersion: postgres-operator.crunchydata.com/v1beta1
kind: PostgresCluster
metadata:
  name: crunchy
spec:
  postgresVersion: 17
  patroni:
    dynamicConfiguration:
      postgresql:
        parameters:
          wal_level: logical
  instances:
  - name: instance1
    replicas: 1
    dataVolumeClaimSpec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 5Gi
  backups:
    pgbackrest:
      repos:
      - name: repo1
        volume:
          volumeClaimSpec:
            accessModes:
            - ReadWriteOnce
            resources:
              requests:
                storage: 5Gi
  users:
  - name: app
    databases:
    - app
  - name: cnpg
    databases:
    - app
    options: "SUPERUSER"
Нажмите, чтобы развернуть и увидеть больше

После развёртывания нам понадобятся два значения:

Загрузка тестовых данных

Создадим таблицу orders и вставим 1000 строк для симуляции нагрузки:

[sample-data-init.yaml]

YAML
apiVersion: batch/v1
kind: Job
metadata:
  name: sample-data-init
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: psql
        image: ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie
        env:
        - name: PGPASSWORD
          valueFrom:
            secretKeyRef:
              name: crunchy-pguser-cnpg
              key: password
        command:
        - psql
        - -h
        - crunchy-primary.default.svc
        - -U
        - cnpg
        - app
        - -c
        - |
          CREATE TABLE orders (
            id SERIAL PRIMARY KEY,
            description TEXT NOT NULL,
            created_at TIMESTAMPTZ DEFAULT now()
          );
          INSERT INTO orders (description)
          SELECT 'Order ' || g FROM generate_series(1, 1000) AS g;
Нажмите, чтобы развернуть и увидеть больше

Проверяем данные и текущее значение последовательности:

BASH
kubectl run psql-check --rm -it --restart=Never \
  --image=ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie \
  --env="PGPASSWORD=$(kubectl get secret crunchy-pguser-cnpg \
    -o jsonpath='{.data.password}' | base64 -d)" \
  -- psql -h crunchy-primary.default.svc -U cnpg app \
     -c "SELECT count(*) FROM orders;" \
     -c "SELECT last_value FROM orders_id_seq;"
Нажмите, чтобы развернуть и увидеть больше

Совместимость расширений

Crunchy PostgreSQL по умолчанию устанавливает pgaudit. Целевой кластер должен также иметь доступ к этому расширению, иначе pg_restore завершится ошибкой. В этом рецепте мы используем imageCatalogRef и объявляем pgaudit в spec.postgresql.extensions. CloudNativePG монтирует образ расширения как read-only том, делая его доступным для PostgreSQL 18 без изменения базового образа.

Перед реальной миграцией проверьте список всех расширений на источнике (SELECT extname FROM pg_extension) и убедитесь, что все они доступны в целевом образе или каталоге.

Офлайн-миграция

Полностью декларативный путь. Применяем следующий манифест Cluster:

[cluster-offline.yaml]

YAML
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg-app
spec:
  instances: 1
  imageCatalogRef:
    apiGroup: postgresql.cnpg.io
    kind: ClusterImageCatalog
    name: postgresql-minimal-trixie
    major: 18
  storage:
    size: 5Gi
  postgresql:
    extensions:
      - name: pgaudit
    parameters:
      pgaudit.log: "all, -misc"
      pgaudit.log_catalog: "off"
      pgaudit.log_parameter: "on"
      pgaudit.log_relation: "on"
  bootstrap:
    initdb:
      import:
        type: microservice
        databases:
        - app
        source:
          externalCluster: crunchy
  externalClusters:
  - name: crunchy
    connectionParameters:
      host: crunchy-primary.default.svc
      port: "5432"
      user: cnpg
      dbname: app
    password:
      name: crunchy-pguser-cnpg
      key: password
Нажмите, чтобы развернуть и увидеть больше

CloudNativePG подключается к источнику, выполняет pg_dump базы app и восстанавливает схему и данные в новом кластере PostgreSQL 18. Импорт происходит один раз при инициализации.

Дождитесь готовности:

BASH
kubectl wait --for=condition=Ready cluster/pg-app --timeout=600s
Нажмите, чтобы развернуть и увидеть больше

Сверьте количество строк и значения последовательностей на источнике и в целевом кластере:

BASH
# Источник
kubectl run psql-count --rm -it --restart=Never \
  --image=ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie \
  --env="PGPASSWORD=$(kubectl get secret crunchy-pguser-cnpg \
    -o jsonpath='{.data.password}' | base64 -d)" \
  -- psql -h crunchy-primary.default.svc -U cnpg app \
     -c "SELECT count(*) FROM orders;" \
     -c "SELECT last_value FROM orders_id_seq;"

# Целевой кластер
kubectl cnpg psql pg-app -- app \
  -c "SELECT count(*) FROM orders;" \
  -c "SELECT last_value FROM orders_id_seq;"
Нажмите, чтобы развернуть и увидеть больше

После сверки масштабируйте pg-app до нужного количества реплик и перенаправьте приложение на pg-app-rw.default.svc.

Онлайн-миграция

Используйте этот путь, если объём данных слишком велик для окна pg_dump. Логическая репликация работает непрерывно, сокращая окно переключения до секунд.

Развёртка целевого кластера

Манифест почти идентичен офлайн-пути, но с параметром schemaOnly: true. Это значит, что при инициализации импортируется только схема, а данные будут поступать через подписку.

[cluster-online.yaml]

YAML
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg-app
spec:
  instances: 1
  imageCatalogRef:
    apiGroup: postgresql.cnpg.io
    kind: ClusterImageCatalog
    name: postgresql-minimal-trixie
    major: 18
  storage:
    size: 5Gi
  postgresql:
    extensions:
      - name: pgaudit
    parameters:
      pgaudit.log: "all, -misc"
      pgaudit.log_catalog: "off"
      pgaudit.log_parameter: "on"
      pgaudit.log_relation: "on"
  bootstrap:
    initdb:
      import:
        type: microservice
        schemaOnly: true
        databases:
        - app
        source:
          externalCluster: crunchy
  externalClusters:
  - name: crunchy
    connectionParameters:
      host: crunchy-primary.default.svc
      port: "5432"
      user: cnpg
      dbname: app
    password:
      name: crunchy-pguser-cnpg
      key: password
Нажмите, чтобы развернуть и увидеть больше

Настройка логической репликации

Создайте публикацию на источнике с помощью плагина cnpg:

BASH
kubectl cnpg publication create pg-app \
  --external-cluster crunchy \
  --publication migration \
  --all-tables
Нажмите, чтобы развернуть и увидеть больше

Теперь создайте декларативный ресурс Subscription в CloudNativePG:

[subscription.yaml]

YAML
apiVersion: postgresql.cnpg.io/v1
kind: Subscription
metadata:
  name: pg-app-migration
spec:
  cluster:
    name: pg-app
  dbname: app
  name: migration
  externalClusterName: crunchy
  publicationName: migration
  subscriptionReclaimPolicy: delete
Нажмите, чтобы развернуть и увидеть больше

CloudNativePG создаст подписку и начнёт начальную синхронизацию таблиц. Проверьте логи:

BASH
kubectl logs \
  -l cnpg.io/cluster=pg-app \
  --follow \
  | grep -i "logical replication"
Нажмите, чтобы развернуть и увидеть больше

Вы должны увидеть сообщение logical replication apply worker for subscription "migration" has started.

Верификация и переключение (cutover)

Перед переключением проверьте отставание репликации (replication lag):

BASH
kubectl run psql-lag --rm -it --restart=Never \
  --image=ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie \
  --env="PGPASSWORD=$(kubectl get secret crunchy-pguser-cnpg \
    -o jsonpath='{.data.password}' | base64 -d)" \
  -- psql -h crunchy-primary.default.svc -U cnpg app -c "
    SELECT slot_name,
           confirmed_flush_lsn,
           pg_current_wal_lsn(),
           pg_current_wal_lsn() - confirmed_flush_lsn AS lag_bytes
    FROM pg_replication_slots
    WHERE slot_name = 'migration';"
Нажмите, чтобы развернуть и увидеть больше

Когда lag_bytes стабильно близок к нулю, данные синхронизированы. Однако логическая репликация не реплицирует состояние последовательностей (sequences). Синхронизируйте их перед переключением:

BASH
kubectl cnpg subscription sync-sequences pg-app \
  --subscription migration
Нажмите, чтобы развернуть и увидеть больше

Рекомендуется выполнить этот шаг один раз перед окном обслуживания и повторно непосредственно перед перенаправлением трафика.

Когда будете готовы:

  1. Остановите запись на источнике.
  2. Дождитесь, пока lag_bytes станет равным 0.
  3. Выполните финальный sync-sequences.
  4. Масштабируйте pg-app и перенаправьте приложение на pg-app-rw.default.svc.

После подтверждения стабильной работы удалите объекты репликации:

BASH
# Удаляем ресурс Subscription (это удалит SQL подписку)
kubectl delete subscription pg-app-migration

# Удаляем публикацию на источнике
kubectl cnpg publication drop pg-app \
  --external-cluster crunchy \
  --publication migration
Нажмите, чтобы развернуть и увидеть больше

Очистка

Удалите исходный кластер:

BASH
kubectl delete postgrescluster crunchy
Нажмите, чтобы развернуть и увидеть больше

Секции bootstrap.initdb.import и externalClusters в манифесте Cluster нужны только при инициализации. После миграции их можно удалить из манифеста и применить его снова — это не вызовет сбоев в работе кластера.

Сравнение образов и безопасность

Переход на CloudNativePG меняет стек образов. Ниже приведено сравнение размеров и уязвимостей.

Размер сжатых образов

ОбразРольРазмер
crunchy-postgres:ubi9-17.9-2610Источник PGO~346 МБ
postgresql:18-minimal-trixieЦель CNPG~87 МБ
Образ расширения pgauditVolume расширения~44 КБ
plugin-barman-cloudПлагин бэкапов~40 МБ
Итоговый стек CNPG~127 МБ

Уязвимости (CVE) — docker scout quickview

ОбразПакетыCriticalHighMediumLow
crunchy-postgres:ubi9-17.9-261062521561053201
postgresql:18-minimal-trixie14004639

Образ minimal-trixie содержит только PostgreSQL 18, расширения поставляются отдельными OCI-образами. Стек CNPG (~127 МБ) значительно меньше образа Crunchy (~346 МБ). Количество CVE также резко снижено: 0 критических против 2 и всего 4 высоких против 156. Меньше пакетов — меньше поверхность атаки.

Заключение

Оба пути миграции сводятся к манифесту Cluster и данным для подключения к источнику. Офлайн-путь — самый короткий и декларативный. Онлайн-путь делает окно переключения независимым от объёма данных. Аналогичный подход применим и для Percona Operator for PostgreSQL , так как он использует те же соглашения об именах сервисов и секретов.

Манифесты в этом рецепте минималистичны. В продакшне обязательно настройте бэкапы через Barman Cloud Plugin, установите лимиты ресурсов и разверните минимум три инстанса. Подробности смотрите в документации CloudNativePG .

Начать поиск

Введите ключевые слова для поиска статей

↑↓
ESC
⌘K Горячая клавиша