4. 持久化存储选型与实战

PostgreSQL 是有状态服务,数据目录 PGDATA 一旦丢失就意味着数据库不可恢复。在 Kubernetes 上部署 PostgreSQL,存储选型不是“能挂上就行”,而是要在性能、持久性、可迁移性、运维复杂度之间做权衡。本节先横向对比六类常见方案,再落到 StatefulSet 的动态存储卷配置上。

一、六类存储方案的本质差异

理解选型,先要抓住两个维度:数据落在哪台机器上,以及Pod 漂移后数据能否跟着走。

  • emptyDir:生命周期与 Pod 绑定,Pod 删除即清空。它只是节点本地临时目录,绝不能用于 PostgreSQL 数据目录,最多用于初始化时的临时解压或缓存。
  • hostPath:直接挂载节点上的目录,数据在节点本地,Pod 重建后若仍调度到同一节点可复用。但它没有调度约束,Pod 漂移到别的节点就会看到空目录,且多 Pod 共享同一路径会互相踩踏,生产环境不推荐。
  • local PV:本质仍是节点本地盘,但通过 PV/PVC 和 nodeAffinity 把“数据位置”和“调度位置”绑定起来。Pod 只会被调度到持有该 PV 的节点,因此数据可复用、性能接近本地盘,代价是节点故障时数据不可迁移,需要依赖 PostgreSQL 流复制做高可用。
  • NFS:网络文件系统,多个节点都能挂载,Pod 漂移后数据仍在。部署简单、共享方便,但受网络延迟和单点 NFS Server 影响,随机写性能一般,适合中小规模或测试环境。
  • Ceph RBD:块存储,通过 CSI 以 RWO 模式挂载给单个 Pod,支持跨节点漂移、快照和克隆,性能优于 NFS,是自建集群里较常见的选择。运维复杂度较高,需要维护 Ceph 集群。
  • 云盘:如 AWS EBS、阿里云 ESSD、腾讯云 CBS,通过云厂商 CSI 动态供给。性能稳定、支持快照与扩容,但通常绑定可用区,跨区漂移受限,且按量计费。

下面的关系图把六类方案按“数据位置—漂移能力—适用场景”三个维度做了定位,便于快速判断。

PostgreSQL 存储方案定位图 横轴:数据位置与漂移能力 纵轴:性能与运维复杂度 跨节点可漂移 → 性能/复杂度 ↑ 节点本地 · 不可漂移 网络共享 · 可漂移 云托管 · 可漂移(限区) emptyDir Pod 删除即清空 · 仅临时 hostPath 节点目录 · 无调度约束 local PV 本地盘 + nodeAffinity 绑定 NFS 共享文件 · 部署简单 Ceph RBD 块存储 · 快照/克隆 云盘 (EBS/ESSD/CBS) CSI 动态供给 · 按量计费 ● 蓝/绿/橙三色圆点表示生产环境优先考虑的候选方案

二、选型判断:先看高可用架构,再看存储

PostgreSQL 在 Kubernetes 上通常有两种部署形态:单实例 + 定期备份,或主从流复制 + 自动故障转移(如 Patroni、CloudNativePG)。存储选型必须与高可用形态匹配。

  • 若采用主从复制,单节点本地盘故障可由副本接管,此时 local PV 是性能最优解,因为数据可靠性由复制层保证,而非存储层。
  • 若采用单实例,则必须依赖存储层本身的高可用,应选择 Ceph RBD 或云盘,避免节点故障导致数据不可访问。
  • NFS 适合开发测试或读多写少的辅助库;生产主库不建议使用,除非 NFS Server 本身做了高可用且网络为低延迟专网。
判断口诀:有副本,可用本地盘换性能;无副本,必须用可漂移的共享或云存储。

三、为 PostgreSQL StatefulSet 配置动态存储卷

动态供给的核心是 StorageClass + volumeClaimTemplates。StatefulSet 会为每个副本自动创建独立的 PVC,命名规则为 <volumeClaimTemplate名>-<StatefulSet名>-<序号>,例如 data-postgres-0。

下面以云盘 CSI 为例,定义一个支持扩容和延迟绑定的 StorageClass。延迟绑定(WaitForFirstConsumer)很关键:它让 PV 在 Pod 调度后再创建,从而与 Pod 落在同一可用区。

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: postgres-ssd
provisioner: ebs.csi.aws.com        # 云厂商 CSI,自建可用 rbd.csi.ceph.com
parameters:
  type: gp3
  fsType: ext4
reclaimPolicy: Retain               # 删除 PVC 时保留底层卷,防误删
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

接着在 StatefulSet 中通过 volumeClaimTemplates 声明数据卷。注意 storageClassName 要与上面一致,容量按业务增长预留。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres-headless
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:16
        ports:
        - containerPort: 5432
        env:
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: postgres-ssd
      resources:
        requests:
          storage: 100Gi

这里有一个容易踩的坑:PGDATA 必须指向挂载点下的子目录(如 pgdata),而不是挂载根本身。因为多数 CSI 卷的根目录会放置 lost+found 等文件,PostgreSQL 初始化时要求数据目录为空,否则会报错退出。

四、验证与结果判断

应用清单后,按以下顺序确认存储是否正确供给:

  1. 查看 PVC 是否绑定:kubectl get pvc -l app=postgres,状态应为 Bound,且每个副本一个 PVC。
  2. 确认 PV 与 Pod 同区:kubectl get pv <pv名> -o yaml,检查 nodeAffinity 中的可用区与 Pod 所在节点一致。
  3. 进入 Pod 验证挂载:kubectl exec -it postgres-0 -- df -h /var/lib/postgresql/data,应看到 100Gi 的卷。
  4. 写入测试数据后删除 Pod,观察重建后数据是否仍在:kubectl delete pod postgres-0,重建后查询表数据,若存在则持久化生效。
若 PVC 长期处于 Pending,优先检查 StorageClass 的 volumeBindingMode 与 CSI 插件是否就绪;若 Pod 报 directory not empty,则是 PGDATA 未指向子目录所致。

最后给出一个简明的选型对照,便于在项目评审时快速决策。

方案数据位置可漂移性能推荐场景
emptyDir节点临时否高仅临时缓存
hostPath节点目录否高不推荐生产
local PV节点本地盘否最高主从复制架构
NFS网络共享是中测试/辅助库
Ceph RBD分布式块是较高自建生产集群
云盘云块存储是(限区)高云上生产

选型没有绝对最优,只有与高可用架构、预算和运维能力最匹配的方案。确定方案后,务必把 reclaimPolicy 设为 Retain,并配合定期备份,才能让 PostgreSQL 在 Kubernetes 上真正“持久”。