Kubernetes 中的 StatefulSets 与持久化存储

使用有序扩展和持久数据部署有状态应用

Kubernetes StatefulSets 是管理需要稳定身份、持久存储和有序部署模式的有状态应用程序的最佳解决方案,对于数据库、分布式系统和缓存层至关重要。

如果您是 Kubernetes 新手或正在设置集群,请考虑探索 Kubernetes 发行版,如 k3s 或 MicroK8s 用于开发,或 使用 Kubespray 安装 Kubernetes 用于生产级集群。

presentation in the coffee shop 这张漂亮的图片是由 AI 模型 Flux 1 dev 生成的。

什么是 StatefulSets?

StatefulSets 是 Kubernetes 的工作负载 API 对象,专门用于管理有状态应用程序。与将所有 Pod 视为可互换的 Deployments 不同,StatefulSets 为每个 Pod 保持唯一的身份,并保证顺序和唯一性。

关键特性:

  • 稳定的网络标识符:每个 Pod 都会获得一个在重启后仍能保持的可预测的主机名
  • 持久存储:专用的 PersistentVolumeClaims 随着 Pod 的重新调度而跟随
  • 有序部署:Pod 按顺序创建(0, 1, 2…)并按相反顺序终止
  • 有序更新:滚动更新按顺序进行,确保应用程序的稳定性

StatefulSets 对于 PostgreSQL、MySQL、MongoDB、Cassandra、Elasticsearch、Kafka、ZooKeeper、Redis 和 etcd 等应用程序至关重要,这些应用程序中 Pod 的身份和数据持久性非常重要。

理解 Kubernetes 中的持久存储

Kubernetes 提供了一种复杂的存储抽象层,将存储管理与 Pod 生命周期解耦:

存储组件

PersistentVolume (PV):由管理员预置或通过 StorageClass 动态创建的集群中的一块存储。PV 独立于 Pod 存在。

PersistentVolumeClaim (PVC):Pod 对存储的请求。PVC 绑定到满足其要求(大小、访问模式、存储类)的可用 PV。

StorageClass:定义不同“类”的存储,具有各种提供者(AWS EBS、GCE PD、Azure Disk、NFS 等)和参数,如复制、性能层级和备份策略。

访问模式

  • ReadWriteOnce (RWO):卷由单个节点以读写方式挂载
  • ReadOnlyMany (ROX):卷由多个节点以只读方式挂载
  • ReadWriteMany (RWX):卷由多个节点以读写方式挂载(需要特殊的存储后端)

StatefulSet 存储架构

StatefulSets 使用 volumeClaimTemplates 自动为每个 Pod 副本创建 PersistentVolumeClaims。这与 Deployments 有根本的不同:

volumeClaimTemplates 的工作方式

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql-service
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: password
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "fast-ssd"
      resources:
        requests:
          storage: 10Gi

当您创建这个 StatefulSet 时:

  1. Kubernetes 创建 Pod mysql-0 和 PVC data-mysql-0
  2. 然后创建 Pod mysql-1 和 PVC data-mysql-1
  3. 最后创建 Pod mysql-2 和 PVC data-mysql-2

每个 Pod 都会获得自己的专用 10GB 持久卷。如果 mysql-1 被删除或重新调度,Kubernetes 会重新创建它并重新附加相同的 data-mysql-1 PVC,保留所有数据。

为 StatefulSets 创建无头服务

StatefulSets 需要一个无头服务来提供稳定的网络身份:

apiVersion: v1
kind: Service
metadata:
  name: mysql-service
spec:
  clusterIP: None  # 这使得它成为一个无头服务
  selector:
    app: mysql
  ports:
  - port: 3306
    name: mysql

这会为每个 Pod 创建 DNS 条目:

  • mysql-0.mysql-service.default.svc.cluster.local
  • mysql-1.mysql-service.default.svc.cluster.local
  • mysql-2.mysql-service.default.svc.cluster.local

应用程序可以使用这些稳定的 DNS 名称直接连接到特定的 Pod 实例。

StatefulSet 部署模式

