在Kubernetes上以Operator方式运行管理PostgreSQL高可用集群
postgres-operator 至少有四个互不兼容的同名项目,这是选型时最容易踩的坑。本文写的是 Zalando 的 postgres-operator(CRD 组 acid.zalan.do,集群对象叫 postgresql),理由是它社区体量最大、仍在活跃发版,并且 CRD 足够简单,适合作为理解「Operator 管数据库」的入口。
| 项目 | 集群 kind | apiVersion | 说明 |
|---|---|---|---|
| Zalando postgres-operator | postgresql(短名 pg) |
acid.zalan.do/v1 |
本文主角,最新 v2.0.2,支持 PG 14→18 |
| Crunchy Data PGO(现称 CPK) | PostgresCluster |
postgres-operator.crunchydata.com/v1beta1(v6 起有 v1) |
功能最全,pgBackRest + PGUpgrade CRD;2025 年被 Snowflake 收购后仍在发版 |
| StackGres | SGCluster 等 SG* 对象 |
stackgres.io/v1 |
全栈方案,自带连接池与监控 |
| Crunchy PGO v4 及更早 | pgcluster |
crunchydata.com/v1 |
已过时,不要照着老文档写 |
Zalando 这套的分工要分清:高可用是 Patroni 在做(选主、流复制、故障切换),Operator 只负责把 postgresql 这个 CR 变成 StatefulSet、Service、Secret 和 Endpoints,并通过 Patroni 的 REST API 观察集群成员状态。所以排障时一半时间在看 Operator 日志,另一半时间在看 Patroni。
# Helm(注意仓库地址以 chart 名结尾)
helm repo add postgres-operator-charts \
https://opensource.zalando.com/postgres-operator/charts/postgres-operator
helm repo update
helm install postgres-operator postgres-operator-charts/postgres-operator \
--namespace postgres-operator --create-namespace
# v2 起也提供 OCI 制品
helm install postgres-operator oci://ghcr.io/zalando/charts/postgres-operator \
--version 2.0.2 -n postgres-operator --create-namespace
kubectl get pods -n postgres-operator
kubectl get crd | grep zalando
acid.zalan.do/v1
postgresql PostgreSQL 集群(短名 pg,注意 kind 是全小写)
operatorconfiguration Operator 自身配置(短名 opconfig)
postgresteam 团队与成员权限(短名 pgteam)
apiVersion: acid.zalan.do/v1
kind: postgresql
metadata:
name: acid-minimal-cluster
spec:
teamId: "acid" # 必填,归属团队
numberOfInstances: 3 # 1 主 + 2 从
postgresql:
version: "17" # 主版本号字符串,创建后改动即触发大版本升级
parameters:
max_connections: "200"
shared_buffers: "1GB"
volume:
size: 100Gi
storageClass: fast
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
memory: 2Gi
tls:
secretName: acid-minimal-cluster-ssl
connectionPooler:
numberOfInstances: 2
mode: transaction # transaction | session
enableLogicalBackup: false # 默认 false,开启后走 CronJob 逻辑备份
kubectl apply -f cluster.yaml -n postgres
# CRD 自带打印列,一眼能看到 Team / Version / Pods / Volume / Status
kubectl get postgresql -n postgres
kubectl get pg acid-minimal-cluster -n postgres -o wide
# 主库(可写):Service 名就是集群名
acid-minimal-cluster.postgres.svc.cluster.local:5432
# 只读:集群名加 -repl 后缀
acid-minimal-cluster-repl.postgres.svc.cluster.local:5432
# 连接池(配置了 connectionPooler 后才有)
acid-minimal-cluster-pooler.postgres.svc.cluster.local:5432
# 取用户密码
kubectl get secret postgres.acid-minimal-cluster.credentials.postgresql.acid.zalan.do \
-n postgres -o jsonpath='{.data.password}' | base64 -d
# 默认强制 TLS,不带 SSL 会被 pg_hba 拒绝
kubectl run -it --rm psql --image=postgres:17 --restart=Never -- \
env PGSSLMODE=require PGPASSWORD=<password> \
psql -h acid-minimal-cluster -U postgres -c '\l'
# 唯一的副本数字段
kubectl patch postgresql acid-minimal-cluster -n postgres --type merge \
-p '{"spec":{"numberOfInstances":5}}'
扩容时 Operator 只改 StatefulSet 副本数,新成员由 Patroni 负责克隆数据并加入复制;从库数据量大时这一步很慢,要盯 Patroni 日志而不是只看 Pod 是否 Running。缩容时 Operator 会先让 Patroni 把节点移出集群再删 Pod。
# 每个实例一套 PVC
kubectl get pvc -n postgres
# 只能扩,不能缩(缩小不生效)
kubectl patch postgresql acid-minimal-cluster -n postgres --type merge \
-p '{"spec":{"volume":{"size":"200Gi"}}}'
缩容默认保留 PVC,但删除 postgresql 清单默认会连 PVC 一起删 —— 这是两套开关,别搞混:
# 定义在 operator 配置里(retention policy 默认 when_deleted: retain、when_scaled: retain)
persistent_volume_claim_retention_policy:
when_deleted: retain
when_scaled: retain
# 但 Operator 默认还会主动删 PVC —— 想保住数据必须关掉
enable_persistent_volume_claim_deletion: false
Zalando 这套没有备份 CRD,备份能力由 Operator 配置项与集群清单共同决定,两条路:
# 1) 连续归档(WAL):把归档送到对象存储,是 PITR 的前提
# operator 配置项,文档仍写作 WAL-E,现役 Spilo 用的是 WAL-G;还需注入 AWS 凭据
wal_s3_bucket: my-wal-bucket
# 2) 逻辑备份:Operator 会创建 CronJob,定期 pg_dump 并上传
spec:
enableLogicalBackup: true
logicalBackupSchedule: "30 00 * * *" # 默认值
恢复到某个时间点靠克隆实现:新建一个集群,把源集群与目标时间写进 spec.clone。
apiVersion: acid.zalan.do/v1
kind: postgresql
metadata:
name: acid-restored
spec:
teamId: "acid"
numberOfInstances: 3
postgresql:
version: "17"
volume:
size: 100Gi
clone:
cluster: "acid-minimal-cluster" # 源集群名(或 S3 上的 key)
timestamp: "2026-09-18T02:00:00+00:00" # 非包含式恢复目标,必须带时区
不做时间点恢复、只复制一个集群时,省掉 timestamp 即可(同 namespace 内走 pg_basebackup);要从 S3 克隆则给出 uid 与 s3_wal_path。新集群的 version 必须不低于源集群。
# 1. 先手动升级 CRD —— Helm 不会升级 CRD,这是官方明确提示的
kubectl apply -f https://raw.githubusercontent.com/zalando/postgres-operator/v2.0.2/charts/postgres-operator/crds/postgresqls.yaml
kubectl apply -f https://raw.githubusercontent.com/zalando/postgres-operator/v2.0.2/charts/postgres-operator/crds/operatorconfigurations.yaml
kubectl apply -f https://raw.githubusercontent.com/zalando/postgres-operator/v2.0.2/charts/postgres-operator/crds/postgresteams.yaml
# 2. 升级 Operator
helm upgrade postgres-operator postgres-operator-charts/postgres-operator \
-n postgres-operator
小版本升级就是换 Spilo 镜像,Operator 做滚动更新加一次受控 switchover,通常只有几秒不可写,但客户端必须能重连。
大版本升级改的是 spec.postgresql.version,没有额外的 annotation 开关:
# 由 Operator 配置项 major_version_upgrade_mode 控制
# off = 不升级
# manual = 清单里改了才升(chart 默认值)
# full = 清单改动或低于 minimal_major_version 都会升
major_version_upgrade_mode: "manual"
kubectl patch postgresql acid-minimal-cluster -n postgres --type merge \
-p '{"spec":{"postgresql":{"version":"18"}}}'
# 升级结果会写回 CR 的 annotation
kubectl get pg acid-minimal-cluster -n postgres \
-o jsonpath='{.metadata.annotations.last\-major\-upgrade\-success}'
升级动作由主库内的 inplace_upgrade.py 完成,执行前 Operator 会检查:是否在 maintenanceWindows 内、是否 standby 集群、所有 Pod 是否 Running、副本复制延迟是否小于 16 MiB。任一不满足就跳过,失败过的集群会被打上 last-major-upgrade-failure 注解并不再自动重试。
三个层级要分清:
# 1) Patroni 暴露集群自身的指标(Prometheus 格式)
kubectl exec -n postgres acid-minimal-cluster-0 -- curl -s localhost:8008/metrics | head
# 2) PostgreSQL 业务指标要自己挂 exporter sidecar(端口 9187)
spec:
sidecars:
- name: exporter
image: prometheuscommunity/postgres-exporter:v0.16.0
ports:
- name: exporter
containerPort: 9187
env:
- name: DATA_SOURCE_URI
value: "localhost:5432/postgres?sslmode=require"
# 3) Operator 自身只有 8080 上的诊断 API(/status、/clusters 等),不提供 Prometheus 指标
kubectl port-forward -n postgres-operator svc/postgres-operator 8080:8080
配好 sidecar 与 podAnnotations 后,再用 ServiceMonitor / PodMonitor 抓 9187 与 8008 两个端口。
kubectl get postgresql、acid.zalan.do/v1);Crunchy PGO 用的是 kubectl get postgrescluster、postgres-operator.crunchydata.com/v1beta1;StackGres 是 SG* 对象。三套文档互相不能照抄。kubectl apply 对应版本的 crds/ 目录。enable_persistent_volume_claim_deletion 默认为 true,它会无视 StatefulSet 上的 retain 策略,把 PVC 一并删除。要保住数据必须显式设为 false。major_version_upgrade_mode: "manual",只要动了 spec.postgresql.version 就会真的执行 pg_upgrade,而一旦开始就无法回滚。改版本号前请确认备份可用、且在维护窗口内。PGSSLMODE=require 会直接被拒,典型报错是 no pg_hba.conf entry for host ..., no encryption。spec.volume.size,缩小会给出警告且不生效;真要缩小只能重建 PVC 与 Pod,等于重建实例。-repl,而 Service 名上限 63 字符,因此集群名被限制在 58 以内,名字太长会导致资源创建失败。maintenanceWindows 不配置就没有窗口限制。字段名容易写错(是复数 maintenanceWindows),写错的后果是升级可能在业务高峰自动执行。kubectl — Kubernetes集群管理工具statefulset — PostgreSQL 实例的实际载体pvc — 持久卷声明,数据盘storageclass — 存储类,决定数据库存储性能helm — Kubernetes包管理器secret — 存放数据库用户与对象存储凭据prometheus — 抓取 Patroni 与 exporter 指标