饮墨

子安饮墨馀三斗,留与卿儿作赋来

CloudNativePG 实战:4 步在 Kubernetes 上部署生产级 PostgreSQL,告别 StatefulSet 手搓 HA

4 views

痛点

在 Kubernetes 上跑 PostgreSQL,最常见的方案是手搓 StatefulSet + PVC + 自定义脚本。看起来能跑,但真到生产环境就暴露三个致命问题:

  1. 故障转移靠人工:Primary 挂了,需要手动 promote Replica、改 Service endpoint、确认数据一致性,停机窗口轻松 10-30 分钟
  2. 备份是定时炸弹:CronJob + pg_dump 管备份,恢复时才发现 dump 文件损坏或缺了 WAL,PITR(时间点恢复)更是奢望
  3. Day-2 运维地狱:滚动升级 PostgreSQL 版本要手动 drain、重建 Pod,扩缩副本要改 StatefulSet + 重新配置流复制,每次操作都是一场冒险

社区里有多个 PostgreSQL Operator(Zalando、CrunchyData、Percona),但 CloudNativePG(CNPG) 是目前唯一由 CNCF 孵化、不依赖外部工具链(无需 Patroni/etcd)的 Kubernetes 原生方案。它直接利用 Kubernetes API 做共识和故障转移,架构更简洁、运维更轻量。

方案

CloudNativePG 的核心设计哲学:PostgreSQL 集群本身就是一个 Kubernetes 自定义资源(CR)。Operator 负责:

  • 自动初始化 Primary + Streaming Replica 拓扑
  • 基于 Kubernetes lease 的自动故障检测与 failover(秒级)
  • 持续归档 WAL 到对象存储(S3/GCS/Azure Blob),支持 PITR
  • 滚动更新、在线扩缩容、主从切换(switchover)零停机
  • 原生集成 Prometheus 指标导出,无需额外 exporter Pod

实操步骤

第 1 步:安装 Operator

# 使用 Helm 安装(推荐生产环境)
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo update

helm install cnpg cnpg/cloudnative-pg \
  --namespace cnpg-system \
  --create-namespace \
  --set monitoring.podMonitorEnabled=true

# 验证 Operator 就绪
kubectl get pods -n cnpg-system
# NAME                    READY   STATUS    RESTARTS   AGE
# cnpg-controller-xxx     1/1     Running   0          30s

第 2 步:部署 PostgreSQL 集群

创建一个 3 节点(1 Primary + 2 Replica)集群,配置 S3 备份:

# pg-cluster.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: app-db
  namespace: production
spec:
  instances: 3
  imageName: ghcr.io/cloudnative-pg/postgresql:16.4

  postgresql:
    parameters:
      max_connections: "200"
      shared_buffers: "512MB"
      effective_cache_size: "1536MB"
      work_mem: "8MB"
      maintenance_work_mem: "128MB"
      wal_level: "logical"           # 如需逻辑复制
      log_min_duration_statement: "500"

  storage:
    size: 50Gi
    storageClass: gp3-enc            # 按实际 StorageClass 修改

  resources:
    requests:
      memory: "2Gi"
      cpu: "1"
    limits:
      memory: "4Gi"
      cpu: "2"

  backup:
    barmanObjectStore:
      destinationPath: s3://my-pg-backups/app-db/
      s3Credentials:
        accessKeyId:
          name: s3-creds
          key: ACCESS_KEY_ID
        secretAccessKey:
          name: s3-creds
          key: SECRET_ACCESS_KEY
      wal:
        compression: gzip
    retentionPolicy: "14d"

  monitoring:
    enablePodMonitor: true
# 创建 S3 凭证 Secret
kubectl create secret generic s3-creds -n production \
  --from-literal=ACCESS_KEY_ID=<your-key> \
  --from-literal=SECRET_ACCESS_KEY=<your-secret>

# 部署集群
kubectl apply -f pg-cluster.yaml

# 观察集群启动(约 60-90 秒)
kubectl get cluster app-db -n production -w
# NAME     AGE   INSTANCES   READY   STATUS
# app-db   90s   3           3       Cluster in healthy state

第 3 步:配置定时备份与 PITR

# scheduled-backup.yaml
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
  name: app-db-daily
  namespace: production
