
上个月帮一个内部项目搭数据库层需求很明确业务量不大但不想把数据库放在单点上要求先有一套 PostgreSQL 主从集群同时把连接池和读写入口的问题一起解决。折腾一圈之后最后落地的方案就是标题里这套组合Helm 部署 Bitnami 的 PostgreSQL chart前面再挂一个 Bitnami 的 Pgpool chart。这套方案最大的价值在于不用手写 StatefulSet不用自己拼初始化脚本也不用纠结每个 Pod 的探针和配置挂载大部分基础设施层面的脏活都被 chart 处理掉了。这篇文章适合两类人看。一类是已经在用 Kubernetes、想快速搭一套能用的 PostgreSQL 主从 读写分离入口的运维或后端同学另一类是对 Helm 部署中间件还不太熟、想找一个完整例子练手的人。我会按“为什么这么选 → 环境准备 → 部署 PostgreSQL → 部署 Pgpool → 读写分离验证 → 常见坑”的顺序讲命令和 values 配置都会给到位照着做基本能复现。先提醒一句这里说的“集群”是指 PostgreSQL 的一主多从拓扑不是 Hadoop 那种分布式计算集群。PostgreSQL 本身是单机数据库它的“集群”靠流复制把主库数据同步到只读副本配合中间件对外暴露统一入口。理解这一点后面所有配置就不会困惑了。1. 方案选型与架构拆解动手写 values 之前先把选型逻辑说清楚。很多人看到标题第一反应是为什么是 Bitnami而不是裸写 Helm chart或者直接用数据库 Operator我一开始也在这几个方案之间犹豫过。1.1 为什么是 Bitnami而不是裸 Chart 或 Operator裸手写 StatefulSet 做 PostgreSQL 主从听起来很“硬核”实际维护成本极高。你要自己处理的东西包括但不限于primary 和 replica 的配置文件差异、流复制用户的创建与认证、Pod 重启后的数据卷挂载、readinessProbe 和 livenessProbe 的探测命令、升级时的滚动策略。这些东西单独拎出来都不难但叠在一起就是大量边缘情况。真要在生产环境手写一套光测试就要花掉不少时间。Operator 方案比如 CloudNativePG 或 Zalando 的 postgres-operator功能确实强可以做自动故障转移、备份恢复、表空间管理。但它的学习曲线和概念模型也重对于只需要“一主一从 一个连接池入口”的中小规模项目来说有点杀鸡用牛刀。Bitnami 的 chart 处在中间位置它把 PostgreSQL 打包成参数化程度很高的 Helm chart镜像基于官方 PostgreSQL社区用的人多踩坑案例也容易搜到。它帮你处理好了探针、权限、初始化脚本、副本配置这些底层逻辑同时又保留了大量 values 参数让你控制集群形态。部署节奏很快后期维护也不会太痛苦。对我这次的需求来说这是最稳的折中选择。1.2 角色分工PostgreSQL 负责存储Pgpool 负责入口这套架构里PostgreSQL 和 Pgpool 分工很明确。PostgreSQL 这一侧通过 Bitnami chart 的architecture: replication模式拉起一个 primary 和一个或多个 read replica。primary 负责所有写操作通过流复制把 WAL 日志持续同步到 replica。replica 只接受读请求具备只读副本的全部能力。默认情况下这套复制是异步的也就是说主库和从库之间存在极小的数据延迟但对大量读多写少的业务场景已经够用。Pgpool 站在所有应用连接的最前面。应用不再直接连 PostgreSQL而是统一连到 Pgpool 的 Service 上。Pgpool 至少帮你做三件事连接池复用后端数据库连接减少 PostgreSQL 频繁建立连接的开销。读写分离识别 SQL 是读还是写把写流量转发给 primary把读流量按权重分发给 replica。健康检查周期性探测后端节点状态节点不可用时把对应后端标记为 down避免请求被打到故障节点上。简单说PostgreSQL 决定数据怎么存、怎么复制Pgpool 决定连接怎么进、请求怎么分发。两者合在一起对外体现为一个数据库地址对内体现为一主一从的高可用拓扑。1.3 版本组合与前置条件我这次用的版本组合是Kubernetes 1.28、Helm 3.14、PostgreSQL 16、Pgpool 4.5。Bitnami 的 chart 版本会持续更新注意锁定你验证过的版本别直接追最新版。前置条件列一张表条件要求用途Kubernetes 集群1.28 及以上建议至少 3 节点提供 Pod 调度和故障恢复Helm3.x部署和管理 chart存储类支持动态创建 PVC云盘或 local-path 均可PostgreSQL 数据持久化节点资源每个数据库节点至少 4 核 8G本地测试也建议 4G 以上primary replica pgpool 都要吃内存本地测试的话minikube 或者 k3s 都行但注意如果是单节点就没法演示节点故障场景。生产环境建议 primary 和 replica 通过节点亲和性分开调度避免同一台物理机挂掉后整个集群一起遭殃。2. 环境准备Helm、仓库与命名空间环境准备这一步看起来很简单但很多人翻车就翻在没先做“参数勘察”直接照着网上的配置文件套结果 chart 版本一升级字段全变了。这里我会把每一步的目的也讲清楚。2.1 安装 Helm 并添加 Bitnami 仓库先确认你的 Helm 版本helm version --short只要输出类似v3.14.0就行。如果本地还没装 Helm请参考官方安装文档mac 用户直接brew install helmLinux 用户可以用官方脚本或包管理器这里不展开了。接着添加 Bitnami 仓库并更新索引helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update添加之后建议先确认你当前能拿到哪些版本的 charthelm search repo bitnami/postgresql helm search repo bitnami/pgpool生产环境记住一个习惯用--version锁住 chart 版本。不加锁的情况下别人在另一个时间点部署拉到的 chart 版本可能不同生成的 Service 名称和 values 字段也就可能不同。2.2 用 helm show values 摸清 Chart 参数这一步是最容易被跳过的但恰恰是最重要的。Bitnami 的 chart 文档更新很快网上的博客写得再好也赶不上版本变化。正确做法是把默认 values 拉到本地先看一遍再改。helm show values bitnami/postgresql postgresql-values.yaml helm show values bitnami/pgpool pgpool-values.yaml比如在 PostgreSQL 的 values 里你要重点关注这几个区域architecture设置成replication才会创建主从。authPostgreSQL 的超级用户密码、复制用户密码。primary主库的资源、存储、节点选择。readReplicas从库数量、存储、资源。Pgpool 的 values 里重点关注authPgpool 用来连接后端 PostgreSQL 的用户名和密码。pgpool.backends后端数据库节点列表也就是 primary 和 replica 的地址。servicePgpool 对外暴露方式。不同 chart 版本的字段命名可能略有差异所以实际操作时以你手里的helm show values输出为准。这不是废话我见过有人把旧版配置直接套到新版上结果 Pgpool 明明起来了却始终找不到后端节点。2.3 命名空间与网络规划部署之前先规划好命名空间。我这次统一放在database命名空间里kubectl create ns database为什么要单独规划命名空间因为 Pgpool 需要访问后端的 PostgreSQL 节点。如果两者在同一个命名空间Pods 之间可以通过 Service 短名访问比如mypg-postgresql-primary和mypg-postgresql-read。一旦跨了命名空间就得写完整的 FQDN比如mypg-postgresql-primary.database.svc.cluster.local。虽然也不是不行但配置起来容易出错也不利于权限隔离。另外如果你在集群里启用了 NetworkPolicy记得允许 database 命名空间内部 5432 端口的互访否则 Pgpool 会一直报连接超时。3. 部署 PostgreSQL 读写分离集群这一章是核心中的核心。先把 PostgreSQL 的主从集群拉起来后面的 Pgpool 才有后端可连。3.1 最小可用 values 配置在刚才拉取下来的postgresql-values.yaml基础上裁剪出一份最小可用配置。以 release 名mypg为例architecture: replication auth: postgresPassword: change-me-postgres replicationPassword: change-me-repl database: appdb username: appuser primary: persistence: size: 8Gi resources: requests: memory: 1Gi cpu: 500m limits: memory: 2Gi cpu: 1 readReplicas: replicaCount: 1 persistence: size: 8Gi resources: requests: memory: 1Gi cpu: 500m limits: memory: 2Gi cpu: 1逐项解释一下为什么这样写。architecture: replication是总开关。不设这个字段chart 默认部署单实例后面的readReplicas.replicaCount根本不会生效。auth.postgresPassword是 PostgreSQL 超级用户postgres的密码。auth.replicationPassword是专门用于流复制的用户repl_user的密码。这两个密码必须设置否则 chart 会在初始化时生成随机密码你后面连接会非常痛苦。auth.database和auth.username让 chart 在初始化时顺便创建一个业务数据库和业务用户避免后面手动建库建用户。primary.persistence和readReplicas.persistence分别控制主库和从库的存储容量。注意这两份 PVC 是独立创建的容量最好保持一致否则以后从库追上主库时数据量不够用。资源限制这里只给了保守值。实际生产建议主库至少 2Gi 内存起步如果业务复杂4Gi 也不为过。不要省略resources不限制资源的话Pod 可能把节点内存吃满触发 OOMKilled那画面太惨。3.2 部署并验证主从就绪执行安装helm install mypg bitnami/postgresql \ --namespace database \ --values postgresql-values.yaml \ --version 12.x.x--version里的具体版本号自己替换成helm search repo bitnami/postgresql查到的稳定版本。安装过程中可以用两个终端一个执行安装另一个执行kubectl get pods -n database -w正常情况下你会看到类似这样的状态变化mypg-postgresql-primary-0先进入Pending等 PVC 绑定后变成ContainerCreating最后Running。随后mypg-postgresql-read-0开始创建它初始化之后会通过流复制从 primary 拉取数据。Pod 全部 Running 之后再看 Servicekubectl get svc -n databaseBitnami 在architecture: replication模式下通常会生成这样几个 Servicemypg-postgresql对外的主服务入口一般指向 primary。mypg-postgresql-primaryprimary 的独立 Service。mypg-postgresql-read所有只读副本的聚合入口。mypg-postgresql-headless用于 StatefulSet 内部稳定网络标识。有了这几个 Service 名后面配 Pgpool 时直接拿来当 host 用。现在验证主从复制是否正常。临时起一个 psql 客户端 Podkubectl run psql-client --rm -it \ --restartNever \ --namespace database \ --image docker.io/bitnami/postgresql:16 \ --env PGPASSWORDchange-me-postgres \ --env PGHOSTmypg-postgresql-primary \ -- psql -U postgres -c SELECT pg_is_in_recovery();主库上执行SELECT pg_is_in_recovery()返回f表示不是只读副本是主库。如果返回t说明你连到了只读副本。再查一下流复制状态SELECT client_addr, state, sync_state FROM pg_stat_replication;正常会有一条state streaming的记录sync_state通常是async代表异步复制。到这里PostgreSQL 主从集群已经就绪。3.3 存储与资源要点存储和调度是 PostgreSQL 上 K8s 之后最常见的两个坑位这里单独强调。存储方面Bitnami 的镜像默认使用非 root 用户运行PVC 挂载目录的权限不对会直接导致初始化失败。如果你用的是云厂商存储类一般没问题如果自己搭的 NFS 或 local-path要确认 StorageClass 支持设置fsGroup或挂载后的目录允许非 root 写入。出现chmod: changing permissions of /bitnami/postgresql: Operation not permitted这类错误基本就是存储权限问题。调度方面如果集群有多个节点强烈建议让 primary 和 replica 分布在不同节点上primary: nodeSelector: role: db-primary readReplicas: nodeSelector: role: db-standby或者用topologySpreadConstraints做打散。不这样做的话你看着是两个副本实际上可能调度到同一台机器上物理节点挂了照样全挂。4. 部署 Pgpool 作为统一入口PostgreSQL 集群起来之后下一步部署 Pgpool。这一章的重点是理解 Pgpool 需要的配置数据和它跟后端数据库之间的认证关系。4.1 先理解 Pgpool 需要什么Pgpool 本质上是一个数据库代理它要正常工作需要三样东西第一一个能连通后端 PostgreSQL 的数据库账号。这个账号用于 Pgpool 做健康检查、在实际连接转发时以对应用户身份连接到后端。最简单的做法是直接用postgres超级用户但生产环境强烈不建议应该创建一个专用账号只授予必要的权限。第二后端节点列表。Pgpool 必须知道 primary 和 replica 的主机名、端口。它会对每个后端做健康检查并根据节点角色决定读写转发策略。第三节点角色识别能力。Pgpool 通过执行SELECT pg_is_in_recovery()来判断一个后端是主库还是从库。返回f的是主返回t的是从。这个探活逻辑内建在 Pgpool 里不需要你额外写脚本。Bitnami 的 Pgpool chart 把这些配置都收敛到了 values 文件里。你只需要提供用户名密码、后端地址列表chart 会在启动时生成 Pgpool 需要的pool_passwd文件和配置文件。4.2 values 配置与参数解释在pgpool-values.yaml里我这里给出一份可用配置auth: username: postgres password: change-me-postgres pgpool: backends: - host: mypg-postgresql-primary port: 5432 - host: mypg-postgresql-read port: 5432 loadBalanceMode: true healthCheckPeriod: 10 healthCheckTimeout: 5 replicaCount: 2 service: type: ClusterIP这份配置的含义auth.username和auth.password是 Pgpool 连接后端 PostgreSQL 用的凭据。这里直接用了postgres超级用户来打通链路适合前期验证。后面如果要收敛权限可以在 PostgreSQL 里创建专用账号pgpool给它pg_monitor等必要权限再把这里的用户名密码替换掉。pgpool.backends是后端节点列表。host 填 primary 和 read 聚合 Service 的名字port 填 5432。这里的 host 之所以能用短名是因为 Pgpool 和 PostgreSQL 部署在同一个database命名空间。pgpool.loadBalanceMode控制读写负载均衡。打开后查询请求会被分发到多个后端节点写请求始终走主库。pgpool.healthCheckPeriod和pgpool.healthCheckTimeout分别控制健康检查的间隔和超时。演示环境里 10 秒和 5 秒够用生产可以调得更保守一些。replicaCount: 2表示部署 2 个 Pgpool 副本。Pgpool 本身无状态多副本可以避免单点。service.type: ClusterIP表示只在集群内部暴露。如果外部业务需要访问可以改成LoadBalancer或NodePort但通常建议数据库入口不直接暴露到外网而是只允许业务 Pod 所在的命名空间访问。4.3 部署和登录验证执行安装helm install pgpool bitnami/pgpool \ --namespace database \ --values pgpool-values.yaml安装完成后确认 Pod 状态kubectl get pods -n database -l app.kubernetes.io/namepgpool两个 Pgpool Pod 都 Running 后进到 Pod 里直接执行show pool_nodes这是验证节点识别最直观的方式kubectl exec -it -n database deploy/pgpool-pgpool -- \ psql -h 127.0.0.1 -U postgres -c show pool_nodes;密码会提示输入输入change-me-postgres即可。输出的大致格式如下不同版本列名可能略有差异node_id | hostname | port | status | pg_role | weight ------------------------------------------------------------------- 0 | mypg-postgresql-primary | 5432 | up | primary | 0.5 1 | mypg-postgresql-read | 5432 | up | standby | 0.5这里最关键的是status和pg_role两列。status为up表示 Pgpool 能成功连上并完成健康检查pg_role则标明了它识别出的主从角色。如果看到down说明 Pgpool 连不上对应后端或者后端认证报错需要排查网络和密码。如果你部署 Pgpool 时碰到类似password authentication failed大概率是auth.password和 PostgreSQL 的postgresPassword不一致或者数据库里对应账号的密码不是这个值。5. 读写分离与故障转移验证部署都完成之后不能光看 Pod 是 Running 就收工。数据库这类基础设施不实际验证读写路径你永远不知道自己配置有没有生效。5.1 验证读写流量走向用一个客户端 Pod 通过 Pgpool 的 Service 连接数据库做一轮基本验证kubectl run pg-test --rm -it \ --restartNever \ --namespace database \ --image docker.io/bitnami/postgresql:16 \ --env PGPASSWORDchange-me-postgres \ --env PGHOSTpgpool-pgpool \ --env PGPORT5432 \ -- psql -U postgres -d appdb连接成功后先建一张测试表并插入一条数据CREATE TABLE IF NOT EXISTS test_table (id serial primary key, value text); INSERT INTO test_table (value) VALUES (write-to-primary);这条 INSERT 会通过 Pgpool 转发到 primary。然后执行查询SELECT * FROM test_table;只要结果能返回说明读路径是通的。如果你连续执行多次查询并打开 Pgpool 的日志观察会发现查询请求会被负载均衡到mypg-postgresql-read后面的副本节点上。更直接的验证方式是再次执行show pool_nodes并配合在 PostgreSQL 主库上观察连接来源SELECT usename, client_addr, application_name FROM pg_stat_activity;你会看到 Pgpool 产生的连接来自 Pgpool Pod 的 IP而不是业务 Pod 的 IP这就说明连接池生效了。5.2 模拟主库不可用观察 Pgpool 行为只验证正常流量还不够数据库高可用架构必须验证故障场景。这里我可以给你一个安全的演练方案。先进入 primary Pod通过pg_ctl stop模拟 PostgreSQL 进程不可用kubectl exec -it -n database mypg-postgresql-primary-0 -- \ bash -c pg_ctl stop -D /bitnami/postgresql/data -m fast注意用-m fast不要用-m immediate避免模拟误删数据。停掉之后等一个健康检查周期再连 Pgpool 执行SHOW pool_nodes;你会发现对应 primary 的节点状态会变成down。此时如果业务继续往这个入口发写请求会被 Pgpool 拒绝或挂起直到节点恢复。这就引出一个重要认知Pgpool 本身并不负责 PostgreSQL 主从角色的自动提升。它只负责感知节点健康状态并在节点恢复后重新纳入池子。真正的故障转移也就是把某个从库提升为新主库还需要在 PostgreSQL 那一侧做处理比如用pg_ctl promote手动提升或者接入自动故障转移组件。对绝大多数中小项目来说默认的异步流复制 Pgpool 健康检查已经能扛住“主库进程假死、自动重启”这一类场景但如果是主库所在节点永久宕机你必须有明确的恢复预案不能指望 Pgpool 帮你变魔术。5.3 应用接入方式的建议验证通过后应用侧的接入方式其实很简单。应用不再需要配置 primary 和 replica 两套数据源只需要一个地址jdbc:postgresql://pgpool-pgpool.database.svc.cluster.local:5432/appdb如果是 Python、Go 等其他语言配置项也差不多就是把 host 换成 Pgpool 的 Service 名。这里有两个实用建议。第一应用的连接池上限不要设得比 Pgpool 的后端连接池大太多否则流量一上来连接堆积在 Pgpool 层照样会把数据库打垮。第二如果业务对只读副本的数据延迟很敏感要提前评估异步复制的延迟量级必要时在应用层针对强一致读走单独入口而不是把所有流量都扔给 Pgpool 自动分发。6. 常见问题与避坑实录最后这部分是真正的干货。下面这些问题都是我在实际部署和后续维护中真实遇到过、排查过、解决过的整理成一份速查表方便你对照。6.1 密码认证失败现象Pgpool Pod 日志里持续报password authentication failed for user postgres或者 Pgpool 启动后所有后端节点都是down。排查思路确认 PostgreSQL chart 里auth.postgresPassword和你记的密码一致。Bitnami chart 在数据库初始化时设置了密码之后如果直接改 values 里的密码再执行helm upgrade数据库的密码不会跟着变容易出现你以为改了、实际没改的情况。如果用auth.username创建了业务用户确认 Pgpool 里用的也是这个用户名而非postgres。检查pg_hba.conf是否允许 Pgpool 所在网段使用scram-sha-256认证。Bitnami 默认配置一般没问题但如果你手动改过数据库认证配置这是高发点。6.2 副本处于 async 状态现象执行SELECT client_addr, state, sync_state FROM pg_stat_replication;所有副本的sync_state都是async。这其实是 Bitnami 默认的异步复制模式不是故障。异步复制的含义是主库提交事务时不会等待从库确认因此主库异常宕机时可能丢失少量已提交但未同步到从库的数据。如果你的业务对数据丢失零容忍需要配置同步复制在 chart 的 values 里找postgresql.synchronousMode或类似字段。注意同步复制会显著增加写延迟因为每次提交都要等从库确认。要根据业务场景权衡不要盲目追求“最强一致性”。6.3 PVC 权限和存储问题现象PostgreSQL Pod 卡在ContainerCreating事件里出现chmod: changing permissions of /bitnami/postgresql: Operation not permitted。原因Bitnami 镜像以非 root 用户运行PVC 挂载目录的所有者不是这个用户导致初始化时无法写入。解决办法云厂商的 StorageClass 大多能自动处理 fsGroup不会遇到这个问题。自己搭的 NFS 或 local-path需要确认 StorageClass 配置了正确的fsGroup策略或者挂载目录本身对非 root 用户可写。千万别为了省事把 Pod 安全上下文改成 root 运行那属于给自己埋雷。6.4 升级与迁移注意事项Bitnami 的 chart 升级频率很高但这不代表你可以无脑helm upgrade。最常见的坑是跨大版本升级 PostgreSQL比如从 15 升到 16。这种升级需要在数据库层面做pg_upgrade而不是简单替换镜像。直接替换镜像启动时数据目录的版本不匹配Pod 会反复崩溃。另一个坑是helm upgrade --reuse-values。这个参数看起来很省事但它会把旧版 values 直接搬到新版本上。如果新版 chart 改了某个字段的语义你会在毫不知情的情况下得到一份错误配置。我的习惯是把自己改过的 values 文件提交到 git升级时先用helm show values对比新版默认值再显式传一份完整配置。还有一件事容易被忽略升级前一定先备份。PostgreSQL 表空间数据不能光靠 PVC 快照建议结合pg_dump或至少把 PVC 快照打一个否则升级过程中出现问题恢复成本会很高。最后再分享一点个人体会。这套 Helm Bitnami Pgpool 的组合最适合的场景是“你已经决定用 Kubernetes 管数据库但又不希望自己造轮子”。它把部署复杂度降得很低但并没有把运维复杂度完全抹掉。你在生产环境真正要花心思的仍然是备份恢复、监控告警、版本升级、容量规划这些老问题。建议上线前把pg_stat_replication的延迟监控和 Pgpool 的show pool_nodes状态采集都接到告警里这两项指标能帮你提前发现大多数隐患。