Skip to content

13 · PostgreSQL 高可用部署(Kubernetes + CloudNativePG)

目标形态:K8s + CloudNativePG(CNPG)Operator,RHEL/Rocky 节点,RPO=0,S3 兼容对象存储做备份/PITR。 本文是可照着执行的 runbook,按 0→12 顺序推进,每步都有验证点。先在预发环境跑通一遍,再上产线。

0. 架构与选型

Patroni 概念如何映射到 CNPG

Patroni 世界CNPG(K8s)世界说明
Patroni(每节点大脑)CNPG Operator + 每 Pod 的 instance manager选主、健康检查、启停 PG
etcd / DCSKubernetes API(Lease/Cluster 状态)免装 etcd,共识交给 K8s
HAProxy + VIPK8s Service(-rw / -ro / -r)自动路由,切主后 Service 自动指向新主
选主(比 LSN)Operator 挑 WAL 最新的 replica 提升逻辑一致
synchronous_mode_strictpostgresql.synchronous.dataDurability: requiredRPO=0 的最后一道锁
PgBouncer(手装)Pooler CRD(内置 PgBouncer)声明式,transaction 池化
pgBackRest/WAL-G内置 Barman Cloud → S3WAL 归档 + 基础备份 + PITR

参考拓扑

                    应用(读写分离)
        写: cluster-rw-pooler:5432   读: cluster-ro-pooler:5432

              ┌───────────┴───────────┐
              ▼                        ▼
   Pooler(PgBouncer, rw)      Pooler(PgBouncer, ro)   ← transaction 池化,各自多副本
        │  transaction              │  分摊到所有 replica
        ▼                           ▼
   Service cluster-rw          Service cluster-ro
        │                           │
   ┌────┴─────────────────────────────────┐
   │  Cluster(CNPG)3 实例,反亲和跨节点/跨区 │
   │  ┌─────────┐  ┌─────────┐  ┌─────────┐ │
   │  │instance1│  │instance2│  │instance3│ │
   │  │ primary │  │ replica │  │ replica │ │  同步复制 ANY 1 (RPO=0, strict)
   │  └─────────┘  └─────────┘  └─────────┘ │
   └───────────────────┬───────────────────┘
                       │ WAL 归档 + 基础备份

              S3 兼容对象存储(PITR)

          Prometheus + Grafana(PodMonitor 抓指标)

为什么至少 3 实例:同步复制要求 ANY 1 个 replica 确认。3 实例时挂 1 个仍有 1 个同步 replica,写入不阻塞;dataDurability: required 保证无同步 replica 时宁可阻塞写也不丢数据。2 实例做不到"既 RPO=0 又不动辄阻塞"。

本系统的 PostgreSQL 角色

与 CBDC 系统的关系

本系统有三类 PG 用途——FSC 节点投影、业务服务数据和账本面 committer,需求不同:

用途数据库读写特征HA 优先级
Wallet Node / Endorser / Auditor / Issuer 的 FSC 节点开发配置为 cbdc_institution / cbdc_endorser1 / cbdc_auditor / cbdc_issuer每节点独立逻辑库,写量与交易量相关高(角色不可用会中断相应链路)
BizHubcbdc_biz机构、钱包元数据、结算、规则、节点注册与链上索引
Centralcbdc_central用户、权限、会话、审批、通知与系统审计
committer state DBsc_db(或 yugabyte)全链交易的最终状态,写量 = 系统 TPS最高(挂了全链停)

本指南可作为上述 PostgreSQL 的部署参考,但具体 HA 产品选择与容量必须在目标环境验证。Fabric-X state DB 也提供 YugabyteDB 拓扑模板;是否采用应以实际压测和运维能力决定。

1. 前置条件与容量规划

1.1 集群前提

  • Kubernetes ≥ 1.27,节点操作系统 Rocky/RHEL
  • 已装 Prometheus Operator(用 PodMonitor 抓指标)——没有可后补
  • 一个低延迟本地盘 StorageClass(NVMe/SSD 优先),volumeBindingMode: WaitForFirstConsumer
  • 专用 DB 节点池:给数据库节点打 label + taint,避免和普通业务 Pod 抢资源
    bash
    kubectl label  node <db-node-1> node-role=database
    kubectl taint  node <db-node-1> dedicated=database:NoSchedule
    # 三台 DB 节点都做;最好分属不同物理机/可用区
  • S3 兼容对象存储:准备好 bucket、endpoint、access key / secret key

1.2 容量规划

连接数是产线最容易踩的坑。原则:PG 层连接数压低,并发靠 PgBouncer 顶。

