CNPG 食谱 24 - 从 Crunchy PGO 迁移到使用 CloudNativePG 管理的 PostgreSQL 18
作者: Gabriele Bartolini
日期: 2026年5月13日
目录
- 先决条件
- 设置本地环境
- 部署源 PostgreSQL 17 集群
- 加载示例数据
- 检查扩展兼容性
- 离线迁移
- 在线迁移
- 部署目标集群
- 设置逻辑复制
- 验证与切换
- 清理
- 镜像体积与安全态势
- 结论
从 Crunchy PGO v6 管理的 PostgreSQL 17 集群迁移到 CloudNativePG 下 PostgreSQL 18 的分步指南。涵盖两条路径:使用 CloudNativePG 内置的 pg_dump 导入的完全声明性离线迁移,以及使用原生 PostgreSQL 逻辑复制实现接近零停机切换的在线迁移。
自从 Crunchy Data 大约一年前被 Snowflake 收购以来,我在 EDB 的工作中,与潜在客户和评估 Kubernetes 上 PostgreSQL 的团队交谈时,反复听到同样的担忧:对 Crunchy PGO 未来的不确定性。问题各不相同(围绕开源 operator 的长期承诺、发布节奏、社区活动),但潜在的担忧是一致的。我应该坦诚地说:鉴于 Crunchy operator 的架构与 CloudNativePG 的根本不同,我对它的直接了解有限,我对其未来可能提供的任何意见充其量只是推测。这里重要的是实际问题:如果你正在考虑你的选择,迁移路径是什么样的?如果你正在运行 Crunchy PGO v6 集群并考虑你的选择,这个食谱将准确地向你展示如何迁移到 CloudNativePG,并在同一操作中升级到 PostgreSQL 18。涵盖两条路径:使用 CloudNativePG 内置的 pg_dump 导入的离线路径,以及使用原生 PostgreSQL 逻辑复制实现接近零停机切换的在线路径。
从 CloudNativePG 的角度来看,迁移的源只是一个通过网络可访问的 PostgreSQL 端点。无论该端点是由 Crunchy PGO、Zalando、Patroni、RDS 还是其他任何东西管理,都无关紧要。重要的是数据库可访问,存在具有足够权限的用户,并且(对于在线路径)源支持逻辑复制。其他一切都是标准的 PostgreSQL 和 CloudNativePG 机制。
本食谱建立在 CNPG 食谱 5 和 CNPG 食谱 15 中引入的声明性逻辑复制的基础上。Crunchy PGO 集群被视为一个黑盒:我们应用一个清单来启动它,记下服务端点和它创建的凭据 secret,并将该信息传递给 CloudNativePG。不需要了解 PGO 内部结构。
两条路径仅在创建 CloudNativePG 集群时有所不同。离线路径是两者中较简单的:整个迁移表示为一个集群清单,端到端完全声明性,无需设置复制。需要一个维护窗口,其时间与数据集大小成比例。
在线路径使用原生 PostgreSQL 逻辑复制来保持数据从源连续流向目标,无论数据集大小如何,都将切换窗口减少到几秒钟,代价是增加几个设置和拆除复制对象的步骤。
如果 pg_dump 窗口在你的工作负载可容忍的范围内,请阅读离线部分并停止。如果不是,请继续阅读在线部分。
以下步骤使用 Kind 作为本地 Kubernetes 环境,但这些清单是纯 YAML,在任何兼容集群上都可以不变地工作。如果你已经有一个运行着 PGO 部署的集群,请直接跳到迁移部分。
先决条件
CNPG 食谱 1 涵盖了完整的本地实验环境设置,并逐步指导安装下面列出的所有工具。如果这是你第一次使用 CloudNativePG,请从那里开始。
- Docker
- Git
- Kind
- 带有
cnpg插件的kubectl
设置本地环境
创建一个 Kind 集群并安装 CloudNativePG:
bash
kind create cluster --name cnpg-migration
kubectl config use-context kind-cnpg-migration
使用 operator 清单安装 CloudNativePG。从安装页面获取最新稳定版本的 URL,然后应用它:
bash
kubectl apply --server-side -f \
https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.29/releases/cnpg-1.29.1.yaml
# 替换为安装页面中的最新发布 URL
等待 operator 变为可用:
bash
kubectl rollout status deployment \
-n cnpg-system cnpg-controller-manager
然后应用 CloudNativePG 扩展镜像目录。该目录提供 pgaudit 和其他扩展作为 OCI 镜像,通过 Kubernetes ImageVolume 功能交付给每个 Pod(从 Kubernetes 1.35 开始默认可用;在 1.33 和 1.34 上必须显式启用 ImageVolume 特性门控):
bash
kubectl apply -f \
https://raw.githubusercontent.com/cloudnative-pg/artifacts/refs/heads/main/image-catalogs-extensions/catalog-minimal-trixie.yaml
检查目录以查看哪些 PostgreSQL 版本和扩展镜像可用:
bash
kubectl describe clusterimagecatalog postgresql-minimal-trixie
部署源 PostgreSQL 17 集群
虽然本食谱使用 PGO v6,但从 CloudNativePG 方面来看,PGO v5 的迁移步骤是相同的:两个版本都使用相同的服务和 secret 命名约定,CloudNativePG 直接连接到 PostgreSQL 端点,无需了解管理它的 operator。如果你已经在运行 v5 集群,请完全跳过本节,直接使用你现有的端点和凭据进入迁移步骤。
按照官方快速入门安装 Crunchy Postgres for Kubernetes (PGO)。克隆 operator 仓库,检出新发布标签,并应用 Kustomize 目标:
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 ..
请查看发布页面以获取当前标签,替换上面的 v6.0.1。
现在部署源 PostgresCluster。该清单创建一个 app 数据库,带有一个常规应用程序用户 (app) 和一个专用的迁移用户 (cnpg),CloudNativePG 将使用后者进行连接。wal_level: logical 已包含在内;它是在线路径所必需的,对离线路径无害。不需要显式的镜像标签:PGO v6 会根据 postgresVersion 自动解析正确的容器镜像。
在应用之前需要注意数据库名称。这里的名称 app 是象征性的;请将其替换为你正在迁移的数据库的名称。CloudNativePG 推荐的模式是每个集群一个数据库(微服务模型)。如果源包含多个数据库,请为每个数据库单独运行此过程。在考虑偏离该模式之前,请参阅 CloudNativePG 常见问题解答。
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"
此处授予 cnpg 用户 SUPERUSER 权限是为了使本食谱自包含。在生产环境中,仅授予 CloudNativePG 将在源上执行的操作所需的权限。
一旦 PGO 协调了集群,本食谱其余部分相关的两个值是:
- 主端点 :
crunchy-primary.default.svc(端口 5432) - 迁移凭据 : secret
crunchy-pguser-cnpg,键password
检查集群状态,直到所有实例报告健康:
bash
kubectl describe postgresclusters.postgres-operator.crunchydata.com crunchy
加载示例数据
以下 Job 创建一个带有 SERIAL 主键的 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-primary.default.svc 可访问的、运行中的 PostgreSQL 17 实例,带有数据、一个值为 1000 的序列和一个名为 cnpg 的超级用户。
检查扩展兼容性
Crunchy PostgreSQL 镜像默认安装 pgaudit。目标集群也必须具备 pgaudit,否则当 pg_restore 在转储中遇到 CREATE EXTENSION pgaudit 时会失败。
本食谱中的两个集群清单都使用 imageCatalogRef 引用之前安装的 postgresql-minimal-trixie 目录,并在 spec.postgresql.extensions 中声明 pgaudit。CloudNativePG 通过 Kubernetes ImageVolume 功能将 pgaudit 扩展镜像作为只读卷挂载到每个 Pod 上,使扩展可用于 PostgreSQL 18,而无需将其嵌入基础操作镜像中。pgaudit GUC 参数在 spec.postgresql.parameters 中设置,CloudNativePG 会自动管理这些参数。在导入过程中,扩展将被干净地创建。
对于实际迁移,请先在源上审计完整的扩展列表(SELECT extname FROM pg_extension),并在开始前确认每个扩展要么在目标镜像中可用,要么可以通过目录中的扩展镜像提供。
离线迁移
这是完全声明性的路径。应用以下集群清单:
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 连接到源,在 app 数据库上运行 pg_dump,并将完整的模式和数据结构恢复到新的 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 窗口不可接受时,使用此路径。逻辑复制在生产环境旁边连续运行,将切换时间减少到排空复制队列所需的几秒钟。它要求源端使用 PostgreSQL 10 或更高版本,这涵盖了所有当前受支持的 PostgreSQL 版本。
部署目标集群
清单与离线路径相同,但有一处更改:schemaOnly: true 指示 CloudNativePG 在引导时仅导入模式。行数据通过下一步设置的订阅到达。
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
等待集群变为就绪状态:
bash
kubectl wait --for=condition=Ready cluster/pg-app --timeout=300s
确认模式已到达但表为空:
bash
kubectl cnpg psql pg-app -- app -c '\dt+'
设置逻辑复制
使用 cnpg 插件在源上创建发布(publication)。它从 pg-app 的 spec 中的 crunchy 外部集群条目派生连接详细信息:
bash
kubectl cnpg publication create pg-app \
--external-cluster crunchy \
--publication migration \
--all-tables
在 CloudNativePG 中,声明性的 Subscription 资源处理此操作:
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 的消息。现在,行数据将从源持续流向 pg-app。
验证与切换
在实际切换之前,至少运行一次预演来测量复制延迟并练习切换流程。检查源上的复制槽:
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 持续接近零时,表明订阅已追赶上。此时行数据已在目标中,但逻辑复制不会复制序列。在同步之前检查目标序列值:
bash
kubectl cnpg psql pg-app -- app \
-c "SELECT last_value FROM orders_id_seq;"
无论复制了多少行,该值都将是 1(未推进序列的默认值)。在切换前同步序列:
bash
kubectl cnpg subscription sync-sequences pg-app \
--subscription migration
在维护窗口前作为预演运行一次,并在重定向流量前立即再运行一次。再次检查目标序列以确认它现在与源匹配:
bash
kubectl cnpg psql pg-app -- app \
-c "SELECT last_value FROM orders_id_seq;"
需要注意的是,PostgreSQL 19 预计将引入通过 CREATE PUBLICATION 和 CREATE SUBSCRIPTION 对象复制序列状态的原生支持,这将使此手动步骤变得不必要。该能力是未来 CloudNativePG 集成的有力候选。
当准备上线时,停止对源的写入。等待 lag_bytes 变为零并运行最后一次 sync-sequences。将 pg-app 扩展到所需的副本数,并将你的应用程序重定向到 pg-app-rw.default.svc。
一旦确认应用程序正常运行,清理复制对象:
bash
# 删除订阅资源
# (subscriptionReclaimPolicy: delete 会删除底层的 SQL 订阅)
kubectl delete subscription pg-app-migration
# 删除源上的发布
kubectl cnpg publication drop pg-app \
--external-cluster crunchy \
--publication migration
清理
当应用程序在 pg-app 上稳定运行后,停用源集群:
bash
kubectl delete postgrescluster crunchy
一旦所有数据库都迁移完毕,就可以删除 PGO operator 及其命名空间。
bootstrap.initdb.import 块和 crunchy 的 externalClusters 条目仅在初始引导期间被参考,对运行中的集群没有影响。迁移完成后,你可以从集群清单中删除这两个部分并应用更改。CloudNativePG 将进行协调,不会造成任何中断。
要拆除本食谱中使用的本地 Kind 环境:
bash
kind delete cluster --name cnpg-migration
镜像体积与安全态势
迁移到 CloudNativePG 也会改变你拉取和操作的镜像栈。下表量化了这种变化。拉取大小是从 OCI 清单层数据测量的压缩后大小;漏洞计数来自 docker scout quickview。
压缩后的拉取大小
| 镜像 | 角色 | 压缩后拉取大小 |
|---|---|---|
registry.developers.crunchydata.com/crunchydata/crunchy-postgres:ubi9-17.9-2610 |
PGO 源集群 | ~346 MB |
ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie |
CNPG 目标(本食谱) | ~87 MB |
pgaudit 扩展镜像 |
pgaudit OCI 镜像卷 |
~44 KB |
ghcr.io/cloudnative-pg/plugin-barman-cloud:v0.12.0 |
CNPG 备份插件 | ~40 MB |
CNPG 最小 + pgaudit + Barman Cloud 插件 |
目标总计 | ~127 MB |
CVE 暴露情况 (docker scout quickview)
| 镜像 | 包数量 | 严重 | 高危 | 中危 | 低危 |
|---|---|---|---|---|---|
crunchy-postgres:ubi9-17.9-2610 |
625 | 2 | 156 | 1053 | 201 |
postgresql:18-minimal-trixie |
140 | 0 | 4 | 6 | 39 |
CNPG minimal-trixie 镜像是一个 Debian Trixie Slim 基础镜像,仅包含 PostgreSQL 18,扩展作为 OCI 镜像卷交付。完整的目标栈(操作镜像、pgaudit 扩展镜像、Barman Cloud 插件)总计约 127 MB,而仅 Crunchy 源操作镜像就为 346 MB。CVE 减少更为显著:140 个包对比 625 个,零个严重漏洞对比两个,四个高危漏洞对比 156 个。包数量不仅仅关乎这些宏观数字:更少的包意味着任何未来安全披露的爆炸半径更小。CNPG 最小镜像还附带完整的 SBOM 来源证明,使得审计镜像中确切包含的内容变得简单直接。
结论
两种迁移路径都归结为一个集群清单和源的连接详细信息。离线路径是两者中较短的:整个迁移是一个单一的声明性资源,应用一次。在线路径增加了发布、订阅和 sync-sequences 步骤,但使切换窗口独立于数据集大小。相同的方法同样适用于 Percona Operator for PostgreSQL,后者使用相同的服务和 secret 命名约定。
本食谱中的集群清单故意保持最小化:一个实例,没有备份配置,没有资源限制。它们仅用于教学目的。在生产环境中,你应至少运行三个实例,在重定向流量之前通过 Barman Cloud 插件配置 WAL 归档和备份,并设置适当的资源请求和限制。CloudNativePG 文档涵盖了所有这些内容;请将这里的清单视为起点,而非生产模板。
请继续关注即将发布的食谱!如需最新更新,请考虑订阅我的 LinkedIn 和 Twitter 频道。
如果你觉得这篇文章信息丰富,请随时使用下面提供的链接在你的社交媒体网络中分享。非常感谢你的支持!
本文在 Claude (Anthropic) 的协助下起草和完善。所有技术内容、更正和编辑方向均由作者本人负责。
封面图片:"大象与河马"。
作者
Gabriele Bartolini
EDB 副总裁、Kubernetes 首席架构师 | PostgreSQL 贡献者 | DoK 大使 | CloudNativePG 维护者 | 前 2ndQuadrant(联合创始人)