通过上一节课的学习,我们知道了如何在 Pod 中使用 Volume 来保存数据。Volume 跟 Pod 的生命周期是绑定的,当 Pod被删除后,Volume 中的数据有可能会一同被删除,具体需要看对应的 volume plugin 的使用要求,你可以看上节课的对比表格。

而这里我们还需要考虑如下几个问题。

共享 Volume。目前 Pod 内的 Volume 其实跟 Pod 是存在静态的一一绑定关系,即生命周期绑定。这导致不同 Pod 之间无法共享 Volume。

复用 Volume 中的数据。当 Pod 由于某种原因失败,被工作负载控制器删除重新创建后,我们需要能够复用 Volume 中的旧数据。

Volume 自身的一些强关联诉求。对于有状态工作负载 StatefulSet 来说,当其管理的 Pod 由于所在的宿主机出现一些硬件或软件问题,比如磁盘损坏、kernel 异常等,Pod 重新“长”到别的节点上,这时该如何保证 Volume 和 Pod 之间强关联的关系?

Volume 功能及语义扩展,比如容量大小、标签信息、扩缩容等。

为此我们在 Kubernetes 中引入了一个专门的对象 Persistent Volume(简称 PV),将计算和存储进行分离,可以使用不同的控制器来分别管理。

同时通过 PV,我们也可以和 Pod 自身的生命周期进行解耦。一个 PV 可以被几个 Pod 同时使用,即使 Pod 被删除后,PV 这个对象依然存在,其他新的 Pod 依然可以复用。为了更好地描述这种关联绑定关系,易于使用,并且屏蔽更多用户并不关心的细节参数(比如 PV 由谁提供、创建在哪个 zone/region、怎么去访问到,等等),我们通过一个抽象对象 Persistent Volume Claim(PVC)来使用 PV。

我们可以把 PV 理解成是对实际的物理存储资源描述,PVC 是便于使用的抽象 API。在 Kubernetes 中,我们都是在 Pod 中通过PVC 的方式来使用 PV 的,见下图。

