1. 为什么要在 K8S 上跑一主两备的 openGauss
把 openGauss 塞进 K8S 里,还要做成一主两备的架构,这件事听起来像是把一头大象装进冰箱——步骤不多,但每一步都容易出岔子。我最初动这个念头,是因为手头有一套业务需要国产数据库,同时又希望借助 K8S 的调度能力做故障自愈和资源隔离。裸机部署 openGauss 当然更直接,但一旦机器宕机,恢复流程全靠人肉,运维成本压不下来。K8S 的 StatefulSet 天然适合有状态服务,稳定的网络标识、有序的启停、独立的存储卷,这些特性正好对上数据库主备的胃口。
openGauss 本身是华为开源的关系型数据库,内核基于 PostgreSQL 做了大量企业级增强,比如全密态计算、行列混合存储、AI 自治能力。它原生支持主备同步和级联备,通过流复制实现数据同步。一主两备的意思是:一个主节点负责读写,两个备节点通过 WAL 日志实时同步,主节点挂掉之后可以切换到备节点继续服务。这种架构在金融、政务场景里很常见,核心诉求就是高可用和数据不丢。
那为什么非要上 K8S?直接物理机部署不行吗?行,但有几个现实问题。第一,资源利用率低,数据库机器通常配置很高,但业务低谷期资源闲置严重。第二,扩缩容麻烦,加一个备节点要重新装系统、配环境、调参数。第三,故障恢复慢,主节点宕机后,人工介入切换的时间可能长达十几分钟。K8S 能把这些事情标准化:用 StatefulSet 管理 Pod 生命周期,用 Headless Service 提供稳定 DNS,用 PVC 挂载持久化存储,用 Operator 或者自定义脚本做集群管理。说白了,就是把数据库的运维动作变成声明式的 YAML,让集群自己朝着期望状态收敛。
不过,K8S 跑数据库并不是没有代价。网络延迟、存储 IO、Pod 漂移带来的 IP 变化,这些都是坑。尤其是 openGauss 对网络和磁盘的敏感度比普通应用高得多,主备同步一旦延迟过大,备节点就会落后,严重时会导致切换后数据不一致。所以这篇文章不会只给你一堆 YAML 就完事,我会把每个配置背后的原因讲清楚,把我在实测中踩过的坑摊开来说,让你少走弯路。
适合谁看?如果你已经对 K8S 的基本概念(Pod、Service、PVC、StatefulSet)有了解,同时想在生产或者准生产环境里跑 openGauss 主备集群,那这篇内容就是为你准备的。如果你完全没碰过 K8S,建议先补一下基础,否则后面很多操作你会知其然不知其所以然。
2. 动手之前的家底盘点:环境、存储与网络的前置决策
2.1 K8S 集群的最低配置与版本选择
openGauss 对资源的要求不算低,官方建议单节点至少 4 核 8G,但那是裸机跑单实例的底线。在 K8S 里跑一主两备,每个 Pod 至少给 2 核 4G,三个 Pod 加起来就是 6 核 12G,这还没算 K8S 系统组件和监控的消耗。我实测下来,如果要做性能压测,单 Pod 给到 4 核 8G 会更稳,尤其是主节点,WAL 发送和查询处理都吃 CPU。
K8S 版本方面,建议用 1.20 以上的稳定版。低版本对 StatefulSet 的滚动更新和 PVC 扩容支持不够完善,而且一些 CSI 插件的兼容性也差。我用的是 1.24 版本,实测没问题。容器运行时用 containerd 或者 Docker 都行,但要注意 openGauss 官方镜像对 cgroup 版本有要求,如果宿主机是 cgroup v2,可能需要调整内核参数,否则容器启动会报错。
节点数量上,至少三个工作节点,这样每个 openGauss Pod 可以调度到不同节点,避免单节点故障导致整个集群挂掉。如果只有两个节点,那就得接受两个 Pod 跑在同一台机器上的风险,一旦这台机器宕机,主备同时丢失,数据就危险了。所以我的建议是:三个工作节点起步,每个节点至少 8 核 16G,磁盘用 SSD,网络万兆起步。
2.2 存储方案选型:Local PV 还是网络存储
这是整个部署里最关键的一个决策。openGauss 对磁盘 IO 的敏感度极高,主备同步写 WAL 日志时,如果存储延迟高,备节点就会持续落后。我试过用 NFS 做后端存储,结果主备同步延迟在业务高峰期飙到十几秒,备节点几乎处于不可用状态。后来换成 Local PV,延迟直接降到毫秒级。
Local PV 的意思是直接用宿主机上的本地磁盘,通过 PV 和 PVC 绑定,Pod 调度到哪个节点,就用哪个节点的盘。优点是性能好、延迟低,缺点是 Pod 不能随意漂移,一旦节点故障,数据就留在那台机器上,需要人工干预才能恢复。对于数据库这种有状态服务,这个取舍是值得的,因为性能比灵活性更重要。
如果你非要用网络存储,那至少得是 Ceph RBD 或者商业存储,NFS 绝对不行。而且要注意,网络存储的 IOPS 和吞吐量必须满足 openGauss 的要求,否则主备同步会成为瓶颈。我个人的建议是:生产环境用 Local PV,测试环境可以用网络存储凑合,但一定要做压测验证。
存储容量方面,主节点和备节点的数据目录至少给 100G,因为 openGauss 的 WAL 日志、数据文件、临时文件都放在一起。如果业务数据量大,还得预留更多空间。PVC 的访问模式用 ReadWriteOnce,因为 Local PV 不支持多节点挂载。
2.3 网络模型:Headless Service 与稳定 DNS
K8S 里 Pod 的 IP 是动态的,重启就会变。但 openGauss 主备之间需要互相知道对方的地址,所以必须用 Headless Service 给每个 Pod 一个稳定的 DNS 名称。StatefulSet 创建的 Pod 命名规则是<statefulset-name>-<ordinal>,比如opengauss-0、opengauss-1、opengauss-2。配合 Headless Service,每个 Pod 的 DNS 就是<pod-name>.<service-name>.<namespace>.svc.cluster.local。
这个 DNS 名称在 openGauss 的配置里会用到,比如主节点的replconninfo参数需要填写备节点的地址。如果用 IP,Pod 重启后就失效了,所以必须用 DNS。另外,客户端连接数据库时,也应该通过 Service 的 ClusterIP 或者 NodePort 来访问,而不是直接连 Pod IP。
网络策略方面,如果 K8S 集群启用了 NetworkPolicy,需要放行 openGauss 默认端口 5432 和主备同步端口。openGauss 的主备同步端口默认是 5432 加上一个偏移量,具体取决于配置。我一般会显式指定replconninfo里的端口,避免混淆。
3. 镜像与配置文件的准备:把 openGauss 装进容器
3.1 基础镜像的选择与定制
openGauss 官方并没有提供官方的 Docker 镜像,社区里有一些第三方镜像,但版本参差不齐,安全性和稳定性没法保证。我建议自己构建镜像,基于 openEuler 或者 CentOS 的基础镜像,手动安装 openGauss。这样你能控制每一个细节,也方便后续升级和打补丁。
构建镜像的 Dockerfile 大致长这样:先装依赖包,比如libaio、readline、zlib,然后创建omm用户和用户组,因为 openGauss 不允许用 root 运行。接着下载 openGauss 的安装包,解压到/opt/software/openGauss,用gs_initdb初始化数据库实例。最后把启动脚本和配置文件复制进去,设置好权限。
这里有个坑:openGauss 的安装包对操作系统版本有要求,比如 openEuler 20.03 和 CentOS 7.6 是官方支持的,其他版本可能会报错。我试过在 Ubuntu 上装,结果依赖库版本不匹配,折腾了很久。所以基础镜像最好选官方支持的操作系统。
另一个坑是容器里的ulimit设置。openGauss 对文件描述符和进程数有要求,默认的容器限制可能不够。需要在 Dockerfile 里或者 K8S 的 Pod spec 里设置ulimits,比如nofile至少 65536,nproc至少 65536。否则数据库启动时会报“too many open files”。
3.2 配置文件的关键参数拆解
openGauss 的配置文件主要有两个:postgresql.conf和pg_hba.conf。前者控制数据库的运行参数,后者控制客户端认证。在主备架构里,还有几个参数必须特别注意。
replconninfo是主备同步的核心参数,格式是replconninfo1 = 'localhost=<本地地址> localport=<本地端口> remotehost=<对端地址> remoteport=<对端端口>'。主节点需要配置两个replconninfo,分别指向两个备节点;备节点只需要配置一个,指向主节点。地址用 Pod 的 DNS 名称,端口用 5432 加上偏移量,比如 5433、5434。
wal_level必须设置为hot_standby或者更高,否则备节点无法接收 WAL 日志。max_wal_senders要大于备节点数量,一般设成 4 或 8。hot_standby设为on,允许备节点提供只读查询。synchronous_standby_names控制同步复制的备节点列表,如果要求数据零丢失,可以设置成'*'或者具体备节点名称,但这样主节点会等待备节点确认,写入延迟会增加。我一般设成'any 1 (*)',表示至少一个备节点确认即可,兼顾性能和数据安全。
pg_hba.conf里要放行备节点和客户端的连接。比如host all all <备节点网段> trust,host all all 0.0.0.0/0 md5。注意,trust表示无密码信任,只适合内网可信环境,生产环境建议用md5或者sha256。
3.3 初始化脚本与启动逻辑
容器启动时,需要判断当前 Pod 是主节点还是备节点,然后执行不同的初始化逻辑。我的做法是在启动脚本里读取环境变量POD_NAME,如果POD_NAME是opengauss-0,就初始化为主节点;如果是opengauss-1或opengauss-2,就初始化为备节点。
主节点的初始化包括:用gs_initdb创建数据目录,启动数据库,创建复制用户,配置replconninfo,然后重启生效。备节点的初始化包括:用gs_basebackup从主节点拉取基础备份,配置replconninfo指向主节点,启动数据库,进入流复制模式。
这里有个细节:gs_basebackup需要在备节点上执行,但备节点此时还没有数据目录,所以要先创建一个空目录,然后用gs_basebackup -D <数据目录> -h <主节点地址> -p <主节点端口> -U <复制用户>拉取备份。拉取完成后,备节点会自动进入恢复模式,等待主节点发送 WAL 日志。
启动脚本里还要处理一个情况:如果 Pod 重启,数据目录已经存在,就不能再执行gs_initdb或gs_basebackup,否则会覆盖数据。所以脚本里要判断数据目录是否为空,如果非空,直接启动数据库即可。
4. 用 StatefulSet 编排一主两备:YAML 里的每一个决策
4.1 StatefulSet 的 Pod 管理模板设计
StatefulSet 是 K8S 里管理有状态服务的标准方式,它保证 Pod 的名称、网络标识、存储卷都是稳定的。对于 openGauss 一主两备,我定义了一个名为opengauss的 StatefulSet,replicas设为 3,serviceName指向一个 Headless Service。
Pod 模板里,容器镜像用我们之前构建的 openGauss 镜像,容器端口暴露 5432 和 5433、5434。环境变量里传入POD_NAME,用fieldRef引用metadata.name。资源限制方面,requests和limits都设为 4 核 8G,避免资源争抢。volumeMounts把 PVC 挂载到/var/lib/opengauss/data,这是 openGauss 的数据目录。
volumeClaimTemplates定义 PVC 模板,每个 Pod 会自动创建一个独立的 PVC,命名规则是<volume-name>-<pod-name>。存储类用local-storage,访问模式ReadWriteOnce,容量 100G。这样每个 Pod 都有自己独立的存储卷,互不干扰。
podManagementPolicy设为OrderedReady,保证 Pod 按顺序启动和停止。updateStrategy设为RollingUpdate,但要注意,数据库的滚动更新风险很高,因为主备版本必须一致。我一般会先手动切换主备,再逐个更新 Pod,避免自动滚动更新导致集群不可用。
4.2 Headless Service 与客户端访问入口
Headless Service 的clusterIP设为None,这样 K8S 不会给它分配 ClusterIP,而是直接返回 Pod 的 IP。Service 的selector匹配 StatefulSet 的 Pod 标签,ports定义 5432 端口。这样每个 Pod 的 DNS 就是<pod-name>.<service-name>.<namespace>.svc.cluster.local。
客户端访问数据库时,不能直接连 Headless Service,因为它返回的是所有 Pod 的 IP,客户端不知道哪个是主节点。所以还需要一个普通的 Service,selector只匹配主节点的标签。但主节点可能会切换,标签也会变,所以这个 Service 的selector需要动态更新。我的做法是用一个 Sidecar 容器或者外部脚本监控主备状态,一旦主备切换,就更新 Service 的selector,把流量指向新的主节点。
另一种做法是用NodePort或者LoadBalancer暴露服务,客户端通过外部地址访问。但这样需要额外的负载均衡器,而且主备切换时,负载均衡器的后端也要更新。我一般用ClusterIP加内部 DNS 的方式,客户端通过一个固定的域名访问,域名解析到当前主节点的 Pod IP。这个域名可以用 K8S 的ExternalNameService 或者自定义 DNS 来实现。
4.3 主备角色的自动识别与切换逻辑
openGauss 本身没有自动故障切换的能力,需要外部工具或者脚本介入。我写了一个简单的 Sidecar 容器,定期检查主节点是否存活。检查方式是连接主节点的 5432 端口,执行SELECT pg_is_in_recovery(),如果返回f,说明是主节点;如果返回t,说明是备节点。如果主节点连接失败,Sidecar 就触发切换流程。
切换流程包括:选择一个备节点,提升为主节点,执行gs_ctl promote;更新其他备节点的replconninfo,指向新的主节点;更新 Service 的selector,把流量切到新的主节点。这个过程需要保证原子性,否则会出现脑裂。我一般用 K8S 的Lease对象做分布式锁,确保只有一个 Sidecar 能执行切换。
这里有个坑:gs_ctl promote执行后,备节点会变成主节点,但原来的主节点如果恢复了,会变成备节点,需要重新配置replconninfo。所以切换脚本里要处理这个情况,避免两个主节点同时存在。
5. 部署后的验证与踩坑实录
5.1 主备同步状态检查与延迟监控
部署完成后,第一件事是检查主备同步状态。在主节点上执行SELECT * FROM pg_stat_replication;,可以看到备节点的连接信息、同步状态、延迟字节数。state应该是streaming,sync_state应该是sync或者async。如果state是startup或者catchup,说明备节点还在追赶,需要等一会儿。
延迟监控可以用pg_stat_replication里的write_lag、flush_lag、replay_lag字段,单位是毫秒。如果延迟持续超过 1 秒,就要检查网络和存储。我遇到过因为 NFS 存储延迟高,导致replay_lag飙到 10 秒的情况,换成 Local PV 后降到 10 毫秒以内。
另外,备节点上执行SELECT pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();,对比两个 LSN 的差值,也能看出延迟。如果差值很大,说明备节点接收了 WAL 但还没应用,可能是磁盘 IO 瓶颈。
5.2 常见启动失败原因与排查路径
openGauss 容器启动失败的原因很多,我列几个最常见的。
第一个是权限问题。openGauss 不允许用 root 运行,但容器默认是 root 用户。需要在 Dockerfile 里创建omm用户,用USER omm切换。如果忘了这一步,启动时会报“cannot be run as root”。
第二个是数据目录权限不对。omm用户需要对数据目录有读写权限,但 PVC 挂载后,目录的属主可能是 root。需要在启动脚本里执行chown -R omm:omm /var/lib/opengauss/data,或者用initContainer提前改好权限。
第三个是ulimit限制。如果nofile太小,数据库启动时会报“too many open files”。需要在 Pod spec 里设置securityContext或者ulimits。
第四个是replconninfo配置错误。如果主备的地址或端口写错了,备节点会一直连不上主节点,日志里会报“could not connect to server”。检查 DNS 解析是否正常,端口是否放行。
第五个是存储卷挂载失败。如果 PVC 没有绑定成功,Pod 会一直处于Pending状态。用kubectl describe pod查看事件,如果是no persistent volumes available,说明存储类配置有问题。
5.3 性能调优:从参数到内核的逐层优化
openGauss 在 K8S 里的性能调优,分几个层面。
参数层面,shared_buffers设成内存的 25%,work_mem设成 4MB 到 16MB,maintenance_work_mem设成 64MB 到 256MB。wal_buffers设成 16MB,checkpoint_completion_target设成 0.9,max_wal_size设成 4GB。这些参数在裸机上和容器里差别不大,但容器内存限制要留足余量,否则 OOM Killer 会杀掉数据库进程。
内核层面,宿主机的vm.swappiness设成 0,避免内存交换。vm.dirty_ratio和vm.dirty_background_ratio调低,减少脏页堆积。网络方面,net.core.somaxconn调大,net.ipv4.tcp_max_syn_backlog调大,避免连接队列溢出。
K8S 层面,给数据库 Pod 设置GuaranteedQoS,即requests和limits相等,避免被其他 Pod 挤占资源。CPU manager策略设为static,给数据库 Pod 分配独占 CPU 核心。Topology Manager策略设为single-numa-node,保证 CPU 和内存在同一 NUMA 节点,减少跨节点访问延迟。
存储层面,用 Local PV 加 SSD,mountOptions里加上noatime,减少文件系统元数据写入。如果用的是 NVMe 盘,可以开启io scheduler的none模式,降低 IO 延迟。
6. 日常运维:备份、扩容与版本升级的实操心得
6.1 逻辑备份与物理备份的取舍
openGauss 支持逻辑备份和物理备份。逻辑备份用gs_dump,导出 SQL 文件,适合小数据量或者跨版本迁移。物理备份用gs_basebackup,直接复制数据目录,适合大数据量或者快速恢复。在主备架构里,我一般用物理备份,因为备节点本身就是主节点的一个实时副本,可以直接从备节点做备份,不影响主节点性能。
备份策略上,每天做一次全量备份,每小时做一次增量备份。全量备份用gs_basebackup,增量备份用 WAL 归档。WAL 归档可以配置到对象存储或者 NFS,但要注意归档延迟,否则恢复时会丢数据。我一般把 WAL 归档到本地磁盘,然后定期同步到远程存储。
恢复流程要定期演练,否则真出问题时手忙脚乱。我一般每季度做一次恢复演练,从备份中恢复一个单节点实例,验证数据完整性。
6.2 备节点扩容:从两备到三备的步骤
业务增长后,可能需要从两备扩展到三备。步骤不复杂,但要注意顺序。先创建一个新的 PVC,然后修改 StatefulSet 的replicas为 4,K8S 会自动创建第四个 Pod。新 Pod 启动后,会执行备节点初始化脚本,从主节点拉取基础备份,然后进入流复制模式。
主节点上需要更新replconninfo,增加指向新备节点的配置,然后重启主节点生效。重启主节点会导致短暂的服务中断,所以最好在业务低峰期操作。如果不想重启,可以动态修改replconninfo,但 openGauss 不支持热加载这个参数,所以还是得重启。
扩容后,检查pg_stat_replication,确认新备节点状态是streaming。如果新备节点一直处于catchup,说明基础备份太大,同步时间较长,耐心等待即可。
6.3 版本升级的滚动策略与回滚预案
openGauss 版本升级是个高风险操作,尤其是主备集群。我的做法是:先升级备节点,再切换主备,最后升级原主节点。具体步骤是:停止一个备节点,升级它的镜像版本,启动并确认同步正常;然后切换主备,把另一个备节点提升为主节点;接着升级原主节点(现在变成备节点);最后升级剩下的备节点。
升级过程中,要保证至少有一个备节点在同步,否则数据丢失风险很高。回滚预案也要准备好,如果升级后备节点无法同步,立即回滚到旧版本镜像,重新加入集群。
版本升级前,一定要做全量备份,并且验证备份可恢复。我吃过亏,有一次升级失败,想回滚却发现备份文件损坏,最后只能从备节点重建,花了整整一天。
6.4 监控告警:哪些指标必须盯死
openGauss 在 K8S 里的监控,我一般用 Prometheus 加 Grafana。关键指标包括:主备同步延迟、连接数、QPS、TPS、慢查询数量、磁盘使用率、CPU 和内存使用率、WAL 生成速率。
主备同步延迟是最重要的指标,超过 1 秒就要告警。连接数超过最大连接数的 80% 也要告警,否则新连接会被拒绝。磁盘使用率超过 80% 要告警,因为 openGauss 在磁盘满时会停止写入。WAL 生成速率突然飙升,可能是业务异常或者有大事务,需要排查。
告警渠道可以用邮件、钉钉或者企业微信。我一般设置两级告警:警告级别发邮件,严重级别发钉钉加电话。告警规则要定期 review,避免误报和漏报。
7. 个人实操体会:那些文档里不会写的细节
折腾完这一套,我最大的体会是:K8S 跑数据库,难的不是部署,而是运维。部署一次可能半天就搞定了,但后续的监控、备份、切换、升级,才是真正消耗精力的地方。所以一开始就要把自动化脚本写好,把监控告警配好,否则后面会疲于奔命。
另一个体会是:存储决定成败。我一开始为了省事用了 NFS,结果主备同步延迟高得离谱,后来换成 Local PV 才解决问题。所以如果你也在规划这个架构,存储方案一定要优先考虑,不要等到出问题了再换。
还有一个细节:openGauss 的日志级别默认是WARNING,很多有用的信息看不到。我一般调成INFO或者DEBUG,方便排查问题。但日志量会增大,所以要配置日志轮转,避免磁盘被撑满。
最后,主备切换的脚本一定要反复测试,模拟各种故障场景:主节点宕机、网络分区、备节点落后、脑裂。只有经过充分测试,才能在真正出故障时从容应对。我见过太多团队,切换脚本写好了但从来没测过,真出问题时一执行就报错,最后只能人工介入,耽误了宝贵的恢复时间。