4、Kubernetes存储:PV/PVC、StorageClass、StatefulSet有状态应用

聊到Kubernetes的存储,我估计不少朋友第一反应就是「头疼」。确实,相比无状态应用那种「挂了就重启,数据丢了就丢了」的洒脱,有状态应用的存储管理要复杂得多。今天我们就来把这几个核心概念掰开揉碎,讲清楚。

4.1 从「数据持久化」说起

容器本身是临时性的,这个大家都知道。但我们的数据库、消息队列、缓存这些中间件,数据可不能随便丢。Kubernetes为了解决这个问题,设计了PV和PVC这套抽象层。

说白了,PV就是集群里的存储资源,好比一块「硬盘」。PVC则是用户对存储的「需求申请」,好比你说「我要一块100G的SSD」。系统看到你的申请,就会找一块合适的PV绑定给你。

我在项目中遇到过一种情况:开发同事直接写死了存储路径,结果Pod漂移到另一台节点上,数据全丢了。嗯,这就是典型的「没理解容器存储本质」的坑。

核心要点:PV和PVC的生命周期是独立的。PV由管理员创建,PVC由用户创建。绑定后,Pod就可以通过PVC来使用存储了。

4.2 PV/PVC的几种访问模式

你想想看,不同的存储设备,支持的访问方式是不一样的。比如NFS可以同时被多个节点读写,但一块云盘通常只能被一个节点挂载。Kubernetes用AccessModes来区分:

访问模式 缩写 说明
ReadWriteOnce RWO 单节点读写,像云盘
ReadOnlyMany ROX 多节点只读,像共享文件
ReadWriteMany RWX 多节点读写,像NFS

我个人习惯,对于数据库这类中间件,首选RWO模式。因为数据库本身有主从复制机制,不需要多个节点同时写同一块盘。RWX模式虽然灵活,但性能瓶颈和锁问题会让你很头疼。

4.3 StorageClass:动态供给的利器

如果每次都要管理员手动创建PV,那运维效率也太低了。StorageClass就是来解决这个问题的——它实现了存储的「动态供给」。

什么意思呢?你创建一个PVC,系统看到对应的StorageClass,就会自动调用存储后端(比如阿里云盘、AWS EBS、Ceph RBD)的API,帮你创建一块存储,再自动生成PV并绑定。整个过程全自动。

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: alicloud-disk-ssd
provisioner: diskplugin.csi.alibabacloud.com
parameters:
  type: cloud_ssd
  regionId: cn-beijing
  zoneId: cn-beijing-a
reclaimPolicy: Retain
allowVolumeExpansion: true

我曾经踩过一个坑:reclaimPolicy默认是Delete,也就是说PVC删除后,PV和底层存储也会被删掉。有一次测试环境清理PVC,结果把生产环境的备份盘也误删了……从那以后,对于重要数据,我习惯把reclaimPolicy设为Retain。

注意:StorageClass的provisioner需要对应存储插件的CSI驱动。不同云厂商的provisioner名称不同,别搞混了。

4.4 StatefulSet:有状态应用的「身份证」

Deployment适合无状态应用,每个Pod都一样,谁替代谁都行。但数据库不一样——每个Pod都有自己的身份和存储。StatefulSet就是为此而生。

StatefulSet有几个关键特性:

  • 稳定的网络标识:每个Pod有固定的主机名,比如mysql-0、mysql-1,不会因为重启而改变
  • 稳定的存储:每个Pod绑定独立的PVC,Pod重建后还能挂载原来的数据
  • 有序的部署和伸缩:从0到N依次创建,从N到0依次删除

举个例子,部署一个三节点的MySQL集群:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql-headless
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: alicloud-disk-ssd
      resources:
        requests:
          storage: 100Gi

注意这里的volumeClaimTemplates,这是StatefulSet特有的。每个Pod都会自动生成一个PVC,命名规则是数据卷名-Pod名,比如data-mysql-0data-mysql-1

经验之谈:StatefulSet配合Headless Service使用效果最佳。Headless Service不提供负载均衡,而是让客户端直接通过Pod的DNS名称访问,这对于数据库的主从切换非常关键。

4.5 实战中的几个坑

说了这么多理论,来点实际的。我在生产环境部署中间件时,遇到过几个典型问题:

  1. PVC扩容问题:早期K8s版本不支持在线扩容,需要重建Pod。现在虽然支持了,但前提是StorageClass里设置了allowVolumeExpansion: true,而且底层存储驱动要支持。
  2. Pod调度与存储的亲和性:有些存储(比如本地SSD)只能被特定节点访问。这时候要用nodeSelectortopologyKey来确保Pod调度到正确的节点上。
  3. StatefulSet的滚动更新:默认是OrderedReady策略,Pod会按顺序更新。对于数据库集群,我建议用OnDelete策略,手动控制更新节奏,避免同时重启多个节点导致集群不可用。

我记得有一次,同事在线上环境直接修改了StatefulSet的replicas从3改成5,结果新Pod启动后一直Pending。排查了半天,发现是PVC的StorageClass对应的云盘配额用完了。嗯,动态供给虽然方便,但别忘了监控存储资源的使用情况。

4.6 总结一下

PV/PVC是Kubernetes存储的基础抽象,StorageClass实现了自动化,StatefulSet则是有状态应用的标配。这三者配合使用,才能在生产环境中跑好数据库、消息队列这类中间件。

最后说一句:别把Kubernetes存储想得太神秘。它本质上就是「声明式」地告诉系统:我要什么样的存储,系统帮我搞定。你只需要理解清楚每个组件的职责和边界,就能用好它。