1. 为什么要在 K8S 上跑一主两备的 openGauss
把 openGauss 塞进 K8S 里,还要做成一主两备的架构,这事我第一次听到的时候也觉得有点拧巴。数据库这种对 IO、内存、网络延迟都极其敏感的东西,天生就适合跑在物理机或者独占虚拟机上,你把它扔进容器编排系统里,光是存储和网络这两块就够喝一壶的。但现实情况是,很多团队的运维体系已经全面转向 K8S 了,监控、日志、发布、扩缩容全在这套体系里,你单独搞几台物理机跑数据库,运维成本反而更高。所以问题不是“要不要在 K8S 上跑 openGauss”,而是“怎么跑才能稳”。
openGauss 是华为开源的关系型数据库,基于 PostgreSQL 内核做了大量企业级增强,尤其在事务处理、安全审计、高可用方面有自己的东西。它原生支持主备复制,通过流复制实现数据同步,配合 CM(Cluster Manager)做故障切换。一主两备是最常见的生产配置:一个主节点负责读写,两个备节点通过流复制实时同步数据,主节点挂了之后 CM 能自动把其中一个备节点提升为新的主节点。这套机制在物理机上跑已经很成熟了,但搬到 K8S 上,核心矛盾就变成了:容器是无状态的,而数据库是有状态的,且对状态的一致性要求极高。
这篇文章适合谁看?如果你已经对 K8S 的基本概念(Pod、Service、StatefulSet、PV/PVC)有了解,同时对 openGauss 的架构有一定认知,那接下来的内容可以直接抄作业。如果你对 K8S 还比较陌生,建议先把 StatefulSet 和 Headless Service 这两个东西搞清楚再回来,不然下面很多设计决策你会看不懂。我会从整体设计思路开始拆,然后一步步讲实操,最后把踩过的坑和排查技巧都倒出来。
2. 整体架构设计与核心思路拆解
2.1 为什么用 StatefulSet 而不是 Deployment
这是第一个必须想清楚的问题。Deployment 管的是无状态副本,Pod 的名字是随机的,重启之后 IP 和名字都变了,存储也通常是共享的或者临时的。数据库集群里每个节点都是有身份的:主节点就是主节点,备节点 1 就是备节点 1,它们的角色不能乱,网络标识必须稳定。StatefulSet 正好解决这个问题——它给每个 Pod 分配一个稳定的序号和域名,比如opengauss-0、opengauss-1、opengauss-2,配合 Headless Service 就能通过 DNS 直接访问到具体某个 Pod。
注意:StatefulSet 的 Pod 删除和重建时,名字和网络标识保持不变,但 IP 会变。所以 openGauss 的复制配置里不能用 IP 硬编码,必须用域名。
2.2 存储方案的选择逻辑
openGauss 对存储的要求可以总结为三点:低延迟、高吞吐、数据持久化。在 K8S 里,存储通过 PV/PVC 来管理。常见的选项有本地盘(Local PV)、NFS、Ceph RBD、以及各种云厂商的块存储。我的建议是:如果追求性能,优先用 Local PV 配合 nodeAffinity,把 Pod 固定到有本地 SSD 的节点上。Local PV 的缺点是节点挂了数据就没了,所以必须配合 openGauss 自身的流复制来做数据冗余——一主两备本身就是三副本,只要不同时挂两个节点,数据就不会丢。
如果你用的是云环境,云厂商的块存储(比如 SSD 云盘)也是不错的选择,延迟比 Local PV 稍高但胜在可靠性和动态供给方便。NFS 我不推荐用在生产环境的数据库上,延迟抖动太大,openGauss 的 WAL 写入对延迟非常敏感,NFS 很容易成为瓶颈。
2.3 网络通信的设计
openGauss 集群内部需要几种通信:主备之间的流复制(默认端口 5432 附近的内部端口)、CM 组件之间的心跳和仲裁通信、以及客户端连接。在 K8S 里,Pod 之间的网络是扁平的,但我们需要用 Headless Service 来提供稳定的 DNS 解析。具体来说,创建一个名为opengauss-headless的 Headless Service,clusterIP: None,这样每个 Pod 都会有类似opengauss-0.opengauss-headless.default.svc.cluster.local的域名。
流复制配置里,主节点通过这个域名找到备节点,备节点也通过域名连主节点。CM 的配置类似。客户端连接则通过一个普通的 ClusterIP Service 或者 NodePort Service 来暴露,这个 Service 只指向主节点(或者通过 CM 做 VIP 漂移,但在 K8S 里更常见的做法是用一个 Service 配合 readiness probe 来只路由到主节点)。
2.4 一主两备的角色分配与故障切换
一主两备的核心是 CM(Cluster Manager)。openGauss 的 CM 负责监控各节点状态、仲裁、故障切换。在 K8S 里,CM 通常以 sidecar 或者独立进程的方式运行在每个 Pod 里。三个节点组成一个集群,CM 通过多数派投票来决定是否切换。这里有个关键点:三个节点必须分布在不同的物理节点上,否则一台物理机挂了三个 Pod 全没了,CM 的多数派机制也救不了。所以需要用 podAntiAffinity 来强制打散。
故障切换的流程大致是:主节点心跳超时 -> CM 发起投票 -> 多数派同意 -> 选一个备节点提升为主 -> 更新集群元数据 -> 其他备节点指向新主。这个过程在 K8S 里需要额外注意一点:Pod 重启后 IP 变了,CM 需要能通过域名重新发现节点,所以 CM 的配置里也要用域名而不是 IP。
3. 核心细节解析与实操要点
3.1 openGauss 镜像的准备
官方并没有提供一个“开箱即用”的 K8S 镜像,你需要自己构建。基础镜像建议用 openEuler 或者 CentOS 的某个稳定版本,然后在里面安装 openGauss 的二进制包。构建镜像时有几个关键点:
- 不要用 latest 标签,必须固定 openGauss 的版本号,比如
opengauss:5.0.0,否则不同节点拉到的镜像版本不一致,流复制会出问题。 - 时区必须统一,所有节点用 UTC 或者同一个时区,否则日志时间戳对不上,排查问题时会疯掉。
- ulimit 和内核参数,openGauss 对文件描述符、共享内存等有要求,这些在容器里需要通过 securityContext 或者 initContainer 来设置。
FROM openeuler:22.03 RUN yum install -y libaio numactl openssl && yum clean all COPY openGauss-5.0.0-openEuler-64bit.tar.gz /tmp/ RUN tar -zxf /tmp/openGauss-5.0.0-openEuler-64bit.tar.gz -C /opt/ && \ useradd -m omm && \ chown -R omm:omm /opt/openGauss USER omm ENV GAUSSHOME=/opt/openGauss ENV PATH=$GAUSSHOME/bin:$PATH ENV LD_LIBRARY_PATH=$GAUSSHOME/lib:$LD_LIBRARY_PATH提示:镜像里不要预置任何数据目录的内容,数据目录必须通过 PVC 挂载,否则 Pod 重建后数据就丢了。
3.2 StatefulSet 的关键配置
StatefulSet 的 YAML 里,有几个字段是必须仔细设置的。serviceName指向 Headless Service,replicas设为 3,podManagementPolicy建议用OrderedReady,这样 Pod 会按 0、1、2 的顺序依次启动,方便控制初始化流程。volumeClaimTemplates定义每个 Pod 的 PVC 模板,存储大小根据你的数据量来定,建议至少 100Gi 起步。
apiVersion: apps/v1 kind: StatefulSet metadata: name: opengauss spec: serviceName: opengauss-headless replicas: 3 podManagementPolicy: OrderedReady selector: matchLabels: app: opengauss template: metadata: labels: app: opengauss spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: opengauss topologyKey: kubernetes.io/hostname containers: - name: opengauss image: opengauss:5.0.0 ports: - containerPort: 5432 name: db - containerPort: 8000 name: cm volumeMounts: - name: data mountPath: /var/lib/opengauss volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] storageClassName: local-ssd resources: requests: storage: 100GipodAntiAffinity用requiredDuringSchedulingIgnoredDuringExecution强制三个 Pod 不能在同一台物理节点上,这是保证高可用的底线。如果你集群节点不够,至少也要用preferred来尽量打散。
3.3 初始化脚本的设计
三个 Pod 启动后,需要有一个初始化流程来搭建主备关系。我的做法是在镜像里放一个 init 脚本,Pod 启动时根据自己序号来决定角色:opengauss-0作为初始主节点,执行gs_initdb初始化数据库;opengauss-1和opengauss-2作为备节点,执行gs_basebackup从主节点拉取基础备份,然后配置流复制。
这个脚本需要处理几种情况:如果是全新部署,按上述流程走;如果是 Pod 重启(数据目录已有数据),则直接启动数据库进程,不重新初始化。判断依据就是数据目录下是否存在postgresql.conf等配置文件。
#!/bin/bash DATA_DIR="/var/lib/opengauss/data" if [ ! -f "$DATA_DIR/postgresql.conf" ]; then if [ "$HOSTNAME" == "opengauss-0" ]; then gs_initdb -D $DATA_DIR -w "YourPassword" --nodename=primary # 配置流复制参数 echo "synchronous_standby_names = '*'" >> $DATA_DIR/postgresql.conf else PRIMARY_HOST="opengauss-0.opengauss-headless" gs_basebackup -D $DATA_DIR -h $PRIMARY_HOST -p 5432 -U omm -W "YourPassword" # 配置 recovery 参数 cat >> $DATA_DIR/postgresql.conf <<EOF primary_conninfo = 'host=$PRIMARY_HOST port=5432 user=omm password=YourPassword' EOF fi fi gs_ctl start -D $DATA_DIR注意:
gs_basebackup需要在主节点已经启动并且允许复制连接的情况下执行。所以OrderedReady策略下,opengauss-0先启动并完成初始化,然后opengauss-1和opengauss-2再依次启动。这个顺序不能乱。
3.4 CM 组件的集成
CM 是 openGauss 高可用的核心,它需要知道集群里所有节点的信息。在 K8S 里,CM 的配置文件里节点地址要用域名。CM 通常以独立进程运行在同一个 Pod 里,通过cm_ctl命令来管理。你需要在镜像里把 CM 的二进制也装好,然后在启动脚本里拉起 CM 进程。
CM 的配置里有一个cluster_static_config文件,里面定义了所有节点的node_id、node_name、instance_type等信息。node_name就用 Pod 的域名,比如opengauss-0.opengauss-headless。CM 启动后会通过这个域名去连接各个节点,做心跳检测和仲裁。
4. 实操过程与核心环节实现
4.1 环境准备与前置检查
在开始部署之前,先确认你的 K8S 集群满足以下条件:至少三个可调度的 worker 节点,每个节点有足够的 CPU(建议 4 核以上)和内存(建议 8Gi 以上),并且有可用的 StorageClass。如果是 Local PV,还需要提前在节点上准备好目录并创建对应的 PV。
kubectl get nodes kubectl get storageclass kubectl describe node <node-name> | grep -A5 "Allocatable"另外,确认 K8S 集群的 DNS 服务正常工作,因为整个方案严重依赖 Headless Service 的域名解析。你可以起一个测试 Pod 来验证:
kubectl run dns-test --image=busybox --rm -it -- nslookup opengauss-0.opengauss-headless如果解析失败,检查 CoreDNS 的配置和 Service 的定义。
4.2 创建 Headless Service 和命名空间
先创建一个独立的命名空间,把 openGauss 相关的资源都放进去,方便管理。
apiVersion: v1 kind: Namespace metadata: name: opengauss-ns --- apiVersion: v1 kind: Service metadata: name: opengauss-headless namespace: opengauss-ns spec: clusterIP: None selector: app: opengauss ports: - name: db port: 5432 targetPort: 5432 - name: cm port: 8000 targetPort: 8000Headless Service 本身不分配 ClusterIP,它的作用就是为每个 Pod 创建 DNS A 记录。创建完成后,你可以用kubectl get endpoints来确认 Pod 是否被正确关联。
4.3 部署 StatefulSet 并观察启动过程
把前面准备好的 StatefulSet YAML 应用上去,然后用kubectl get pods -n opengauss-ns -w实时观察 Pod 的启动顺序。你会看到opengauss-0先变成 Running,然后opengauss-1开始启动,最后是opengauss-2。
kubectl apply -f opengauss-statefulset.yaml -n opengauss-ns kubectl get pods -n opengauss-ns -w在opengauss-0启动过程中,你可以查看它的日志来确认初始化是否成功:
kubectl logs opengauss-0 -n opengauss-ns -f如果看到gs_initdb完成并且数据库进程正常启动,说明主节点 OK。然后观察opengauss-1的日志,确认gs_basebackup是否成功从主节点拉取了数据。
4.4 验证主备复制状态
三个 Pod 都 Running 之后,进入主节点 Pod 执行查询来验证复制状态:
kubectl exec -it opengauss-0 -n opengauss-ns -- bash gsql -d postgres -p 5432 -c "select * from pg_stat_replication;"这个查询会返回当前连接到主节点的备节点信息,包括client_addr、state、sync_state等字段。如果看到两行记录,state都是streaming,说明两个备节点都在正常接收流复制数据。sync_state如果是Sync表示同步复制,Async表示异步复制。生产环境建议至少一个备节点配置为同步复制,保证数据不丢。
提示:
client_addr显示的是 Pod 的 IP,不是域名。因为流复制连接建立后,底层还是 TCP 连接。但这不影响,因为连接建立时用的是域名解析出来的 IP。
4.5 模拟故障切换
验证高可用最直接的方法就是手动干掉主节点,看 CM 是否能自动切换。你可以用kubectl delete pod opengauss-0来模拟主节点故障。StatefulSet 会自动重建这个 Pod,但重建需要时间,在这段时间内 CM 应该已经完成了切换。
kubectl delete pod opengauss-0 -n opengauss-ns # 观察 CM 日志和剩余两个 Pod 的状态 kubectl logs opengauss-1 -n opengauss-ns -c cm --tail=50切换完成后,用gs_ctl query或者 CM 的命令行工具查看当前集群状态,确认新的主节点是谁。然后等opengauss-0重建完成后,它应该自动作为备节点重新加入集群。
5. 常见问题与排查技巧实录
5.1 Pod 启动卡在 Init 阶段
这是最常见的问题之一。Pod 一直处于Init:0/1或者PodInitializing状态,通常是因为初始化脚本执行失败。排查步骤:先看kubectl describe pod的 Events 部分,看有没有明显的错误信息;然后看 initContainer 的日志(如果有的话)。常见原因包括:PVC 挂载失败、主节点还没就绪导致gs_basebackup连接超时、密码配置错误等。
实操心得:在初始化脚本里加足够的日志输出,每一步都 echo 出来,这样排查时一目了然。另外,
gs_basebackup的超时时间可以适当调大,避免因为网络抖动导致失败。
5.2 流复制中断
备节点日志里出现replication terminated或者could not receive data from WAL stream之类的错误,说明流复制断了。先检查主备之间的网络连通性,用kubectl exec进入备节点 Pod,ping或者telnet主节点的域名和端口。如果网络没问题,检查主节点的pg_hba.conf是否允许备节点的 IP 段连接。在 K8S 里,Pod 的 IP 段是动态的,所以pg_hba.conf里最好配置为允许整个 Pod CIDR 网段。
# 在主节点的 pg_hba.conf 里添加 host replication omm 10.244.0.0/16 md5如果流复制中断时间较长,备节点的 WAL 日志可能已经被主节点回收了,这时候需要重新做gs_basebackup。所以生产环境要合理配置 WAL 保留策略,wal_keep_segments或者max_slot_wal_keep_size要设置得足够大。
5.3 CM 无法完成仲裁
CM 的多数派仲裁需要至少两个节点存活。如果三个节点里有两个同时挂了,CM 就无法完成切换,集群会进入只读或者不可用状态。在 K8S 里,这种情况可能发生在节点维护或者资源不足导致 Pod 被驱逐时。预防措施是给 openGauss 的 Pod 设置较高的priorityClassName,避免被低优先级的 Pod 挤掉。同时,给 Pod 配置PodDisruptionBudget,限制同时不可用的 Pod 数量。
apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: opengauss-pdb namespace: opengauss-ns spec: minAvailable: 2 selector: matchLabels: app: opengauss5.4 性能不达预期
如果发现数据库性能比物理机上差很多,先排查存储。用fio或者dd在 PVC 里测试随机读写 IOPS 和延迟。如果存储是瓶颈,考虑换 Local NVMe SSD。然后检查网络,K8S 的 overlay 网络(比如 Calico 的 IPIP 模式)会带来额外延迟,可以尝试用 hostNetwork 或者 SR-IOV 来降低网络开销。最后检查资源限制,resources.requests和limits不要设置得太紧,openGauss 在内存不足时会频繁刷盘,性能急剧下降。
| 问题现象 | 可能原因 | 排查命令 | 解决方案 |
|---|---|---|---|
| Pod 一直 Pending | 资源不足或亲和性不满足 | kubectl describe pod | 调整资源请求或节点标签 |
| 流复制延迟高 | 网络带宽不足或主节点负载高 | pg_stat_replication查看write_lag | 优化网络或降低主节点压力 |
| CM 频繁切换 | 心跳网络抖动 | 查看 CM 日志 | 调整心跳超时参数 |
| 数据库启动失败 | 数据目录权限问题 | kubectl logs | 确保 PVC 挂载目录属主为 omm |
5.5 数据备份与恢复的注意事项
在 K8S 里做 openGauss 的备份,推荐用gs_dump做逻辑备份,或者用gs_basebackup做物理备份。逻辑备份可以通过 CronJob 定期执行,把备份文件写到对象存储或者 NFS 上。物理备份需要停库或者用pg_start_backup的方式,在容器环境里操作起来稍微麻烦一些。我的建议是:逻辑备份每天一次,物理备份每周一次,并且定期做恢复演练。备份文件不要放在同一个 PVC 里,否则 PVC 挂了备份也没了。
注意:
gs_dump在备节点上执行会报错,必须在主节点上执行。所以 CronJob 里需要先判断当前哪个节点是主节点,或者直接通过 Service 连接到主节点。
6. 一些个人体会和后续扩展思路
这套方案我在几个不同规模的集群里都跑过,整体稳定性是可以接受的,但前提是你把存储和反亲和性这两个基础打牢。我见过太多人为了省节点,把三个 Pod 挤在两台物理机上,结果一台机器维护,整个集群就只剩一个节点,CM 直接失去仲裁能力,数据库变成只读。这种坑踩一次就够记一辈子。
另外,openGauss 的版本升级在 K8S 里也需要特别小心。StatefulSet 的滚动更新默认是从序号最大的 Pod 开始删,但数据库集群的升级顺序通常要求先升级备节点,最后升级主节点。所以你需要把podManagementPolicy和更新策略配合好,或者干脆手动控制升级流程。我个人的做法是:先升级opengauss-2,确认没问题后再升级opengauss-1,最后手动触发主备切换,把opengauss-0降为备节点后再升级。
后续如果要做多集群容灾,可以考虑用 openGauss 的级联备或者逻辑复制,把数据同步到另一个 K8S 集群。不过那就是另一个话题了,涉及跨集群的网络打通和冲突解决,复杂度会上升一个量级。先把单集群的一主两备跑稳,再考虑这些也不迟。