资讯动态

K8S 部署 openGauss 一主两备:StatefulSet 与流复制实战

发布时间:2026/10/21 13:24:44 来源:尧图企业网站定制
1. 为什么要在 K8S 上跑一主两备的 openGauss把 openGauss 塞进 K8S一开始我也觉得是自找麻烦。数据库这种对 IO、内存、网络延迟都极其敏感的东西传统做法都是物理机或者虚拟机独占部署谁愿意给它套一层容器编排的壳子。但真到了多环境交付、快速扩缩容、统一运维的阶段你会发现 K8S 带来的声明式管理和自愈能力确实能省掉大量重复劳动。尤其是需要频繁搭建测试环境、做高可用演练的场景一套 YAML 就能拉起一主两备的集群比手工装三台机器再配流复制要舒服得多。这篇内容聊的就是怎么在 K8S 集群里搭出一主两备的 openGauss。核心关键词很明确K8S、openGauss、一主两备、StatefulSet、流复制、高可用。它解决的是数据库容器化部署 主备自动复制这个组合问题适合已经对 K8S 有基本了解、又想尝试把关系型数据库跑在容器里的运维和开发人员。如果你连 Pod、Service、PVC 是什么都还没搞明白建议先把 K8S 基础过一遍再回来不然下面的内容会有点吃力。先说清楚一个前提openGauss 官方并没有像 MySQL Operator 那样成熟的 K8S Operator 方案社区里能查到的资料也比较零散。所以我们的思路是用 K8S 的原生能力去模拟传统主备部署——用 StatefulSet 保证 Pod 的稳定网络标识和存储用 headless Service 做 DNS 发现用初始化脚本完成主备角色分配和流复制配置。这套方案不是生产级的终极形态但足够稳定、足够透明你能清楚知道每一步在干什么出问题也好排查。我踩过的第一个坑就是很多人以为把 openGauss 的 Docker 镜像跑起来就完事了结果发现备节点根本连不上主节点或者连上了但复制状态一直是 disconnected。问题往往出在网络标识和复制槽配置上。下面我会把这些细节一个个拆开讲。2. 整体架构设计与核心思路拆解2.1 为什么选 StatefulSet 而不是 Deployment数据库是有状态应用的典型代表每个实例都需要稳定的网络标识、独立的持久化存储、有序的启动和终止顺序。Deployment 管的是无状态副本Pod 名字随机、IP 随机、存储共享这些特性对数据库来说全是灾难。StatefulSet 则刚好反过来Pod 名字固定比如opengauss-0、opengauss-1、opengauss-2每个 Pod 绑定自己的 PVC启动顺序严格按序号来。一主两备的场景下我习惯把opengauss-0定为主节点opengauss-1和opengauss-2作为备节点。这样角色分配是确定性的不需要额外引入选举逻辑。StatefulSet 的podManagementPolicy建议设为OrderedReady保证主节点先起来、备节点再依次启动避免备节点启动时找不到主节点而反复重试。注意StatefulSet 的稳定网络标识依赖 headless Service。如果你只建了普通 ClusterIP ServicePod 之间是没法通过opengauss-0.opengauss-headless这种域名互相访问的流复制配置会直接失败。2.2 存储方案的选择local PV 还是网络存储openGauss 对磁盘 IO 的要求不低尤其是 WAL 写入。在 K8S 里存储选项大致分两类本地存储local PV、hostPath和网络存储NFS、Ceph、云盘。本地存储性能好但 Pod 漂移到别的节点后数据就丢了网络存储可以跨节点漂移但延迟和吞吐往往打折扣。我的建议是如果是测试环境或者对性能要求不极端的场景用 NFS 或云厂商的块存储就够配置简单、可漂移。如果是性能敏感的生产场景优先考虑本地 SSD 加 nodeAffinity把 Pod 固定在有数据的节点上牺牲一部分调度灵活性换性能。下面示例里我用的是 StorageClass 动态供给的方式你可以根据实际环境替换成自己的存储方案。2.3 主备复制的实现路径openGauss 的主备复制基于 WAL 日志传输和 PostgreSQL 的流复制思路一脉相承。核心步骤就三步主节点开启归档和复制权限、备节点用gs_basebackup从主节点拉一份基础备份、备节点配置recovery.conf或等效参数指向主节点并启动复制。在 K8S 里这三步的难点在于备节点怎么知道主节点是谁以及基础备份怎么在容器里完成。我的做法是写一个初始化脚本通过环境变量和 DNS 解析拿到主节点地址判断当前 Pod 序号序号为 0 的走主节点初始化流程其余走备节点流程。备节点启动时先检测数据目录是否为空为空就执行基础备份不为空就直接启动并进入复制状态。这套逻辑听起来简单但实际写脚本时要注意幂等性——Pod 重启后脚本会再跑一遍不能因为重复执行把已有数据覆盖掉。我一般用数据目录是否存在postgresql.conf作为判断依据存在就跳过初始化。3. 核心细节解析与实操要点3.1 镜像准备与基础环境openGauss 官方提供了 Docker 镜像但版本更新不算频繁而且默认配置未必适合 K8S 环境。我一般会基于官方镜像做一层薄封装主要做三件事调整shared_buffers、max_connections等参数适配容器资源限制预置初始化脚本设置合适的时区和字符集。镜像里需要包含gs_initdb、gs_basebackup、gs_ctl这些工具官方镜像都有。另外要注意容器内不能以 root 用户跑 openGauss官方镜像默认用的是omm用户UID 一般是 1000 左右。如果你用 PVC 挂载数据目录要确保目录权限对omm用户可写否则初始化会报权限错误。FROM opengauss/opengauss:5.0.0 USER root COPY init-scripts/ /opt/opengauss/init-scripts/ RUN chmod x /opt/opengauss/init-scripts/*.sh \ chown -R omm:omm /opt/opengauss/init-scripts USER omm提示PVC 挂载后目录属主可能变成 root导致 omm 用户无法写入。可以在 initContainer 里用 root 身份执行chown修正权限这是很常见的一个坑。3.2 Headless Service 与 DNS 发现StatefulSet 要配合 headless Service 使用clusterIP: None是必须的。这样每个 Pod 会得到一条 DNS 记录格式是pod-name.service-name.namespace.svc.cluster.local。主备复制就靠这个域名来定位。apiVersion: v1 kind: Service metadata: name: opengauss-headless namespace: opengauss spec: clusterIP: None selector: app: opengauss ports: - name: db port: 5432 targetPort: 5432 - name: repl port: 5433 targetPort: 5433这里我额外暴露了一个 5433 端口专门用于复制。openGauss 默认的复制端口和数据库端口可以分开配置分开的好处是网络策略可以更精细地控制——只允许备节点访问主节点的复制端口减少暴露面。3.3 主备角色判定逻辑角色判定是整个方案的核心。我的做法是在 StatefulSet 的容器启动命令里调用一个包装脚本脚本里通过hostname拿到 Pod 名字解析出序号然后决定走哪条分支。#!/bin/bash set -e POD_NAME$(hostname) POD_INDEX${POD_NAME##*-} PRIMARY_HOSTopengauss-0.opengauss-headless.opengauss.svc.cluster.local DATA_DIR/var/lib/opengauss/data if [ $POD_INDEX 0 ]; then echo 当前节点是主节点执行主节点初始化 if [ ! -f $DATA_DIR/postgresql.conf ]; then gs_initdb -D $DATA_DIR --nodenameprimary -w OpenGauss123 fi exec gs_ctl start -D $DATA_DIR -M primary else echo 当前节点是备节点执行备节点初始化 if [ ! -f $DATA_DIR/postgresql.conf ]; then gs_basebackup -D $DATA_DIR -h $PRIMARY_HOST -p 5433 -U repl_user -W Repl123 fi exec gs_ctl start -D $DATA_DIR -M standby fi这段脚本的关键点在于主节点只在数据目录为空时初始化备节点只在数据目录为空时做基础备份。这样 Pod 重启不会破坏已有数据。gs_ctl start -M standby会读取数据目录里的复制配置自动进入备节点模式。注意gs_basebackup需要主节点已经创建好复制用户并配置了pg_hba.conf允许备节点连接。如果主节点还没准备好备节点会一直重试失败。所以 StatefulSet 的启动顺序很重要OrderedReady策略能保证主节点先就绪。4. 实操过程与核心环节实现4.1 命名空间与配置准备先把命名空间和 ConfigMap 建好。ConfigMap 里放 openGauss 的基础配置模板比如postgresql.conf的覆盖项、pg_hba.conf的复制规则。这样镜像本身不用改配置通过挂载注入灵活很多。apiVersion: v1 kind: Namespace metadata: name: opengauss --- apiVersion: v1 kind: ConfigMap metadata: name: opengauss-config namespace: opengauss data: postgresql.conf.override: | listen_addresses * port 5432 max_connections 200 shared_buffers 512MB wal_level hot_standby max_wal_senders 4 hot_standby on pg_hba.conf.override: | host all all 0.0.0.0/0 md5 host replication repl_user 0.0.0.0/0 md5wal_level必须设为hot_standby或更高否则备节点无法提供只读查询。max_wal_senders要大于备节点数量一主两备至少设 3我一般留点余量设 4。4.2 StatefulSet 完整定义StatefulSet 是整个部署的主体。三个副本每个副本一个 PVC通过 volumeClaimTemplates 自动创建。资源限制要根据实际机器配置来openGauss 比较吃内存shared_buffers加上连接开销2GB 内存起步比较稳妥。apiVersion: apps/v1 kind: StatefulSet metadata: name: opengauss namespace: opengauss spec: serviceName: opengauss-headless replicas: 3 podManagementPolicy: OrderedReady selector: matchLabels: app: opengauss template: metadata: labels: app: opengauss spec: initContainers: - name: fix-permission image: busybox command: [sh, -c, chown -R 1000:1000 /var/lib/opengauss/data] volumeMounts: - name: data mountPath: /var/lib/opengauss/data containers: - name: opengauss image: my-registry/opengauss-custom:5.0.0 command: [/opt/opengauss/init-scripts/entrypoint.sh] ports: - containerPort: 5432 - containerPort: 5433 env: - name: GS_PASSWORD valueFrom: secretKeyRef: name: opengauss-secret key: password resources: requests: memory: 2Gi cpu: 1 limits: memory: 4Gi cpu: 2 volumeMounts: - name: data mountPath: /var/lib/opengauss/data - name: config mountPath: /opt/opengauss/config volumes: - name: config configMap: name: opengauss-config volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] storageClassName: standard resources: requests: storage: 20GiinitContainer里用 busybox 修正目录权限这一步很关键。很多 PVC 动态供给出来的目录属主是 rootomm 用户写不进去初始化直接失败。用 initContainer 以 root 身份 chown 一下问题就解决了。4.3 主节点复制用户与权限配置主节点初始化完成后需要创建复制用户并配置pg_hba.conf。这一步我放在主节点的 entrypoint 脚本里初始化完成后自动执行。if [ $POD_INDEX 0 ]; then gs_ctl start -D $DATA_DIR -M primary sleep 10 gsql -d postgres -c CREATE USER repl_user WITH REPLICATION PASSWORD Repl123; gsql -d postgres -c SELECT * FROM pg_create_physical_replication_slot(standby1_slot); gsql -d postgres -c SELECT * FROM pg_create_physical_replication_slot(standby2_slot); fi复制槽的作用是防止主节点过早清理备节点还没收到的 WAL 日志。一主两备就建两个槽分别对应两个备节点。如果不建槽主节点 WAL 被回收后备节点可能永远追不上复制直接断掉。提示复制槽名字要和备节点配置里的primary_slot_name对应上否则备节点启动时会报找不到槽的错误。4.4 备节点基础备份与复制启动备节点的流程是检测数据目录为空 → 执行gs_basebackup→ 写入复制配置 → 启动。gs_basebackup会从主节点拉取完整的数据目录包括配置文件所以拉完之后只需要追加复制相关参数即可。gs_basebackup -D $DATA_DIR \ -h $PRIMARY_HOST \ -p 5433 \ -U repl_user \ -W Repl123 \ -X stream \ -R cat $DATA_DIR/postgresql.conf EOF primary_conninfo host$PRIMARY_HOST port5433 userrepl_user passwordRepl123 primary_slot_name standby${POD_INDEX}_slot EOF-R参数会自动生成recovery.conf或等效的恢复配置省去手工写的麻烦。-X stream表示备份期间同时流式接收 WAL保证备份一致性。4.5 验证主备状态三个 Pod 都起来之后连到主节点查复制状态。openGauss 用pg_stat_replication视图看备节点连接情况。SELECT client_addr, state, sync_state, sent_lsn, replay_lsn FROM pg_stat_replication;正常的话应该看到两行记录state是Streamingsync_state是Async异步复制或Sync同步复制。如果只有一行或者一行都没有说明某个备节点没连上需要去备节点看日志。备节点上可以用gs_ctl query查看自己的角色和复制延迟gs_ctl query -D /var/lib/opengauss/data输出里会显示local_role是Standbysender_replay_lsn和主节点的 LSN 差距就是复制延迟。5. 常见问题与排查技巧实录5.1 备节点一直连不上主节点这是最常见的问题排查顺序我一般是这样先看备节点日志里报什么错是 DNS 解析失败、连接被拒、还是认证失败。DNS 解析失败通常是 headless Service 没建对或者 namespace 写错了。连接被拒多半是主节点的pg_hba.conf没放行备节点网段。认证失败就是密码或用户名不对。现象可能原因排查方法备节点日志报could not translate host nameheadless Service 缺失或名称错误kubectl get svc -n opengauss确认报connection refused主节点未就绪或端口未监听主节点gs_ctl query确认状态报password authentication failed复制用户密码错误检查 Secret 和primary_conninfo报replication slot does not exist复制槽未创建主节点查pg_replication_slots5.2 复制延迟越来越大复制延迟大通常是网络带宽不够或者主节点写入压力太大。先看备节点的replay_lsn和主节点sent_lsn的差距如果差距持续扩大说明备节点回放速度跟不上。可以适当调大wal_receiver_buffer_size或者检查备节点的磁盘 IO 是否成为瓶颈。另一个容易被忽略的点是max_wal_senders设太小导致备节点连接被挤掉。一主两备至少设 3我一般设 4 留余量。如果还有级联备节点要相应增加。5.3 Pod 重启后数据丢失如果 PVC 用的是emptyDir或者没配volumeClaimTemplatesPod 重启后数据目录就清空了备节点会重新做基础备份主节点数据也没了。这是配置错误不是 bug。检查 StatefulSet 里有没有volumeClaimTemplatesPVC 有没有正常绑定。还有一种情况是 PVC 绑定了但storageClassName写错PVC 一直 PendingPod 起不来。kubectl describe pvc能看到具体原因。5.4 主备切换怎么做这套方案没有内置自动故障转移主节点挂了需要人工介入。手动切换的步骤是确认主节点确实不可用 → 选一个备节点执行gs_ctl promote提升为主 → 修改 Service 或 DNS 指向新主节点 → 原主节点恢复后作为备节点重新加入。自动切换需要引入额外的仲裁组件比如 etcd 或者 K8S 的 Lease 机制复杂度会高不少。如果对可用性要求极高建议直接上 openGauss 的企业版高可用方案或者用 Patroni 这类通用工具做编排。容器化环境下做数据库自动故障转移坑比想象中多没有充分测试不要轻易上生产。注意gs_ctl promote之后原主节点如果恢复不能直接启动需要重建数据目录或者用gs_rewind回滚否则会出现双主或者数据冲突。5.5 资源限制导致的 OOMopenGauss 默认的shared_buffers可能偏大容器内存限制设小了会被 OOM Killer 干掉。建议shared_buffers设为容器内存限制的 25% 左右max_connections也不要设太大每个连接都有内存开销。如果 Pod 频繁重启且kubectl describe pod显示OOMKilled就是内存不够调大 limit 或者调小shared_buffers。6. 几个实操心得与后续扩展方向这套方案我在测试环境跑了大半年稳定性还可以但有几个点值得再强调一下。第一初始化脚本的幂等性一定要做好Pod 重启是常态脚本重复执行不能出问题。第二复制槽一定要建不然 WAL 被回收后复制断了很难恢复。第三资源限制要留余量数据库不像无状态服务内存给少了直接崩。后续如果想做得更完善可以考虑几个方向引入 Operator 把部署和运维逻辑封装起来减少手工 YAML 的维护成本用 K8S 的 PodDisruptionBudget 保证滚动更新时至少有一个备节点在线把备份任务做成 CronJob定期做基础备份和 WAL 归档。这些都是在现有基础上逐步叠加的不用一次性全上。最后分享一个小技巧调试阶段可以把备节点的 entrypoint 脚本里加上sleep infinity让容器起来后不自动启动数据库方便你进去手工执行命令排查问题。等配置调通了再去掉比反复看日志猜问题要高效得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