spec:
  schedule: "0 2 * * *"        # 每天凌晨 2 点
  backupOwnerReference: self
  cluster:
    name: app-db
  method: barmanObjectStore
kubectl apply -f scheduled-backup.yaml

# 手动触发一次备份验证
kubectl cnpg backup app-db -n production

# 查看备份状态
kubectl get backups -n production
# NAME               AGE   CLUSTER   METHOD              PHASE       
# app-db-20260907    30s   app-db    barmanObjectStore    completed

PITR 恢复示例——假设需要恢复到某个时间点:

# restore-cluster.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: app-db-restored
  namespace: production
spec:
  instances: 3
  imageName: ghcr.io/cloudnative-pg/postgresql:16.4

  bootstrap:
    recovery:
      source: app-db-backup
      recoveryTarget:
        targetTime: "2026-09-06T18:30:00Z"   # 恢复到此时间点

  externalClusters:
    - name: app-db-backup
      barmanObjectStore:
        destinationPath: s3://my-pg-backups/app-db/
        s3Credentials:
          accessKeyId:
            name: s3-creds
            key: ACCESS_KEY_ID
          secretAccessKey:
            name: s3-creds
            key: SECRET_ACCESS_KEY

  storage:
    size: 50Gi
    storageClass: gp3-enc

第 4 步:连接应用与日常运维

CNPG 自动创建三个 Service,应用直接用 Kubernetes DNS 连接:

# 读写连接(指向 Primary)
app-db-rw.production.svc:5432

# 只读连接(负载均衡到 Replica)
app-db-ro.production.svc:5432

# 读连接(优先 Replica,Replica 不可用时回退 Primary)
app-db-r.production.svc:5432

常用运维命令:

# 查看集群状态
kubectl cnpg status app-db -n production

# 手动主从切换(零停机 switchover)
kubectl cnpg promote app-db-2 -n production

# 在线扩容副本数
kubectl patch cluster app-db -n production \
  --type merge -p '{"spec":{"instances":4}}'

# 连接 psql 调试
kubectl cnpg psql app-db -n production -- -c "SELECT pg_is_in_recovery();"

避坑

坑 1:StorageClass 必须支持 volume expansion

CNPG 的 PVC 创建后如果磁盘不够,需要在线扩容。如果 StorageClass 没有设置 allowVolumeExpansion: true,扩容会直接失败。部署前先确认:

kubectl get sc <your-sc> -o jsonpath='{.allowVolumeExpansion}'
# 必须返回 true

坑 2:Pod Disruption Budget 与节点维护冲突

CNPG 自动创建 PDB(minAvailable=1),集群维护 kubectl drain 时如果只剩一个健康实例,drain 会卡住。正确做法是先确保所有 Replica 健康,再逐节点 drain:

# 先检查集群健康
kubectl cnpg status app-db -n production | grep -E "Instances|Ready"
# 确保 Ready = Instances 后再 drain 节点

坑 3:WAL 归档中断导致备份链断裂

S3 凭证过期、网络抖动都可能导致 WAL 归档失败。一旦归档中断超过 wal_keep_size,备份链断裂,PITR 窗口缩小甚至不可用。必须配置告警

# Prometheus 告警规则
- alert: CNPGWalArchivingFailing
  expr: cnpg_pg_stat_archiver_failed_count > 0
  for: 5m
  labels:
    severity: critical
  annotations:
    summary: "PostgreSQL WAL archiving is failing on {{ $labels.cluster }}"

总结

维度 StatefulSet 手搓 CloudNativePG
故障转移 手动,10-30 分钟 自动,秒级
备份/PITR CronJob + pg_dump,无 PITR 原生对象存储归档 + 精确 PITR
滚动升级 手动 drain + 重建 声明式,自动滚动
监控集成 需额外部署 exporter 内置 Prometheus 指标
运维复杂度 高(脚本、配置散落) 低(一个 CR 描述全部)

CNPG 是目前在 Kubernetes 上跑 PostgreSQL 最省心的方案。如果你的团队已经在 K8s 上跑应用但数据库还在集群外面"特殊照顾",CNPG 值得认真评估——把数据库也纳入 GitOps 工作流,备份、HA、监控全部声明式管理,运维负担直接减半。