对于管理复杂 Kubernetes 部署的团队,Helm Charts 提供了一种强大的方式,通过模板化、版本管理和依赖管理来打包和部署 StatefulSets。Helm 简化了在不同环境中管理 StatefulSet 配置。

有序扩展

从 3 个副本扩展到 5 个副本时:

kubectl scale statefulset mysql --replicas=5

Kubernetes 按顺序创建 Pod:mysql-3 → 等待 Ready → mysql-4

从 5 个副本扩展到 3 个副本时:

kubectl scale statefulset mysql --replicas=3

Kubernetes 按相反顺序终止:mysql-4 → 等待终止 → mysql-3

滚动更新

StatefulSets 支持两种更新策略:

OnDelete:手动更新——只有在删除 Pod 时才会更新 RollingUpdate:按逆序自动顺序更新

spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      partition: 2  # 仅更新序号 >= 2 的 Pod

partition 参数启用金丝雀部署——您可以先更新高序号的 Pod,测试后再推广到所有副本。

存储最佳实践

动态供应

始终使用 StorageClasses 进行动态卷供应:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp3
  iops: "3000"
  throughput: "125"
allowVolumeExpansion: true
reclaimPolicy: Retain

allowVolumeExpansion:启用在不重新创建 PVC 的情况下调整 PVC 大小 reclaimPolicyRetain 在 PVC 删除后保留 PV 数据,Delete 自动删除数据

PVC 保留策略

Kubernetes 1.23+ 支持 persistentVolumeClaimRetentionPolicy

spec:
  persistentVolumeClaimRetentionPolicy:
    whenDeleted: Retain    # StatefulSet 删除时保留 PVC
    whenScaled: Delete     # 缩放时删除 PVC

选项:

  • Retain:保留 PVC(默认行为,最安全)
  • Delete:自动删除 PVC(适用于开发环境)

备份策略

卷快照:使用 VolumeSnapshot 资源创建时间点备份

apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: mysql-snapshot-20251113
spec:
  volumeSnapshotClassName: csi-snapclass
  source:
    persistentVolumeClaimName: data-mysql-0

应用级备份:使用 mysqldumppg_dump 或 Velero 进行数据库特定备份

分布式复制:配置应用级复制(MySQL 复制、PostgreSQL 流式复制)作为第一道防线

实际应用案例

数据库集群(PostgreSQL)

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  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: password
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: "standard"
      resources:
        requests:
          storage: 20Gi

分布式缓存(Redis)

对于 Redis 集群,您需要 StatefulSet 和仔细的配置:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  serviceName: redis-service
  replicas: 6
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:7-alpine
        command:
        - redis-server
        - "--appendonly"
        - "yes"
        - "--appendfsync"
        - "everysec"
        ports:
        - containerPort: 6379
          name: redis
        volumeMounts:
        - name: data
          mountPath: /data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 5Gi

消息队列(Kafka)

Kafka 需要持久存储用于日志和稳定的网络标识用于经纪人协调:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: kafka
spec:
  serviceName: kafka-service
  replicas: 3
  selector:
    matchLabels:
      app: kafka
  template:
    metadata:
      labels:
        app: kafka
    spec:
      containers:
      - name: kafka
        image: confluentinc/cp-kafka:7.5.0
        ports:
        - containerPort: 9092
          name: kafka
        env:
        - name: KAFKA_BROKER_ID
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        volumeMounts:
        - name: data
          mountPath: /var/lib/kafka/data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 100Gi

监控和故障排除

有关本节中使用的 Kubernetes 命令的全面参考,请参阅 Kubernetes 快速参考

查看 StatefulSet 状态

# 查看 StatefulSet 详细信息
kubectl get statefulset mysql
kubectl describe statefulset mysql

# 检查 Pod 创建顺序和状态
kubectl get pods -l app=mysql -w

# 查看 PVC 状态
kubectl get pvc
kubectl describe pvc data-mysql-0

常见问题

Pod 卡在 Pending 状态:检查 PVC 状态和存储可用性

