主题
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 / DCS | Kubernetes API(Lease/Cluster 状态) | 免装 etcd,共识交给 K8s |
| HAProxy + VIP | K8s Service(-rw / -ro / -r) | 自动路由,切主后 Service 自动指向新主 |
| 选主(比 LSN) | Operator 挑 WAL 最新的 replica 提升 | 逻辑一致 |
synchronous_mode_strict | postgresql.synchronous.dataDurability: required | RPO=0 的最后一道锁 |
| PgBouncer(手装) | Pooler CRD(内置 PgBouncer) | 声明式,transaction 池化 |
| pgBackRest/WAL-G | 内置 Barman Cloud → S3 | WAL 归档 + 基础备份 + 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 | 每节点独立逻辑库,写量与交易量相关 | 高(角色不可用会中断相应链路) |
| BizHub | cbdc_biz | 机构、钱包元数据、结算、规则、节点注册与链上索引 | 高 |
| Central | cbdc_central | 用户、权限、会话、审批、通知与系统审计 | 中 |
| committer state DB | sc_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_connections | 300~500 | 别开几千,进程模型扛不住 |
PgBouncer pool_mode | transaction | 复用最好(注意会话级特性限制,见 §5) |
PgBouncer max_client_conn | 10000+ | 对应用暴露的"廉价连接" |
PgBouncer default_pool_size | 40~100 / 池 | 对 PG 的真实连接数 |
shared_buffers | 内存 25% | |
effective_cache_size | 内存 65~75% | |
work_mem | 32~128MB | 按并发和排序需求调,过大易 OOM |
maintenance_work_mem | 1~2GB | |
| CPU/内存 requests=limits | 按机型给足 | 避免被限流/被驱逐 |
与本系统连接预算的关系
开发控制器可把多套逻辑库放在同一 PostgreSQL 实例中,但各服务的连接池配置会叠加。生产预算应从实际加载的 dev/config/token/*、dev/config/bizhub、dev/config/central 或对应独立配置逐项汇总,再预留迁移、监控和运维连接。不要沿用旧 compose 文档中的固定连接数。
读扩展:replica 数可以 >2(如 1 主 4 备),
-roPooler 把读流量分摊到所有 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-prod3.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: truebash
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_size 取 160GB 而非乐观的 180GB。上线后按 container_memory_working_set_bytes 实测再微调。
版本注意:CNPG < 1.24 没有
postgresql.synchronous段,改用minSyncReplicas: 1/maxSyncReplicas: 1。dataDurability需 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: barmanObjectStorebash
kubectl apply -f scheduledbackup.yaml
kubectl cnpg backup pg-cluster -n pg-prod # 立刻手动跑一次验证
kubectl -n pg-prod get backupHA ≠ 备份。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-prod9.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 验证
- 持续写入并记录已 commit 的最大 id
- 制造 failover
- 切换后查最大 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-v2SCC,一般无需 anyuid - 本地盘:大并发强烈建议本地 NVMe(local-path / TopoLVM),网络盘 IOPS 往往是瓶颈
- hugepages(
shared_buffers=64GB收益明显):bashPod resources 里加 hugepages 后,# 每台 DB 节点 sysctl -w vm.nr_hugepages=34000 # 64GB ÷ 2Mi ≈ 32768 页 + 余量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 ≥ 1且dataDurability: 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