(https://phoenixnap.com/kb/wp-content/uploads/2020/01/graphic-of-persistent-volume-bond.png)

在 Kubernetes 中,创建 PV(PV Provision) 有两种方式,即静态和动态,如下图所示。

(https://platform9.com/wp-content/uploads/2019/05/kubernetes-Persistent-volumes-claims-storage-classes.jpg)

静态 PV

(https://thenewstack.io/wp-content/uploads/2016/09/Kubernetes_PVC.png)

我们先来看静态 PV(Static PV),管理员通过手动的方式在后端存储平台上创建好对应的 Volume,然后通过 PV 定义到 Kubernetes 中去。开发者通过 PVC 来使用。我们来看个 HostPath 类型的 PV 例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: v1
kind: PersistentVolume
metadata:
  name: task-pv-volume # pv 的名字
  labels: # pv 的一些label
    type: local
spec:
  storageClassName: manual
  capacity: # 该 pv 的容量
    storage: 10Gi
  accessModes: # 该 pv 的接入模式
    - ReadWriteOnce
  hostPath: # 该 pv 使用的 hostpath 类型,还支持通过 CSI 接入其他 plugin
    path: "/mnt/data"

这里,我们定义了一个名为task-pv-volume的 PV,PV 是集群的资源,并不属于某个 namespace。其中storageClassName这个字段是某个StorageClass对象的名字。我们会在下一段动态 PV 中讲解StorageClass的作用。

对于每一个 PV,我们都要为其设置存储能力,目前只支持对存储空间的设置,比如我们这里设置了 10G 的空间大小。未来社区还会加入其他的配置,诸如 IOPS(Input/Output Operations Per Second,每秒输入输出次数)、吞吐量等。

这里头accessMode可以指定该 PV 的几种访问挂载方式:

ReadWriteOnce(RWO)表示该卷只可以以读写方式挂载到一个 Pod 内;

ReadOnlyMany( ROX)表示该卷可以挂载到多个节点上,并被多个 Pod 以只读方式挂载;

ReadWriteMany(RWX)表示卷可以被多个节点以读写方式挂载供多个 Pod 同时使用。

注意一个 PV 只能有一种访问挂载模式。不同的 volume plugin 支持的 accessMode 并不相同,在使用的时候,你可以参照官方的这个表格进行选择。

我们创建好后查看这个 PV:

1
2
3
$ kubectl get pv task-pv-volume
NAME             CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS      CLAIM     STORAGECLASS   REASON    AGE
task-pv-volume   10Gi       RWO           Retain          Available             manual                   4s

可以看到,这个 PV 的状态为Available(可用)。这里我们还看到上面 kubectl get 的输出里面有个 ReclaimPolicy 字段,该字段表明对 PV 的回收策略,默认是 Retain,即 PV 使用完后数据保留,需要由管理员手动清理数据。除了 Retain 外,还支持如下策略:

Recycle,即回收,这个时候会清除 PV 中的数据;

Delete,即删除,这个策略常在云服务商的存储服务中使用到,比如 AWS EBS。

下面我们再创建一个 PVC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: task-pv-claim
  namespace: dmeo
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 3Gi

创建好了以后,Kubernetes 会为 PVC 匹配满足条件的 PV。我们在 PVC 里面指定storageClassName为 manua,这个时候就只会去匹配storageClassName同样为 manual 的 PV。一旦发现合适的 PV 后,就可以绑定到该 PV 上。

PVC 是 namespace 级别的资源,我们来创建看看:

1
2
3
$ kubectl get pvc -n demo
NAME            STATUS   VOLUME           CAPACITY   ACCESS MODES   STORAGECLASS   AGE
task-pv-claim   Bound    task-pv-volume   10Gi       RWO            manual         9s

我们可以看到 这个 PVC 已经和我们上面的 PV 绑定起来了。我们再来查看下task-pv-volume这个 PV 对象,可以看到它的状态也从Available变成了 Bound。

1
2
3
$ kubectl get pv task-pv-volume
NAME             CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                   STORAGECLASS   REASON   AGE
task-pv-volume   10Gi       RWO            Retain           Bound    default/task-pv-claim   manual                  2m12s

PV 一般会有如下五种状态:

Pending 表示目前该 PV 在后端存储系统中还没创建完成;

Available 即闲置可用状态,这个时候还没有被绑定到任何 PVC 上;

Bound 就像上面例子里似的,这个时候已经绑定到某个 PVC 上了;

Released 表示已经绑定的 PVC 已经被删掉了,但资源还未被回收掉;

Failed 表示回收失败。

同样,对于 PVC 来说,也有如下三种状态:

Pending 表示还未绑定任何 PV;

Bound 表示已经和某个 PV 进行了绑定;

Lost 表示关联的 PV 失联。

下面我们来看看,如何在 Pod 中使用静态的 PV。看如下的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
apiVersion: v1
kind: Pod
metadata:
  name: task-pv-pod
  namespace: demo
spec:
  volumes:
    - name: task-pv-storage
      persistentVolumeClaim:
        claimName: task-pv-claim
  containers:
    - name: task-pv-container
      image: nginx:1.14.2
      ports:
        - containerPort: 80
          name: "http-server"
      volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: task-pv-storage

创建完成以后:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ kubectl get pod task-pv-pod -n demo
NAME          READY   STATUS    RESTARTS   AGE
task-pv-pod   1/1     Running   1          82s
$ kubectl exec -it task-pv-pod -n demo -- /bin/bash
root@task-pv-pod:/# df -h
Filesystem      Size  Used Avail Use% Mounted on
overlay          40G  5.0G   33G  14% /
tmpfs            64M     0   64M   0% /dev
tmpfs           996M     0  996M   0% /sys/fs/cgroup
/dev/vda1        40G  5.0G   33G  14% /etc/hosts
shm              64M     0   64M   0% /dev/shm
overlay         996M  4.0M  992M   1% /usr/share/nginx/html
tmpfs           996M   12K  996M   1% /run/secrets/kubernetes.io/serviceaccount
tmpfs           996M     0  996M   0% /proc/acpi
tmpfs           996M     0  996M   0% /sys/firmware

可以看到,PV 已经正确挂载到 Pod 内。静态 PV 最大的问题就是使用起来不够方便,都是管理员提前创建好一批指定规格的 PV,无法做到按需创建。使用过程中,经常会遇到由于资源大小不匹配,规格不对等,造成 PVC 无法绑定 PV 的情况。同时还会造成资源浪费,比如一个只需要 1G 空间的 Pod,绑定了 10G 的 PV。

这些问题,都可以通过动态 PV 来解决。

动态 PV

要想动态创建 PV,我们需要一些参数来帮助我们创建 PV。这里我们用StorageClass这个对象来描述,你可以在 Kubernetes 中定义很多的 StorageClass,如下就是一个 Storage 的定义例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-rbd-sc
  annotation:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: kubernetes.io/rbd # 必填项,用来指定volume plugin来创建PV的物理资源
parameters: # 一些参数
  monitors: 10.16.153.105:6789
  adminId: kube
  adminSecretName: ceph-secret
  adminSecretNamespace: kube-system
  pool: kube
  userId: kube
  userSecretName: ceph-secret-user
  userSecretNamespace: default
  fsType: ext4
  imageFormat: "2"
  imageFeatures: "layering"

你可以通过注释 storageclass.kubernetes.io/is-default-class 来指定默认的 StorageClass。这样新创建出来的 PVC 中的 storageClassName 字段就会自动使用默认的 StorageClass。

这里有个 provisioner 字段是必填项,主要用于指定使用那个 volume plugin 来创建 PV。没错,这里正是对应我们上节课讲过的 CSI driver 的名字。

现在我们来讲一下动态 PV 工作的过程:

(https://lenadroid.github.io/posts/cassandra-kube/4.png)

首先我们定义了一个 StorageClass。当用户创建好 Pod 以后,指定了 PVC,这个时候 Kubernetes 就会根据 StorageClass 中定义的 Provisioner 来调用对应的 plugin 来创建 PV。PV 创建成功后,跟 PVC 进行绑定,挂载到 Pod 中使用。

StatefulSet 中怎么使用 PV 和 PVC?

还记得我们之前讲 StatefulSet 中遗留的问题吗?对于 StatefulSet 管理的 Pod,每个 Pod 使用的 Volume 中的数据都不一样,而且相互之间关系是需要强绑定的。这个时候就不能在 StatefulSet 的 spec.template 去直接指向 PV 和 PVC了。于是我们在 StatefulSet 中使用了volumeClaimTemplate,有了这个 template 我们就可以为每一个 Pod 生成一个单独的 PVC,并且绑定 PV 了,从而实现有状态服务各个 Pod 都有自己专属的存储。这里生成的 PVC 名字跟 StatefulSet 的 Pod 名字一样,都是带有特定的序列号的。

你可以看看这里 StatefulSet 的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: k8s.gcr.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

写在最后

这节课我们讲了 PV、PVC 以及 StorageClass,它们直接的关系以及设计思路。你也许刚接触这几个概念的时候,有些稀里糊涂,但是通过分析各个对象要解决的问题,可以帮助你更好地掌握它们。

好的,如果你对本节课有什么想法或者疑问,欢迎你在留言区留言,我们一起讨论。

-– ### 精选评论 ##### **锋: > 这节课讲的非常好,看完又加深了PV/PVC的理解。老师能否给个动态PV绑定到pod的栗子(yaml)呢? ######     讲师回复: >     官方文档有个例子 https://kubernetes.io/zh/docs/tasks/configure-pod-container/configure-persistent-volume-storage/ ##### **伟: > StatefulSet 中怎么使用 PV 和 PVC给的样例没有看明白,volumeClaimTemplates的配置是如何和PVC做关联的? ##### **元: > 假如我创建了10G的PV,已经通过PVC挂载至Pod,用过一段时间后,发现空间不够,需要扩展PV,如何将它扩展至20G? ######     讲师回复: >     修改pvc的size ##### **星: > 有几个扩容的问题请教一下1、修改PVC后,已绑定的pod是不是不需要修改?2、如果是动态创建pv,修改了PVC以后会不会重新创建一个pv?原来的数据会不会丢失?3、如果是静态创建pv,扩容的时候是不是要同步修改pv和PVC? ##### **6250: > 挂载在pod内的pvc怎么扩容?