2. Kubernetes核心对象与PostgreSQL工作负载

在Kubernetes上运行PostgreSQL,第一步不是写YAML,而是先建立一张“对象地图”:哪些对象负责调度、哪些负责稳定身份、哪些负责网络入口、哪些负责配置注入。只有把对象职责分清,才能理解为什么PostgreSQL这类有状态数据库不能简单套用无状态应用的部署方式。

Kubernetes的最小调度单元是Pod,它封装一个或多个容器、共享网络命名空间与存储卷。Pod本身是“易失”的:被删除后重建,名字和IP都会变化。对于无状态Web服务,这没有问题;但PostgreSQL的数据目录、主从复制身份、客户端连接地址都依赖稳定标识,因此需要更高层的控制器来管理Pod。

Deployment管理无状态Pod副本,支持滚动更新与回滚,但它不保证Pod名称和网络标识的稳定性。它适合前端、API网关等场景,不适合直接承载数据库主节点。StatefulSet则专为有状态负载设计:它为每个副本分配固定序号(如postgres-0、postgres-1),配合Headless Service提供稳定的DNS名称,并通过volumeClaimTemplates为每个Pod绑定独立的PersistentVolumeClaim(PVC)。这意味着即使Pod被重新调度,它仍会挂载原来的数据卷,并保留原有网络身份。

Service解决“如何访问Pod”的问题。普通ClusterIP Service为一组Pod提供虚拟IP和负载均衡;Headless Service(clusterIP: None)不分配虚拟IP,而是直接返回Pod的DNS记录,常用于StatefulSet的稳定网络标识。对于PostgreSQL,通常需要两个Service:一个Headless Service用于主从复制和内部发现,一个普通Service用于客户端读写入口。

ConfigMap与Secret负责配置与敏感信息注入。ConfigMap存放非敏感配置,如postgresql.conf片段、时区、日志级别;Secret存放密码、复制凭据、TLS证书等。两者都可以通过环境变量或卷挂载进入Pod。对于PostgreSQL,推荐将配置文件以卷方式挂载到/etc/postgresql,密码通过Secret注入环境变量,避免硬编码在镜像中。

PostgreSQL StatefulSet 核心对象关系图 StatefulSet postgres Headless Service postgres-headless ClusterIP Service postgres-client Pod: postgres-0 Primary (主节点) 10.244.1.5 Pod: postgres-1 Replica (备节点) 10.244.2.7 Pod: postgres-2 Replica (备节点) 10.244.3.9 PVC: data-postgres-0 10Gi PVC: data-postgres-1 10Gi PVC: data-postgres-2 10Gi ConfigMap postgresql.conf 片段 Secret POSTGRES_PASSWORD 流复制 图例 管理关系 配置注入

上图展示了PostgreSQL StatefulSet部署中核心对象的协作关系。StatefulSet创建并管理三个Pod,每个Pod拥有独立的PVC和稳定的DNS名称。Headless Service为Pod提供postgres-0.postgres-headless.default.svc.cluster.local这类可预测的域名,主从复制通过该域名建立连接。ClusterIP Service为客户端提供统一读写入口,ConfigMap和Secret分别注入配置与密码。

下面给出一个可参考的PostgreSQL StatefulSet部署示例。该示例使用官方postgres:16镜像,配置1主2从的流复制拓扑,存储类使用standard(示例假设集群已存在该StorageClass)。

apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
type: Opaque
stringData:
  POSTGRES_PASSWORD: "PgDemo#2024"
  REPLICATION_PASSWORD: "ReplDemo#2024"
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-config
data:
  postgresql.conf: |
    listen_addresses = '*'
    max_connections = 200
    shared_buffers = 256MB
    wal_level = replica
    max_wal_senders = 5
    hot_standby = on
---
apiVersion: v1
kind: Service
metadata:
  name: postgres-headless
spec:
  clusterIP: None
  selector:
    app: postgres
  ports:
    - port: 5432
      name: postgres
---
apiVersion: v1
kind: Service
metadata:
  name: postgres-client
spec:
  selector:
    app: postgres
    role: primary
  ports:
    - port: 5432
      targetPort: 5432
---
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: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: POSTGRES_PASSWORD
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
            - name: config
              mountPath: /etc/postgresql/postgresql.conf
              subPath: postgresql.conf
      volumes:
        - name: config
          configMap:
            name: postgres-config
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: standard
        resources:
          requests:
            storage: 10Gi

应用该清单后,可以通过以下命令观察部署状态与Pod身份:

kubectl get statefulset postgres
kubectl get pods -l app=postgres -o wide
kubectl get pvc -l app=postgres
kubectl exec postgres-0 -- pg_isready -U postgres

预期结果:postgres-0、postgres-1、postgres-2依次创建,每个Pod绑定一个独立的PVC,名称分别为data-postgres-0、data-postgres-1、data-postgres-2。通过kubectl exec postgres-0 -- psql -U postgres -c "SELECT pg_is_in_recovery();"应返回f(表示主节点),在postgres-1上执行同样命令应返回t(表示备节点)。

StatefulSet的Pod命名规则是<statefulset-name>-<ordinal>,序号从0开始。删除Pod后重建,名称和PVC绑定关系保持不变,这是与Deployment最本质的区别。
示例中的密码仅用于教学演示,生产环境应使用更复杂的凭据,并通过RBAC限制Secret的读取权限。另外,storageClassName需根据实际集群的StorageClass名称调整,否则PVC会一直处于Pending状态。