Skip to content
Published on

CloudNativePG 在 Kubernetes 上跑起 Postgres 再杀掉它 — 实测故障转移 23 秒

分享
Authors

引言 — 把 Postgres 交给 Operator 意味着什么

在 Kubernetes 上运行有状态(stateful)数据库,长期以来被视为一种禁忌。CloudNativePG(CNPG) 就是一款正面打破这一禁忌的 Operator — 只要用一个 CRD 声明 Postgres 的高可用、故障转移、备份和滚动升级,Operator 就会替你完成主库选举和副本管理。这篇文章不是介绍,而是实测:在真实的 8 节点集群上装好 CNPG,启动 3 实例的 Postgres,真的把主库杀掉,量一量故障转移到底要花几秒。它与我亲手写 Operator 的Rust GPU Operator 篇成对,讲的是"使用一款做得好的 Operator"这一侧的故事。

第 1 部 — 安装:一份清单

CNPG 的安装很简单。apply 一份发布清单即可完成。

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

这一行会一次性装好多个 CRD(clusterspoolersscheduledbackupspublicationssubscriptions 等)、cnpg-system 命名空间里的控制器 Deployment、RBAC 和 Webhook。控制器启动大约花了 20 秒:

$ kubectl -n cnpg-system rollout status deploy/cnpg-controller-manager
deployment "cnpg-controller-manager" successfully rolled out

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
cnpg-controller-manager   1/1     1            1           16s

第 2 部 — 创建 3 实例集群

Postgres 集群通过 Cluster CR 来声明。主库 1 + 副本 2,也就是 3 个实例。

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg-test
  namespace: cnpg-test
spec:
  instances: 3
  storage:
    size: 1Gi
    storageClass: nfs-client     # 家庭实验室的 NFS — 陷阱见第 5 部
  bootstrap:
    initdb:
      database: appdb
      owner: appuser

apply 之后,Operator 就开始引导启动。我实时观察了状态迁移:

Setting up primary
→ Waiting for the instances to become active
→ ready=1  Creating a new replica       ← 主库起来后开始复制副本
→ ready=2  Creating a new replica
→ ready=3  Cluster in healthy state      ← 约 2 分钟后 3/3 正常

三个实例被自动分散到了不同的节点(cubi02、cubi03、cubi04)上 — Operator 通过 anti-affinity 避免它们挤在同一个节点。CNPG 还会创建三个用于连接的服务:

服务           角色
────────────  ─────────────────────────────
pg-test-rw     读写 → 始终路由到当前主库
pg-test-ro     只读 → 负载均衡到各副本
pg-test-r      任意实例(读取)

-rw 服务是关键 — 应用只要盯着这一个名字,即便故障转移换了主库,也会自动连接到新的主库

第 3 部 — 复制确认

往主库插入 1000 行,看三个实例是否完全一致。

$ 向主库(pg-test-1)INSERT 1000 行
INSERT 0 1000

$ 各实例的行数
  pg-test-1 (primary): 1000 rows
  pg-test-2 (replica): 1000 rows      ← 已复制
  pg-test-3 (replica): 1000 rows      ← 已复制

流复制立刻把三个节点同步了。接下来才是真正的实验。

第 4 部 — 杀掉主库:故障转移 23.1 秒

我最好奇的问题 — 主库突然消失,几秒能恢复?用 --grace-period=0 --force 让主库 Pod 瞬间死亡,然后每隔 0.5 秒轮询一次,直到新主库出现,同时计时。

=== FAILOVER TEST: killing primary pg-test-1 ===
deleted at t0; polling for new primary...
=== NEW PRIMARY: pg-test-2 (was pg-test-1) ===
failover time: 23.1 s

  rows after failover: 1000        ← 数据零丢失

23.1 秒内 pg-test-2 晋升为新主库1000 行原封不动地存活着。而死掉的 pg-test-1 并不会被丢弃 — Operator 会自动把它接回来,重新编入为副本

=== 自愈后的最终角色 ===
  pg-test-1: replica     ← 死而复生,降级为副本回归
  pg-test-2: primary     ← 晋升后的新主库
  pg-test-3: replica

$ 向新主库追加写入 → 正常,共 1500 行

新主库立刻开始接受写入(1000→1500 行),集群又回到了 3/3 healthy。**零次人工介入。**这就是 Operator 的价值。

第 5 部 — 诚实的数字与陷阱

秉持"博客只写经过验证的内容"这一原则,这次实验的局限也如实保留。

  • 23 秒算快吗? 视情况而定。CNPG 的故障转移时间,是主库死亡检测(健康检查周期)、副本晋升,以及 -rw 服务端点更新三者之和。生产环境里更常见的不是删除 Pod,而是节点故障,那种情况下还要加上节点检测时间(node-monitor-grace-period 等),可能会更长。反过来,做了调优则会更短。"23 秒"是这套家庭实验室、这套配置下的实测值,并不是普适常量。
  • NFS 存储的陷阱。 我用的是 nfs-client(NFS provisioner)存储,但把 Postgres 放到 NFS 上在生产环境里并不推荐 — 原因在于 fsync 保证和文件锁问题。在家庭实验室的测试里跑得不错,但真正上线的话应该用本地 SSD 或块存储(如 Ceph RBD)。
  • 同步 vs 异步复制。 这次测试用的是默认(异步)复制。异步复制在理论上,可能会在主库死亡前的一瞬丢失极少量尚未复制的事务。如果需要零丢失,就得打开 CNPG 的同步复制(minSyncReplicas),代价是写入延迟会增加。

结语

CNPG 用实测反驳了"在 Kubernetes 上跑数据库很危险"这一成见 — 一份清单就能拉起 3 节点的 HA Postgres,即便让主库瞬间死亡也能在 23 秒内自我恢复,死掉的节点则回归为副本。当然,存储、复制模式、故障转移调优这些真正的功课还在,但那已不是"能不能把数据库交给 Operator"的问题,而是"怎样才能交得好"的问题。下次我打算用同样的方式,把备份(ScheduledBackup)和时间点恢复(PITR)也杀一杀来验证。

参考资料