
1. 为什么要在 Kubernetes 上折腾 Stolon 这套 Postgres 集群1.1 从单点 Postgres 的痛处说起但凡在生产环境跑过单实例 Postgres 的人大概率都经历过这种场景业务量一上来主库 CPU 打满读写全挤在一个节点上或者更糟宿主机磁盘出问题数据库直接躺平恢复全靠备份和运气。传统做法是上主从流复制加 Keepalived 或者 Patroni 做故障切换但这套东西在物理机时代还行搬到 Kubernetes 上就有点水土不服——Pod 的 IP 是漂移的节点随时可能被驱逐你用固定 VIP 的思路去套容器编排迟早要出问题。Stolon 就是在这个背景下进入视野的。它是一套专门为云原生环境设计的 Postgres 高可用方案核心思路是把Postgres 实例、集群状态存储、代理路由这三件事拆开Postgres 进程本身跑在容器里集群的元数据和选主信息放在 etcd 或 Consul 这类分布式 KV 存储里客户端不直连数据库而是通过一个叫stolon-proxy的组件做转发。这样一来主库挂了Stolon 的 keeper 组件会重新选主proxy 自动把流量切到新的主库上客户端几乎无感知。我最初接触 Stolon 是因为一个内部项目需要一套能自愈的 Postgres 集群要求是节点宕机后 30 秒内自动恢复写入能力且不能依赖人工介入。试过 Patroni配置繁琐且和 K8s 的集成需要额外写不少脚本后来换成 Stolon配合 Helm 部署整个集群的声明式管理清爽很多。这篇文章就把我用 Helm 部署 Stolon 的完整过程拆开讲包括架构选型、参数计算、踩过的坑以及一套可以直接抄作业的 values 配置。1.2 Stolon 的组件分工与数据流在动手之前得先把 Stolon 的几个核心组件搞清楚不然出了问题你都不知道该查哪个 Pod。stolon-keeper真正跑 Postgres 进程的组件每个 keeper 管理一个 Postgres 实例。它负责启动、停止 Postgres执行流复制配置并在收到 sentinel 的指令后执行主从切换。keeper 的数据目录必须持久化否则 Pod 重建后数据就没了。stolon-sentinel集群的“大脑”负责监控 keeper 的健康状态、维护集群视图、在需要时发起选主。sentinel 本身是无状态的多个 sentinel 通过 etcd 做分布式协调避免脑裂。stolon-proxy客户端接入层对外暴露一个稳定的服务地址。proxy 从 sentinel 获取当前主库的位置把写请求转发过去。读请求默认也走主库但可以通过配置做读写分离。stolonctl命令行工具用来查看集群状态、手动触发切换、初始化集群。它直接和 etcd 以及 sentinel 通信。数据流是这样的客户端连 proxy 的 Serviceproxy 查询 sentinel 拿到 master 的地址然后把连接转发到对应的 keeper Pod。keeper 内部的 Postgres 通过流复制保持同步。etcd 里存的是集群的拓扑信息比如哪个 keeper 是 master、复制位点在哪、集群是否健康。注意Stolon 的 proxy 并不是一个连接池它只做 TCP 转发。如果你需要连接池得在 proxy 前面再挂一个 PgBouncer或者用支持连接池的客户端。这一点和很多人的直觉不一样我第一次部署时就以为 proxy 自带池化结果压测时连接数直接爆了。1.3 为什么用 Helm 而不是手写 YAMLStolon 官方提供了一套 Kubernetes 的部署清单但那是纯 YAML改起来很痛苦。比如你想调整 keeper 的副本数、改一下 Postgres 的版本、或者给 proxy 加个 NodePort都得手动去改好几个文件。Helm 的价值在于把这些可变的部分抽成 values.yaml用模板渲染出最终的清单。社区里有几个 Stolon 的 Helm Chart我对比过之后选了一个维护相对活跃的下面会讲怎么选 Chart 以及怎么改。用 Helm 的另一个好处是版本管理。Stolon 的镜像版本、Postgres 的版本、etcd 的版本都可以在 values 里锁定回滚的时候直接helm rollback就行不用去记上次改了哪些 YAML。对于生产环境来说这种可追溯性很重要。2. 部署前的环境准备与关键参数计算2.1 集群资源规划与节点要求Stolon 对 Kubernetes 集群本身没有太特殊的要求但有几个硬性条件必须满足。首先你需要一个可用的StorageClass因为 keeper 的 PVC 要靠它动态供给。我用的是本地盘做的 StorageClass生产环境建议用云厂商的 SSD 盘或者 Ceph RBD。其次etcd 集群必须独立于 Stolon 部署不要图省事把 etcd 和 keeper 混在一起否则 etcd 的 IO 抖动会直接影响数据库性能。资源方面我按每个 keeper 分配 2 核 4G 起步Postgres 的 shared_buffers 设为内存的 25% 左右也就是 1G。proxy 和 sentinel 很轻量各给 0.5 核 512M 就够。存储上keeper 的 PVC 至少 20G具体看你的数据量但一定要留出 WAL 日志的空间。我吃过亏有一次 PVC 只给了 10G结果 WAL 堆积把盘写满了Postgres 直接拒绝写入。节点数量上如果你要部署 3 个 keeper一主两从那至少要有 3 个可调度的 worker 节点并且用PodAntiAffinity让它们分散到不同节点。不然一个节点挂了主从全没高可用就成了笑话。这个反亲和性配置在 Helm 的 values 里可以开后面会讲。2.2 etcd 集群的独立部署与连接串Stolon 依赖 etcd 存储集群状态所以你得先有一个可用的 etcd 集群。如果你已经有现成的 etcd直接拿连接串来用就行。如果没有可以用 Helm 单独部署一个三节点的 etcd 集群或者用云厂商的托管 etcd 服务。连接串的格式是etcd://user:passwordhost:port多个节点用逗号分隔。比如etcd://10.0.0.1:2379,10.0.0.2:2379,10.0.0.3:2379这里有个细节Stolon 的 sentinel 和 keeper 都需要访问 etcd所以这个连接串要配在 values 里并且确保网络策略允许 Pod 访问 etcd 的端口。我遇到过因为 NetworkPolicy 没放行 2379 端口导致 sentinel 一直选不出主集群卡在初始化状态。提示etcd 的认证信息建议用 Kubernetes Secret 存储不要明文写在 values.yaml 里。Helm 的 values 文件如果进了 Git 仓库明文密码就等于泄露了。2.3 存储类与 PVC 的选型考量StorageClass 的选择直接决定了数据库的 IO 性能和可靠性。我测试过三种方案本地盘、NFS、云盘。本地盘性能最好但 Pod 漂移到其他节点后数据就丢了除非你用 Local PV 加节点亲和性把 Pod 钉死在特定节点上。NFS 方便共享但延迟高不适合高并发写入。云盘是折中方案性能稳定支持跨节点挂载但成本高一些。我的建议是生产环境用云盘或者 Ceph RBD测试环境可以用本地盘。在 values 里指定 storageClassName 的时候要确认这个 StorageClass 的volumeBindingMode是WaitForFirstConsumer这样 PVC 会等到 Pod 调度到某个节点后再创建避免出现 PVC 和 Pod 不在同一个可用区的问题。PVC 的大小计算有个经验公式数据量 × 2 WAL 预留。比如你的数据是 10G那 PVC 至少给 25G留出 WAL 和临时文件的余量。Postgres 的 WAL 在写入高峰期可能涨到几个 G如果盘满了数据库会进入只读模式业务直接受影响。3. Helm Chart 的选型与 values 配置详解3.1 挑选合适的 Stolon Chart社区里的 Stolon Helm Chart 有好几个版本我试过两个最后选了一个叫stolon的 Chart它的 values 结构比较清晰支持 keeper、sentinel、proxy 分别配置。选 Chart 的时候要看几个点最近一次更新时间、支持的 Stolon 版本、有没有人提 issue 并且被回复。如果一个 Chart 半年没更新了大概率有坑没人修。添加仓库的命令是helm repo add stolon https://example-chart-repo.com/stolon helm repo update然后可以用helm search repo stolon看看有哪些版本。我一般会锁定一个具体的 Chart 版本比如--version 0.3.2避免自动升级到不兼容的新版本。下载下来之后先别急着改 values用helm show values stolon/stolon my-values.yaml把默认配置导出来然后在这个基础上改。这样你能看到所有可配项不会漏掉关键参数。3.2 核心 values 参数逐项拆解下面是我实际用的 values.yaml 的关键部分逐项解释为什么这么配。keeper: replicaCount: 3 image: repository: sorintlab/stolon tag: v0.16.0-pg13 resources: requests: cpu: 2 memory: 4Gi limits: cpu: 4 memory: 8Gi persistence: enabled: true size: 25Gi storageClassName: fast-ssd podAntiAffinity: enabled: true requiredDuringSchedulingIgnoredDuringExecution: truereplicaCount: 3表示三个 keeper一主两从。为什么是 3 而不是 2因为 Stolon 的选主需要多数派2 个 keeper 的情况下如果主库挂了剩下的 1 个无法形成多数派集群会失去写入能力。3 个 keeper 可以容忍 1 个节点故障仍然保持多数派。image.tag里带了pg13表示这个镜像内置的是 Postgres 13。Stolon 的镜像版本和 Postgres 版本是绑定的换 Postgres 版本要换镜像 tag。我选 pg13 是因为它比较稳定且社区支持好。podAntiAffinity的requiredDuringSchedulingIgnoredDuringExecution: true是硬性反亲和意思是三个 keeper 必须调度到不同节点。如果节点不够Pod 会一直 Pending。测试环境如果只有两个节点可以改成preferred但生产环境强烈建议用硬性反亲和。sentinel 的配置sentinel: replicaCount: 3 resources: requests: cpu: 500m memory: 512Misentinel 也是 3 个和 keeper 数量一致。sentinel 之间通过 etcd 做 leader 选举3 个 sentinel 可以容忍 1 个故障。proxy 的配置proxy: replicaCount: 2 service: type: ClusterIP port: 5432 resources: requests: cpu: 500m memory: 512Miproxy 给 2 个副本前面挂一个 Service 做负载均衡。客户端连 Service 的 ClusterIP 就行。如果你需要从集群外访问把 type 改成 NodePort 或者 LoadBalancer。etcd 的连接串配置etcd: endpoints: - etcd-0.etcd:2379 - etcd-1.etcd:2379 - etcd-2.etcd:2379这里假设 etcd 是部署在同一个命名空间下的 StatefulSetDNS 名称是etcd-0.etcd这种格式。如果你的 etcd 在别的命名空间要写完整的 FQDN比如etcd-0.etcd.other-namespace.svc.cluster.local。3.3 密码与敏感信息的管理Stolon 的 Postgres 超级用户密码、复制用户密码都需要配置。默认情况下Chart 会生成随机密码存在 Secret 里。但如果你想指定密码可以在 values 里设置superuser: username: postgres password: your-strong-password replication: username: repl password: another-strong-password注意密码里不要有特殊字符比如、#、$因为 Stolon 在拼接连接串时可能不做转义导致连接失败。我踩过这个坑密码里带了个结果 keeper 一直连不上 etcd排查了半天才发现是密码解析问题。更好的做法是用existingSecret引用一个预先创建好的 SecretexistingSecret: stolon-secrets然后手动创建 Secretkubectl create secret generic stolon-secrets \ --from-literalsuperuser-passwordxxx \ --from-literalreplication-passwordyyy这样密码不会出现在 values 文件里安全性更高。4. 完整部署流程与集群初始化实操4.1 命名空间与前置资源创建先创建一个独立的命名空间把 Stolon 相关的资源都放进去kubectl create namespace stolon-demo然后确认 StorageClass 可用kubectl get storageclass如果要用本地盘可能需要先创建一个 Local PV 或者用local-path这种 provisioner。我用的fast-ssd是云厂商提供的 SSD StorageClass确认它的allowVolumeExpansion是 true方便以后扩容。etcd 如果还没部署可以用 Bitnami 的 Chart 快速起一个helm repo add bitnami https://charts.bitnami.com/bitnami helm install etcd bitnami/etcd \ --namespace stolon-demo \ --set auth.rbac.enabledfalse \ --set replicaCount3测试环境可以关掉 RBAC生产环境建议开启并配置认证。4.2 Helm 安装与参数覆盖把前面准备好的 values 文件保存为stolon-values.yaml然后执行安装helm install stolon stolon/stolon \ --namespace stolon-demo \ --values stolon-values.yaml \ --version 0.3.2安装完成后用kubectl get pods -n stolon-demo查看 Pod 状态。正常情况下你会看到 3 个 keeper、3 个 sentinel、2 个 proxy以及 etcd 的 Pod。keeper 的启动会慢一些因为它要初始化 Postgres 数据目录。如果 keeper 一直处于Init或者CrashLoopBackOff先用kubectl logs看日志。常见的错误是 PVC 挂载失败或者 etcd 连接超时。4.3 用 stolonctl 初始化集群Helm 安装完成后集群可能还没有初始化。Stolon 需要一个初始的集群视图可以通过stolonctl init来创建。先进入一个 sentinel 或者 keeper 的 Podkubectl exec -it stolon-keeper-0 -n stolon-demo -- sh然后在容器里执行stolonctl --cluster-namestolon --store-backendetcd \ --etcd-endpointsetcd-0.etcd:2379,etcd-1.etcd:2379,etcd-2.etcd:2379 \ init--cluster-name要和 Helm values 里配置的一致默认是stolon。init命令会创建一个初始的集群规格然后 sentinel 会根据这个规格选出主库。初始化完成后用stolonctl status查看集群状态stolonctl --cluster-namestolon --store-backendetcd \ --etcd-endpointsetcd-0.etcd:2379,etcd-1.etcd:2379,etcd-2.etcd:2379 \ status输出里会显示当前的 master 是哪个 keeper以及各个 keeper 的健康状态。如果看到Master: stolon-keeper-0这样的信息说明集群已经正常工作了。4.4 连接测试与读写验证proxy 的 Service 在stolon-demo命名空间下名字是stolon-proxy。在集群内可以用这个 DNS 名称连接kubectl run pg-client --rm -it --imagepostgres:13 -- \ psql -h stolon-proxy.stolon-demo.svc.cluster.local -U postgres输入密码后如果能进入 psql 提示符说明连接成功。然后建个表、插条数据CREATE TABLE test (id serial primary key, name text); INSERT INTO test (name) VALUES (hello stolon); SELECT * FROM test;再开一个终端连到从库上看看数据有没有同步。从库的地址可以通过stolonctl status找到或者直接连 keeper 的 Pod IP。不过 Stolon 默认不允许直连从库需要在 keeper 的配置里开启hotStandby。提示生产环境不要用 postgres 超级用户跑业务应该创建一个单独的数据库和用户并限制权限。Stolon 初始化后你可以用超级用户登录然后执行CREATE DATABASE app; CREATE USER app_user WITH PASSWORD xxx; GRANT ALL ON DATABASE app TO app_user;。5. 故障切换测试与常见问题排查5.1 模拟主库宕机与自动切换高可用方案不测试切换等于没做。我一般会手动删掉 master 的 Pod观察集群多久能恢复写入。先找到当前的 masterstolonctl status | grep Master假设 master 是stolon-keeper-0直接删掉kubectl delete pod stolon-keeper-0 -n stolon-demo然后立刻用stolonctl status反复查看状态。正常情况下sentinel 会在几秒内检测到 master 失联然后从剩下的两个 keeper 中选出一个新的 master。新的 master 会接管写入proxy 也会更新路由。我实测下来从删除 Pod 到新 master 可写大概需要 10 到 15 秒。这个时间主要花在 sentinel 的故障检测和选主上。如果你觉得太慢可以调整 sentinel 的failInterval和maxFailures参数但不要调得太激进否则网络抖动就会触发误切换。切换完成后原来的stolon-keeper-0会被重新拉起作为从库加入集群并从新 master 同步数据。你可以用stolonctl status确认它的角色是standby。5.2 常见问题速查表下面这张表是我在实际部署和运维中遇到的高频问题按现象、原因、解决方法整理现象可能原因解决方法keeper Pod 一直 Pending节点资源不足或反亲和性太严格检查节点资源调整反亲和性为 preferredkeeper 启动后 CrashLoopBackOffPVC 挂载失败或 etcd 连接超时查看日志确认 StorageClass 和 etcd 地址sentinel 选不出 masteretcd 集群不健康或网络策略阻断检查 etcd 状态放行 2379 端口proxy 连接被拒绝proxy Pod 未就绪或 Service 配置错误检查 proxy 日志和 Endpoints数据不同步复制用户密码错误或 WAL 配置不当检查 replication 用户权限和wal_level切换后旧 master 无法重新加入数据目录损坏或时间线不一致手动清理旧 master 的 PVC让它重新初始化5.3 几个容易踩的坑与避坑技巧第一个坑是PVC 的回收策略。默认情况下删掉 Helm release 后 PVC 可能被保留也可能被删除取决于 StorageClass 的reclaimPolicy。如果你不小心删了 releasePVC 又被回收了数据就没了。我的做法是在 values 里给 PVC 加上helm.sh/resource-policy: keep注解这样helm delete不会删 PVC需要手动清理。第二个坑是Postgres 版本升级。Stolon 的镜像 tag 里带了 Postgres 版本如果你想从 pg13 升到 pg14不能直接改 tag 然后滚动更新因为数据目录的格式不兼容。正确的做法是新建一个集群用逻辑复制或者 pg_dump 迁移数据然后切流量。我试过直接改 tag结果 keeper 启动时报数据目录版本不匹配只能回滚。第三个坑是proxy 的连接数限制。proxy 本身不限制连接数但 Postgres 的max_connections默认是 100。如果你的应用连接池配置得比较大很容易打满。解决办法是在 Postgres 配置里调大max_connections或者在 proxy 前面加 PgBouncer。我一般会在 values 里通过postgresql.parameters覆盖这个值postgresql: parameters: max_connections: 500 shared_buffers: 1GB第四个坑是etcd 的磁盘 IO。etcd 对磁盘延迟非常敏感如果和 keeper 共用一块盘数据库的写入高峰会导致 etcd 响应变慢进而影响 sentinel 的选主。生产环境一定要把 etcd 独立部署并且用 SSD。6. 生产环境的调优与扩展建议6.1 Postgres 参数调优Stolon 默认的 Postgres 配置比较保守生产环境需要根据硬件调整。我一般会覆盖这几个参数shared_buffers设为内存的 25%比如 4G 内存的 keeper 设为 1GB。effective_cache_size设为内存的 50% 到 75%帮助查询规划器做决策。work_mem根据并发查询数调整一般 4MB 到 16MB。maintenance_work_mem设为 256MB 到 512MB加速 VACUUM 和索引创建。wal_level必须是replica或logical否则流复制无法工作。max_wal_senders至少等于 keeper 数量比如 3 个 keeper 设为 5。这些参数可以在 Helm values 的postgresql.parameters里覆盖Stolon 会在启动 Postgres 时把这些参数写进postgresql.conf。6.2 监控与告警接入Stolon 本身暴露了一些指标可以通过 Prometheus 采集。keeper 和 sentinel 的 Pod 上都有 metrics 端口默认是 8080。你可以在 values 里开启 ServiceMonitormetrics: enabled: true serviceMonitor: enabled: true然后 Prometheus 就能自动发现并采集这些指标。关键指标包括stolon_keeper_is_master当前 keeper 是否是 master、stolon_sentinel_master_epoch选主轮次、stolon_proxy_connectionsproxy 连接数。告警规则我一般会配这几条master 数量不等于 1 时告警、keeper 数量少于预期时告警、proxy 连接数突增时告警。这些能帮你提前发现集群异常。6.3 水平扩展与只读副本Stolon 支持动态增加 keeper。你只需要改 values 里的keeper.replicaCount然后helm upgrade新的 keeper Pod 会自动加入集群并作为从库同步数据。不过要注意新 keeper 加入后会从 master 拉取全量数据如果数据量很大这个过程可能持续很久期间会占用大量网络带宽。如果你需要只读副本分担读流量可以在 proxy 层面做读写分离或者让应用直接连从库的 keeper。Stolon 的 keeper 默认不允许直连从库需要在配置里开启hotStandby并设置primaryConnInfo。这部分配置稍微复杂一些建议先在测试环境验证。提示水平扩展 keeper 时确保 etcd 的存储容量足够。每个 keeper 的状态信息都会写到 etcd 里keeper 越多etcd 的数据量越大。如果 etcd 磁盘满了整个集群都会挂掉。6.4 备份策略的配合Stolon 本身不做备份它只负责高可用。备份需要你另外配置比如用pg_dump做逻辑备份或者用pg_basebackup做物理备份。我一般会在 keeper 的 Pod 里挂一个 sidecar定时执行pg_dump并把备份文件上传到对象存储。物理备份更适合大数据量但恢复起来麻烦一些。逻辑备份简单直接但备份和恢复速度慢。我的建议是核心业务用物理备份加 WAL 归档做到时间点恢复非核心业务用逻辑备份每天一次就够了。备份的验证也很重要。我见过太多人配了备份但从来没恢复过真出事了才发现备份文件是坏的。定期做恢复演练确保备份可用这比什么高可用方案都实在。7. 个人实操体会与几个小技巧这套 Stolon 集群我在几个内部项目里跑了大半年整体稳定性不错切换逻辑也可靠。但有几个点是我踩过坑之后才明白的分享出来能帮你少走弯路。第一个体会是etcd 的稳定性直接决定 Stolon 的稳定性。我早期图省事把 etcd 和 keeper 部署在同一批节点上结果数据库写入高峰时 etcd 的 leader 频繁切换sentinel 跟着抽风集群状态一直不稳定。后来把 etcd 独立到三台低配但磁盘好的节点上问题就消失了。所以如果你要认真跑 Stolonetcd 必须独立部署且用 SSD。第二个体会是不要频繁手动切换。Stolon 的自动切换已经够用了除非你在做维护否则不要没事去stolonctl failkeeper。手动切换会触发一次完整的选主流程期间写入会短暂中断。我有一次为了测试连续切换了三次结果 proxy 的连接池里积累了大量半开连接应用侧报了一堆超时。第三个技巧是用stolonctl status --formatjson做自动化巡检。这个命令输出的 JSON 里包含了集群的完整状态你可以写个脚本定时采集推送到监控系统。比 Prometheus 的指标更直观能看到每个 keeper 的角色、复制延迟、健康状态。最后再分享一个小技巧如果你在测试环境频繁创建和删除 Stolon 集群PVC 会残留一堆。可以在 StorageClass 里设置reclaimPolicy: Delete这样删掉 PVC 后底层存储也会自动清理。但生产环境千万别这么干一定要用Retain防止误删。