LabHub
博客

博客

CloudNativePG 深入解析 — 在 Kubernetes 里由谁照看 PostgreSQL

한국어English日本語中文

PostgreSQL 不会照看自己

PostgreSQL 是一个出色的数据库,但放进容器并不会让它自己创建副本、在主库死掉时提升备库、每天做备份、或者回退到昨天下午三点。这些事到目前为止都是人在做。

CloudNativePG(CNPG)就是把这个人的工作变成 由 Kubernetes 控制器持续调谐(reconcile) 的 Operator。本文用一个真实在生产中运行的集群的值,说明 CNPG 做什么、怎么做。

Operator   cloudnative-pg 1.30.0  +  plugin-barman-cloud v0.14.0
集群       labhub-db-prod — PostgreSQL 18.4,2 个实例,5Gi 卷(NFS)
当前状态   primary = labhub-db-prod-1,standby = labhub-db-prod-2,timeline 3

设计上与众不同的两点

不用 StatefulSet

在 Kubernetes 里有状态的东西用 StatefulSet 跑,这是常识。CNPG 不这么做。每一个 Pod 都由 Operator 直接创建和删除。

原因是数据库实例彼此并不相同。StatefulSet 假设"从 0 号开始按顺序、用同一个模板"。而数据库需要做这样的决定:"只把 2 号实例用新卷重建,让它从头重新复制"、"最后才动主库"。要做这些决定,就必须能单独处理每个 Pod。

Pod 的 PID 1 不是 postgres

CNPG 的 Pod 里 1 号进程是 实例管理器。它把 postgres 作为子进程启动,回应就绪和存活探针,接收提升命令,把 WAL 推送到归档,配置变化时重新加载。

这个结构有两个好处。Operator 死了数据库也继续运行(管理器在 Pod 里面)。而且"这个实例是否真的活着"由 Kubernetes 根据 PostgreSQL 的实际响应 判断,而不是 TCP 端口。

按角色拆解

集群就是一个 CR

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata: { name: labhub-db-prod }
spec:
  instances: 2
  imageName: ghcr.io/cloudnative-pg/postgresql:18.4-system-trixie
  storage: { size: 5Gi, storageClass: nfs-synology }
  primaryUpdateStrategy: unsupervised
  primaryUpdateMethod: restart
  postgresql:
    parameters: { wal_level: logical, archive_timeout: 5min }
  replicationSlots: { highAvailability: { enabled: true } }
  plugins:
    - name: barman-cloud.cloudnative-pg.io
      isWALArchiver: true

这一个对象生成下面要讲的全部——Pod、Service、Secret、证书、复制、备份。改配置的方法也只有一个:改这个 CR,Operator 计算差异,只动需要动的部分。

复制与角色

一个实例是主库,其余是通过流复制跟随的备库。这是在主库上看到的当前状态:

application_name | state     | sync_state
labhub-db-prod-2 | streaming | async

async 意味着主库确认提交时不等待备库的确认。快,但主库死掉那一刻还没到达备库的事务就会丢失。这个集群的延迟实际为零——那是因为今天负载小,不是有什么保证。

复制槽 HA 已开启,备库短暂断开时主库不会删除备库还没收到的 WAL。没有它,网络断几分钟就得从头重建备库。

三个 Service

应用不用 Pod 名连接,而是连到 Operator 创建的三个 Service 之一。

Service指向用途
labhub-db-prod-rw当前主库写入,应用连这里
labhub-db-prod-ro仅备库读分流
labhub-db-prod-r任意实例读(含主库)

发生故障转移时,Operator 只改 -rw 的端点。应用看到连接断开,重连即可。应用配置里没有写主库地址,这正是迁移和更换数据库的自由的起点。

故障转移、切换与 timeline

主库死掉时,Operator 选出最领先的备库并提升它。此时 PostgreSQL 把 timeline 加一——这是历史分叉的标记。这个集群是 timelineID: 3,至今主库换过两次的历史就留在这个数字里。

有计划的更换是切换(switchover)。kubectl cnpg promote labhub-db-prod labhub-db-prod-2 指定备库,Operator 会处理旧主库、立起新主库,再把旧的变成备库。没有数据丢失,只换角色。

滚动更新

镜像或需要重启的配置变化时,Operator 先替换备库,最后才动主库。最后这一步怎么做由两个值决定:

小版本升级(18.4 → 18.5)只需改 imageName,按这个流程走。

配置写在 CR 里,reload 还是 restart 由 Operator 判断

写在 postgresql.parameters 里的值由 Operator 写入 postgresql.conf,并判断该项是 reload 即可还是需要 restart,然后相应处理。这个集群只动了两个:

备份 — 两种合在一起才能恢复

这是误解最多的部分。"做了备份"只说了一半。

WAL 归档。 PostgreSQL 把所有变更先写进 WAL(预写日志)。每当一个 WAL 段写满或过了 archive_timeout(5 分钟),CNPG 就把该段推送到对象存储。也就是说,最近 5 分钟内的变更可能还不在归档里——这就是这个集群的 RPO。

基础备份。 整个数据目录的副本。这个集群由 ScheduledBackup 每天 03:30 UTC 做一次,最近三天都是 completed

恢复是把两者合起来。 展开基础备份,再把之后的 WAL 重放到想要的时刻。所以能回到"昨天下午三点"(PITR),能回到的最早时刻记录在状态的 firstRecoverabilityPoint 里。

这个集群的备份不是用 CNPG 内置的 barmanObjectStore,而是用 CNPG-I 插件plugin-barman-cloud)。因此 spec.backup 是空的,取而代之的是 plugins[].isWALArchiver: true。把备份逻辑从 Operator 本体剥离,是 CNPG 近期的方向。

其他功能

真实集群里能看到的事实与陷阱

从这里开始不是文档,而是经历。

状态字段停住了。 备份每天都在做,status.lastSuccessfulBackup 却停在 8 月 23 日。因为迁到插件方式后,Cluster 的状态字段和 cnpg_collector_* 指标不再更新。拿这个值做告警会成为 误报。判断应该看 Backup 对象的 phase 和 barman_cloud_* 指标。

第一次备份可能无法恢复。 刚开启归档后做的备份显示 completed,但它的起始 WAL 位置(beginWal)不在归档里,恢复曾经失败过。先用 pg_switch_wal() 切换一次 WAL 再重新备份,并且 一定要真的恢复一次。备份只能用"恢复回来了"确认,不能用"做了"确认。

async 复制加两个实例。 如上所述,async 在故障转移时可能丢失最后几个事务。开同步复制能解决,但只有两个实例时会产生相反的风险——备库一死 写入就停。先扩到三个再开,这才是顺序。

NFS 上的数据库。 卷是 NFS(5Gi)。现在数据只有 7.8MB,容量不是问题,但 WAL 的 fsync 比本地磁盘慢,CNPG 文档也推荐本地存储。负载增大时这是第一个要迁的。

指标是关的。 enablePodMonitor 关闭,Prometheus 里没有数据库指标。要看连接数、复制延迟、WAL 归档失败,得打开。

一句话

CNPG 的角色是让控制器——而不是人——随时能回答 "现在主库是谁、它前面的地址是什么、能不能回到昨天"。这个集群三者都具备。剩下的功课是定期做恢复演练、三个实例加同步复制,以及打开指标。

登录后即可点赞

评论

还没有评论。

登录后即可发表评论