kubectl describe pod mysql-0
kubectl get events --sort-by='.lastTimestamp'

存储已满:如果 StorageClass 允许,扩展 PVC

kubectl patch pvc data-mysql-0 -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'

Pod 不会终止:检查应用级锁或卷卸载问题

kubectl delete pod mysql-0 --grace-period=0 --force

需要监控的指标

  • 存储使用情况:监控 PVC 容量和使用百分比
  • I/O 性能:跟踪 IOPS、吞吐量和延迟
  • Pod 重启:频繁重启可能表明存储问题
  • PVC 绑定时间:绑定缓慢表明供应问题

迁移策略

在迁移到 StatefulSets 时,请确保 Kubernetes 集群已正确配置。对于家庭实验室或小型集群设置,请查看我们的 Kubernetes 发行版全面比较,以选择适合您工作负载需求的平台。

从 Deployment 迁移到 StatefulSet

  1. 创建 StatefulSet 并包含 volumeClaimTemplates
  2. 优雅地缩放 Deployment
  3. 从备份中恢复数据 到 StatefulSet Pod
  4. 更新 DNS/Service 引用
  5. 删除旧的 Deployment 和 PVC

迁移前的备份

# 快照现有 PVC
kubectl get pvc -o yaml > pvc-backup.yaml

# 创建卷快照
kubectl apply -f volume-snapshot.yaml

安全考虑

存储加密

使用 StorageClass 参数启用静态加密:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: encrypted-storage
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp3
  encrypted: "true"
  kmsKeyId: arn:aws:kms:us-east-1:123456789012:key/abcd-1234

访问控制

使用 RBAC 限制谁可以创建/修改 StatefulSets 和 PVC:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: statefulset-manager
rules:
- apiGroups: ["apps"]
  resources: ["statefulsets"]
  verbs: ["get", "list", "create", "update", "delete"]
- apiGroups: [""]
  resources: ["persistentvolumeclaims"]
  verbs: ["get", "list", "create", "delete"]

网络策略

限制 Pod 之间的通信:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: mysql-network-policy
spec:
  podSelector:
    matchLabels:
      app: mysql
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: backend
    ports:
    - protocol: TCP
      port: 3306

性能优化

存储性能层级

根据工作负载选择适当的 StorageClasses:

  • 高 IOPS:具有大量随机读写的数据库(gp3, io2)
  • 高吞吐量:日志聚合、分析(st1, sc1)
  • 平衡:通用应用程序(gp3)

Pod 分布

使用 Pod 反亲和性将 StatefulSet Pod 分布在可用区域中:

spec:
  template:
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
                app: mysql
            topologyKey: topology.kubernetes.io/zone

资源请求和限制

设置适当的资源以实现一致的性能:

resources:
  requests:
    cpu: "2"
    memory: "4Gi"
    ephemeral-storage: "10Gi"
  limits:
    cpu: "4"
    memory: "8Gi"
    ephemeral-storage: "20Gi"

高级模式

使用初始化容器的有状态应用程序

使用初始化容器进行数据库初始化:

spec:
  template:
    spec:
      initContainers:
      - name: init-mysql
        image: mysql:8.0
        command:
        - bash
        - "-c"
        - |
          if [[ ! -d /var/lib/mysql/mysql ]]; then
            mysqld --initialize-insecure --datadir=/var/lib/mysql
          fi          
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql

用于 Sidecar 的多容器 Pod

添加备份 Sidecar 或监控代理:

spec:
  template:
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        # ... mysql 配置 ...
      - name: backup-sidecar
        image: mysql-backup:latest
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          readOnly: true

用于动态配置的 ConfigMaps

将配置与 StatefulSet 定义分离:

apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-config
data:
  my.cnf: |
    [mysqld]
    max_connections=200
    innodb_buffer_pool_size=2G    
---
spec:
  template:
    spec:
      containers:
      - name: mysql
        volumeMounts:
        - name: config
          mountPath: /etc/mysql/conf.d
      volumes:
      - name: config
        configMap:
          name: mysql-config

有用的链接