参数建议起点说明
PG max_connections300~500别开几千,进程模型扛不住
PgBouncer pool_modetransaction复用最好(注意会话级特性限制,见 §5)
PgBouncer max_client_conn10000+对应用暴露的"廉价连接"
PgBouncer default_pool_size40~100 / 池对 PG 的真实连接数
shared_buffers内存 25%
effective_cache_size内存 65~75%
work_mem32~128MB按并发和排序需求调,过大易 OOM
maintenance_work_mem1~2GB
CPU/内存 requests=limits按机型给足避免被限流/被驱逐

与本系统连接预算的关系

开发控制器可把多套逻辑库放在同一 PostgreSQL 实例中,但各服务的连接池配置会叠加。生产预算应从实际加载的 dev/config/token/*dev/config/bizhubdev/config/central 或对应独立配置逐项汇总,再预留迁移、监控和运维连接。不要沿用旧 compose 文档中的固定连接数。

读扩展:replica 数可以 >2(如 1 主 4 备),-ro Pooler 把读流量分摊到所有 replica。写永远只走 primary。

2. 安装 CloudNativePG Operator

bash
# 锁定具体版本,别用 latest
kubectl apply --server-side -f \
  https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.26/releases/cnpg-1.26.0.yaml

# 验证
kubectl -n cnpg-system get deploy cnpg-controller-manager
kubectl -n cnpg-system rollout status deploy/cnpg-controller-manager

(可选)kubectl 插件:

bash
kubectl krew install cnpg
kubectl cnpg version

验证点:cnpg-controller-manager Running,kubectl api-resources | grep postgresql.cnpg.io 能看到 Cluster / Pooler / ScheduledBackup

3. 命名空间与密钥

bash
kubectl create namespace pg-prod

3.1 应用数据库账号密钥

bash
kubectl -n pg-prod create secret generic pg-app-user \
  --from-literal=username=app \
  --from-literal=password='<强随机密码>'

3.2 S3 备份凭证

bash
kubectl -n pg-prod create secret generic s3-backup-creds \
  --from-literal=ACCESS_KEY_ID='<S3 AK>' \
  --from-literal=ACCESS_SECRET_KEY='<S3 SK>'

生产建议用 External Secrets Operator / Vault 注入,别把密钥写进 Git。

4. 部署 Cluster(核心清单)

cluster.yaml:

yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg-cluster
  namespace: pg-prod
spec:
  instances: 3
  imageName: ghcr.io/cloudnative-pg/postgresql:18.0

  # ---- RPO=0 同步复制 ----
  postgresql:
    synchronous:
      method: any
      number: 1
      dataDurability: required   # 无同步 replica 时宁可阻塞写,也不降级为异步
    parameters:
      max_connections: "500"
      shared_buffers: "64GB"              # 内存 25%
      effective_cache_size: "160GB"       # 容器内取保守值,见下方警告
      work_mem: "128MB"
      maintenance_work_mem: "4GB"
      autovacuum_work_mem: "2GB"
      wal_buffers: "128MB"
      huge_pages: "try"
      # 并行(64 核)
      max_worker_processes: "64"
      max_parallel_workers: "64"
      max_parallel_workers_per_gather: "8"
      max_parallel_maintenance_workers: "8"
      # WAL / checkpoint
      wal_compression: "on"
      max_wal_size: "32GB"
      min_wal_size: "4GB"
      checkpoint_completion_target: "0.9"
      max_slot_wal_keep_size: "200GB"
      max_wal_senders: "20"
      max_replication_slots: "20"
      # NVMe I/O
      random_page_cost: "1.1"
      effective_io_concurrency: "256"
      maintenance_io_concurrency: "256"
      # autovacuum
      autovacuum_max_workers: "8"
      autovacuum_vacuum_cost_limit: "3000"
      autovacuum_naptime: "15s"
      hot_standby_feedback: "on"
      # 慢查询
      log_min_duration_statement: "1000"
      pg_stat_statements.track: "all"
    pg_hba:
      - host all all 10.0.0.0/8 scram-sha-256
    shared_preload_libraries:
      - pg_stat_statements

  # ---- 存储:数据盘与 WAL 盘分离 ----
  storage:
    size: 8Ti
    storageClass: fast-nvme
  walStorage:
    size: 500Gi
    storageClass: fast-nvme

  # ---- 资源(requests=limits,独占节点)----
  resources:
    requests: { cpu: "56", memory: "224Gi" }
    limits:   { cpu: "56", memory: "224Gi" }

  # ---- 反亲和 + DB 节点池 ----
  affinity:
    enablePodAntiAffinity: true
    topologyKey: kubernetes.io/hostname
    podAntiAffinityType: required
    nodeSelector:
      node-role: database
    tolerations:
      - key: dedicated
        operator: Equal
        value: database
        effect: NoSchedule

  # ---- bootstrap ----
  bootstrap:
    initdb:
      database: appdb
      owner: app
      secret:
        name: pg-app-user
      encoding: UTF8
      localeCType: C
      localeCollate: C

  # ---- 备份到 S3 ----
  backup:
    retentionPolicy: "30d"
    barmanObjectStore:
      destinationPath: s3://your-bucket/pg-cluster
      endpointURL: https://s3.your-region.amazonaws.com
      s3Credentials:
        accessKeyId:
          name: s3-backup-creds
          key: ACCESS_KEY_ID
        secretAccessKey:
          name: s3-backup-creds
          key: ACCESS_SECRET_KEY
      wal:
        compression: gzip
        maxParallel: 8
      data:
        compression: gzip
        jobs: 4

  # ---- 监控 ----
  monitoring:
    enablePodMonitor: true
bash
kubectl apply -f cluster.yaml
kubectl -n pg-prod get cluster pg-cluster -w
kubectl cnpg status pg-cluster -n pg-prod

验证点:

  • kubectl cnpg status 显示 1 primary + 2 replica,replica 状态为 sync
  • 三个 Service 自动生成:pg-cluster-rw / pg-cluster-ro / pg-cluster-r
  • Continuous Archiving 状态 OK

K8s 特有坑 · cgroup 页缓存

容器里 PG 读文件产生的内核页缓存会计入 cgroup 内存用量(受 limits.memory 约束)。所以 224Gi 上限里,shared_buffers 64G 之外只剩 ~160G 给页缓存 + work_mem + 各进程栈,effective_cache_size160GB 而非乐观的 180GB。上线后按 container_memory_working_set_bytes 实测再微调。

版本注意:CNPG < 1.24 没有 postgresql.synchronous 段,改用 minSyncReplicas: 1 / maxSyncReplicas: 1dataDurability 需 1.25+。

5. 连接池(Pooler / PgBouncer)

给读写各建一个 Pooler。poolers.yaml:

yaml
apiVersion: postgresql.cnpg.io/v1
kind: Pooler
metadata:
  name: pg-cluster-rw-pooler
  namespace: pg-prod
spec:
  cluster:
    name: pg-cluster
  instances: 3
  type: rw
  pgbouncer:
    poolMode: transaction
    parameters:
      max_client_conn: "10000"
      default_pool_size: "50"
      max_db_connections: "300"
      reserve_pool_size: "10"
      reserve_pool_timeout: "3"
---
apiVersion: postgresql.cnpg.io/v1
kind: Pooler
metadata:
  name: pg-cluster-ro-pooler
  namespace: pg-prod
spec:
  cluster:
    name: pg-cluster
  instances: 3
  type: ro
  pgbouncer:
    poolMode: transaction
    parameters:
      max_client_conn: "10000"
      default_pool_size: "80"
      max_db_connections: "400"
bash
kubectl apply -f poolers.yaml
kubectl -n pg-prod get pooler

应用连接地址:

  • :pg-cluster-rw-pooler.pg-prod.svc:5432
  • :pg-cluster-ro-pooler.pg-prod.svc:5432

transaction 模式的坑

不能依赖会话级特性:SET(会话变量)、LISTEN/NOTIFY、advisory lock、WITH HOLD cursor。prepared statements 需 PgBouncer 1.21+ 并开 max_prepared_statements迁移前用预发压一轮。

6. 定时备份

scheduledbackup.yaml:

yaml
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
  name: pg-cluster-daily
  namespace: pg-prod
spec:
  schedule: "0 0 2 * * *"
  backupOwnerReference: self
  cluster:
    name: pg-cluster
  method: barmanObjectStore
bash
kubectl apply -f scheduledbackup.yaml
kubectl cnpg backup pg-cluster -n pg-prod      # 立刻手动跑一次验证
kubectl -n pg-prod get backup

HA ≠ 备份。CNPG 只防节点挂,防不了误删表/逻辑损坏。备份 + PITR 是独立的第二道防线。

7. 应用接入与读写分离

  • 写 → pg-cluster-rw-pooler(始终跟随 primary,切主自动跟随)
  • 读 → pg-cluster-ro-pooler(分摊到所有 replica)
  • 连接串统一走 Service DNS,不要硬编码 Pod IP
  • 读一致性:replica 有复制延迟。写后立即读走 rw;能容忍毫秒级延迟的走 ro

TLS:CNPG 默认签发服务端证书,生产建议应用侧 sslmode=verify-full 并挂载 CA。

8. 监控与告警

CNPG 已 enablePodMonitor: true,导入官方 Grafana 面板。必配告警:

  • 同步 replica 数 < 期望(RPO=0 面临风险 / 写可能阻塞)
  • 复制延迟(replay lag)过高
  • WAL 归档失败 / 落后
  • 备份连续失败
  • 磁盘使用率 > 80%(数据盘 & WAL 盘分别)
  • Pooler cl_waiting 升高、sv_active 打满
  • Failover / 主从切换事件

9. 上线前必做:故障切换 & 恢复演练

没演练过的 HA 等于没有 HA。

9.1 计划内切换(switchover)

bash
kubectl cnpg promote pg-cluster <target-replica-pod> -n pg-prod
kubectl cnpg status pg-cluster -n pg-prod

9.2 计划外故障(failover)

bash
kubectl -n pg-prod delete pod <primary-pod> --grace-period=0 --force
kubectl cnpg status pg-cluster -n pg-prod -w

记录 RTO(CNPG 通常十几~几十秒)。

9.3 RPO=0 验证

  1. 持续写入并记录已 commit 的最大 id
  2. 制造 failover
  3. 切换后查最大 id:所有返回成功的事务都必须在

9.4 同步 replica 全丢验证

两个 replica 都停掉 → primary 阻塞写入(不是偷偷继续)——这正是 dataDurability: required 的预期。恢复 replica 后写入自动放行。

9.5 PITR 恢复演练

yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg-restore-test
  namespace: pg-prod
spec:
  instances: 1
  imageName: ghcr.io/cloudnative-pg/postgresql:18.0
  storage: { size: 8Ti, storageClass: fast-nvme }
  bootstrap:
    recovery:
      source: pg-cluster
      recoveryTarget:
        targetTime: "2026-07-01 03:00:00+00"
  externalClusters:
    - name: pg-cluster
      barmanObjectStore:
        destinationPath: s3://your-bucket/pg-cluster
        endpointURL: https://s3.your-region.amazonaws.com
        s3Credentials:
          accessKeyId:     { name: s3-backup-creds, key: ACCESS_KEY_ID }
          secretAccessKey: { name: s3-backup-creds, key: ACCESS_SECRET_KEY }

10. RHEL / Rocky / OpenShift 特有事项

  • SELinux:保持 enforcing;CNPG 容器非 root 运行,通常无需关
  • OpenShift:兼容 restricted-v2 SCC,一般无需 anyuid
  • 本地盘:大并发强烈建议本地 NVMe(local-path / TopoLVM),网络盘 IOPS 往往是瓶颈
  • hugepages(shared_buffers=64GB 收益明显):
    bash
    # 每台 DB 节点
    sysctl -w vm.nr_hugepages=34000    # 64GB ÷ 2Mi ≈ 32768 页 + 余量
    Pod resources 里加 hugepages 后,memory 相应下调(如 224→160Gi),二者独立预算。

11. Day-2 运维

  • 小版本升级:改 imageName,CNPG 滚动升级(先 replica 后主),几乎无感
  • 主版本升级:走 CNPG major upgrade 或 logical 迁移,单独规划
  • 扩容读副本:改 instances(如 3→5),Operator 自动加 replica
  • 扩磁盘:改 storage.size(不可缩小)
  • 参数变更:改 postgresql.parameters 后自动 reload;需重启的参数滚动重启
  • 凭证轮换:更新 Secret,CNPG 侦测并生效

12. 上产线前检查清单

数据 / RPO=0:

  • [ ] kubectl cnpg status 三实例,replica 状态为 sync
  • [ ] synchronous.number ≥ 1dataDurability: required
  • [ ] §9.3 RPO=0 演练通过
  • [ ] §9.4 同步 replica 全丢时 primary 正确阻塞写

可用性:

  • [ ] §9.1 switchover、§9.2 failover 演练通过,RTO 记录在案
  • [ ] 三实例反亲和,分布在不同物理节点/可用区
  • [ ] rw / ro Pooler 各多副本,无单点

备份 / 恢复:

  • [ ] WAL 持续归档到 S3,状态 OK
  • [ ] 每日基础备份成功,retention 生效
  • [ ] §9.5 PITR 恢复演练通过

连接 / 性能:

  • [ ] 应用连 Pooler(非直连 PG),读写分离正确
  • [ ] transaction 池化下应用无会话级特性依赖(或已适配)
  • [ ] 压测通过,连接数/池大小/work_mem 无 OOM、无排队打满

可观测:

  • [ ] Grafana 面板就绪,§8 全部告警配置并测试触达

安全:

  • [ ] pg_hba 网段收紧,scram-sha-256
  • [ ] 应用侧 TLS(建议 verify-full)
  • [ ] 密钥用 Vault/External Secrets,不进 Git