k8s的PV与PVC的体系的最大的优点在于可以让用户定制化,而对k8s已有的用户几乎不造成影响。虽然k8s中很多设计看起来繁杂,但是正是这种解耦的设计,使得可扩展和兼容性大大提高。

k8s希望能够直接使用宿主机上的本地磁盘目录,而不依赖远程存储服务,来提供持久化的容器volume。本地SSD的读写性能相比于大部分远程存储来说都要好。

Local Persistent Volume的适用范围非常固定,比如:高优先级的系统应用,需要在多个不同节点上存储数据,并且对 I/O 较为敏感。典型的应用包括:分布式数据存储比如 MongoDB、Cassandra 等,分布式文件系统比如 GlusterFS、Ceph 等,以及需要在本地磁盘上进行大量数据缓存的分布式应用。

相比于正常的PV,一旦这些节点宕机且不能恢复时,Local Persistent Volume的数据就可能丢失。这就要求使用Local Persistent Volume的应用必须具备数据备份和恢复的能力,能够把这些数据定时备份在其它位置

Local Persistent Volume的设计难点

第一个难点在于:如何将本地磁盘抽象为PV

使用hostPath和nodeAffinity可以指定Pod运行的节点并使用本地的目录作为volume,看似好像与Local Persistent Volume一样,只要绑定的节点提前创建好了hostPath对应的目录。但是,事实上,绝不应该将宿主机上的目录当作PV使用。因为本地目录的存储行为不可控,它所在的磁盘随时可能被写满,甚至造成宿主机宕机。

Local Persistent Volume的存储介质一定是一块额外挂载在宿主机的磁盘或者块设备。一个PV一个盘。

第二个难点在于:调度器如何保证Pod 能够始终被调度到它所请求的Local Persistent Volume所在的节点

不同于普通PV,k8s是先调度Pod,然后在Pod所在节点通过两阶段处理持久化Volume目录,将Volume与容器挂载。Local PV是运维人员提前创建好的,要让Pod调度到Local PV所在的节点。因此调度器就必须能够知道所有节点与 Local Persistent Volume 对应的磁盘的关联关系,然后根据这个信息来调度 Pod。这个原则被称为“在调度的时候考虑Volume分布”。

Local PV的使用

在使用Local PV之前,必须在集群中配置好磁盘或者块设备。

以实践来说明如何使用, 首先,在名为node-1的节点上创建一个挂载点,比如/mnt/disks,这里是实验环境,所以用几个RAM disk来模拟本地磁盘并挂载到挂在点上。

# 在node-1上执行
$ mkdir /mnt/disks
$ for vol in vol1 vol2 vol3; do
    mkdir /mnt/disks/$vol
    mount -t tmpfs $vol /mnt/disks/$vol
done

想要支持的节点都要执行这些操作,并且确保磁盘的名字不会重复。

接下来就是为这些磁盘创建对应的PV了。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-pv
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /mnt/disks/vol1
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - node-1

这里的local字段就说明了这是一个Local PV,并且指定了本地磁盘的路径。定义中还有nodeAffinity字段,指明了节点的名字,这样Pod如果想要使用这个PV,那么它就必须运行在node-1这个节点上。

创建这个PV

$ kubectl create -f local-pv.yaml 
persistentvolume/example-pv created
 
$ kubectl get pv
NAME         CAPACITY   ACCESS MODES   RECLAIM POLICY  STATUS      CLAIM             STORAGECLASS    REASON    AGE
example-pv   5Gi        RWO            Delete           Available                     local-storage             16s

使用PV与PVC的最佳实践,是创建一个StorageClass,这里创建一个用来描述这个PV:

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

不过 Local Persistent Volume 目前尚不支持 Dynamic Provisioning,也就是说不会自动创建出来,所以需要前面的创建步骤是不能省略的

这个 StorageClass 还定义了一个==volumeBindingMode=WaitForFirstConsumer 的属性。它是 Local Persistent Volume 里一个非常重要的特性,即:延迟绑定==。

之所以需要延迟绑定是因为Local PV存在这样的情况,现在在节点node-1和node-2上都存在满足使用的PVC的PV,如果现在立即将PVC与node-1上的PV进行绑定,那么如果创建的Pod使用了这个PVC但同时指明了只能运行在node-2上,那么这个Pod的调度就会失败。

因此延迟绑定就这个绑定推迟到了调度时第一个声明使用该 PVC 的 Pod 出现在调度器之后,调度器再综合考虑所有的调度规则,当然也包括每个 PV 所在的节点位置,来统一决定,这个 Pod 声明的 PVC,到底应该跟哪个 PV 进行绑定

这样绑定的结果就不会影响Pod的调度了。

创建这个StorageClass

$ kubectl create -f local-sc.yaml 
storageclass.storage.k8s.io/local-storage created

然后声明使用这个StorageClass的PVC:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: example-local-claim
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: local-storage

同样创建出来

$ kubectl create -f local-pvc.yaml 
persistentvolumeclaim/example-local-claim created
 
$ kubectl get pvc
NAME                  STATUS    VOLUME    CAPACITY   ACCESS MODES   STORAGECLASS    AGE
example-local-claim   Pending                                       local-storage   7s

Kubernetes 的 Volume Controller 看到这个 PVC 的时候,不会为它进行绑定操作,所以状态是Pending。

然后编写一个Pod来使用这个Local PV

kind: Pod
apiVersion: v1
metadata:
  name: example-pv-pod
spec:
  volumes:
    - name: example-pv-storage
      persistentVolumeClaim:
       claimName: example-local-claim
  containers:
    - name: example-pv-container
      image: nginx
      ports:
        - containerPort: 80
          name: "http-server"
      volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: example-pv-storage

这个Pod创建之后,PVC就与前面的PV绑定了:

$ kubectl create -f local-pod.yaml 
pod/example-pv-pod created
 
$ kubectl get pvc
NAME                  STATUS    VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS    AGE
example-local-claim   Bound     example-pv   5Gi        RWO            local-storage   6h

Static Provisioner

上面的Local PV的手动创建和删除都比较麻烦。k8s提供了Static Provisioner来帮助管理。比如,我们现在的所有磁盘,都挂载在宿主机的 /mnt/disks 目录下。

Static Provisioner 启动后,它就会通过 DaemonSet,自动检查每个宿主机的 /mnt/disks 目录。然后,调用 Kubernetes API,为这些目录下面的每一个挂载,创建一个对应的 PV 对象出来。

这个 PV 里的各种定义,比如 StorageClass 的名字、本地磁盘挂载点的位置,都可以通过 provisioner 的配置文件指定。当然,provisioner 也会负责前面提到的 PV 的删除工作。