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 7sKubernetes 的 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 6hStatic Provisioner
上面的Local PV的手动创建和删除都比较麻烦。k8s提供了Static Provisioner来帮助管理。比如,我们现在的所有磁盘,都挂载在宿主机的 /mnt/disks 目录下。
Static Provisioner 启动后,它就会通过 DaemonSet,自动检查每个宿主机的 /mnt/disks 目录。然后,调用 Kubernetes API,为这些目录下面的每一个挂载,创建一个对应的 PV 对象出来。
这个 PV 里的各种定义,比如 StorageClass 的名字、本地磁盘挂载点的位置,都可以通过 provisioner 的配置文件指定。当然,provisioner 也会负责前面提到的 PV 的删除工作。