发展历程
为了能够屏蔽底层存储实现的细节,便于使用和管理,Kubernetes从1.0版本就引入PersistentVolume(PV)和PersistentVolumeClaim(PVC)两个资源对象来实现对存储的管理子系统。
PV是对底层网络共享存储的抽象,将共享存储定义为一种“资源”,比如Node也是一种容器应用可以“消费”的资源。PV由管理员创建和配置,它与共享存储的具体实现直接相关,例如GlusterFS、iSCSI、RBD或GCE或AWS公有云提供的共享存储,通过插件式的机制完成与共享存储的对接,以供应用访问和使用。
PVC则是用户对存储资源的一个“申请”。就像Pod“消费”Node的资源一样,PVC能够“消费”PV资源。PVC可以申请特定的存储空间和访问模式。
若使用PVC“申请”到一定的存储空间仍然不能满足应用对存储设备的需求。比如通常应用程序都会对存储设备的特性和性能有不同的要求,包括读写速度、并发性能、数据冗余等更高的要求,因此Kubernetes从1.4版本开始引入了一个新的资源对象StorageClass,用于标记存储资源的特性和性能。
Kubernetes 1.6版本时,StorageClass和动态资源供应的机制得到了完善,实现了存储卷的按需创建。通过StorageClass的定义,管理员可以将存储资源定义为某种类别(Class),正如存储设备对于自身的配置描述(Profile),例如“快速存储”“慢速存储”“有数据冗余”“无数据冗余”等。用户根据StorageClass的描述就能够直观地得知各种存储资源的特性,就可以根据应用对存储资源的需求去申请存储资源。
Kubernetes从1.9版本开始引入容器存储接口Container Storage Interface(CSI)机制,目标是在Kubernetes和外部存储系统之间建立一套标准的存储管理接口,通过该接口为容器提供存储服务,类似于CRI(容器运行时接口)和CNI(容器网络接口)。
PV和PVC使用介绍
对于PV和PVC使用有两种使用方式,一种静态方式创建,一种是动态方式创建;
静态方式创建
我们创建一个 hostPath 类型的 PersistentVolume。Kubernetes 支持 hostPath 类型的 PersistentVolume 使用节点上的文件或目录来模拟附带网络的存储,但是需要注意的是在生产集群中,我们不会使用 hostPath,集群管理员会提供网络存储资源,比如 NFS 共享卷或 Ceph 存储卷,集群管理员还可以使用 StorageClasses 来设置动态提供存储。因为 Pod 并不是始终固定在某个节点上面的,所以要使用 hostPath 的话我们就需要将 Pod 固定在某个节点上,这样显然降低了应用的容错性。
- 我们在demo-slave-2节点上创建如下目录/data/k8s/hostpath/index.html;
<!DOCTYPE html>
<html>
<head>
<title>hello world</title>
</head>
<body>
<p>hello world</p>
</body>
</html>
- 创建一个 hostPath 类型的 PV 资源对象,配置文件中指定了该卷位于集群节点上的 /data/k8s/hostpath 目录,还指定了 10G 大小的空间和 ReadWriteOnce 的访问模式,定义了名称为 manual 的 StorageClass,该名称用来将 PersistentVolumeClaim 请求绑定到该 PersistentVolum;
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-hostpath
labels:
type: local
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
storageClassName: slow
hostPath:
path: "/data/k8s/hostpath"
- 创建PV资源;
kubectl apply -f pv-hostpath.yaml
- 查看PersistentVolume的状态,PersistentVolume的状态为Available,表示可用状态,还未被任何 PVC 绑定,Reclaim Policy的状态为Retain,表示保留数据,需要管理员手工清理数据;
kubectl get pv pv-hostpath
- 创建一个对应的PVC来和PV进行绑定;
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-hostpath
spec:
resources:
requests:
storage: 1Gi
storageClassName: slow
accessModes:
- ReadWriteOnce
- 创建 PVC 之后,Kubernetes 就会去查找满足我们声明要求的PV;
kubectl create -f pvc-hostpath.yaml
- 接下来我们再次查看 PV 的信息,我们会发现PV已经处于被绑定状态;
- 接下来我们也看下PVC的信息,我们会发现PVC已经处于被绑定状态;
- 接下来我们创建Pod,声明的PVC作为存储卷,我们通过nodeSelector绑定demo-slave-2节点;
apiVersion: v1
kind: Pod
metadata:
name: pv-hostpath-pod
spec:
volumes:
- name: pv-hostpath
persistentVolumeClaim:
claimName: pvc-hostpath
nodeSelector:
kubernetes.io/hostname: demo-slave-2
containers:
- name: pv-hostpath-pod
image: nginx
ports:
- containerPort: 80
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: pv-hostpath
- 查看Pod创建的状态;
kubectl get pod pv-hostpath-pod
- 进入容器验证是否Pod是否从hostPath卷提供index.html文件,我们可以看到输出结果是我们前面写到 hostPath卷种的index.html 文件中的内容;
#进入容器
kubectl exec -it pv-hostpath-pod -- /bin/bash
#安装curl
apt-get update
apt-get install curl -y
#访问
curl localhost
动态方式创建
一个大规模的 Kubernetes 集群里很可能有成千上万个 PVC,这就意味着运维人员必须得事先创建出成千上万个 PV。更麻烦的是,随着新的 PVC 不断被提交,运维人员就不得不继续添加新的、能满足条件的 PV,否则新的 Pod 就会因为 PVC 绑定不到 PV 而失败。在实际操作中,这几乎没办法靠人工做,Kubernetes 为我们提供了一套可以自动创建 PV 的机制,这里我们采用Local Persistent Volum先做简单实验,但是Local Persistent Volum不支持StorageClass动态绑定,这里我们首先创建PV,先讲明白用法,当使用Kubernetes的支持的持久卷的是不需要创建PV的,动态创建的流程如下:
实验前Local Persistent Volum创建准备:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-local
spec:
capacity:
storage: 10Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
storageClassName: local-storage
local:
path: "/data/k8s/local"
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- demo-slave-1
- 创建StorageClass对象,StorageClass配置了volumeBindingMode=WaitForFirstConsumer,告诉 Kubernetes在发现这个StorageClass关联的PVC与PV进行的延迟绑定,通过这个延迟绑定机制;
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
- 查看StorageClass创建情况;
kubectl apply -f pv-local.yaml
- 创建PVC资源;
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pvc-local
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
storageClassName: local-storage
- 检查绑定状况,PVC处于Pending状态,也就是等待绑定的状态,原本实时发生的 PVC 和 PV 的绑定过程,就被延迟到了 Pod 第一次调度的时候在调度器中进行,从而保证了这个绑定结果不会影响 Pod 的正常调度;
kubectl get pvc
- 创建一个Pod来使用pvc-local这个PVC;
apiVersion: v1
kind: Pod
metadata:
name: pv-local-pod
spec:
volumes:
- name: pv-local-demo
persistentVolumeClaim:
claimName: pvc-local
containers:
- name: pv-local-pod
image: nginx
ports:
- containerPort: 80
volumeMounts:
- mountPath: /usr/share/nginx/html
name: pv-local-demo
- 查看PVC状况,这个时候我们会发现被绑定了;
核心概念介绍
PV
PV是对存储资源的抽象,将存储定义为容器可以使用的资源,管理员进行创建和配置。主要包括存储能力、访问模式、存储类型、回收策略、后端存储类型等关键信息的设置。
生命周期
PV在生命周期中可能处于以下4个阶段:
-
Available:可用状态,还未与某个PVC绑定;
-
Bound:已与某个PVC绑定;
-
Released:绑定的PVC已经删除,资源已释放,但没有被集群回收;
-
Failed:资源回收失败;
创建好一个 PV 以后,我们就处于一个 Available 的状态,当一个 PVC 和一个 PV 绑定的时候,这个 PV 就进入了 Bound 的状态,此时如果我们把 PVC 删掉,Bound 状态的 PV 就会进入 Released 的状态。
一个 Released 状态的 PV 会根据自己定义的 ReclaimPolicy 字段来决定自己是进入一个 Available 的状态还是进入一个 Deleted 的状态。如果 ReclaimPolicy 定义的是 recycle类型,它会进入一个Available状态,如果转变失败,就会进入 Failed 的状态。
PVC
PVC是用户对存储资源的一个申请,PVC消耗PV的资源。PVC可以申请存储大小和访问模式等。
生命周期介绍
PVC在生命周期中可能处于以下3个阶段:
-
Pending:等待与PV进行绑定;
-
Bound: PVC找到对应PV进行绑定;
-
Lost: 绑定状态的PVC,PV被删除掉;
一个创建好的 PVC 会处于 Pending 状态,当一个 PVC 与 PV 绑定之后,PVC 就会进入 Bound 的状态,当一个 Bound 状态的 PVC 的 PV 被删掉之后,该 PVC 就会进入一个 Lost 的状态。对于一个 Lost 状态的 PVC,它的 PV 如果又被重新创建,并且重新与该 PVC 绑定之后,该 PVC 就会重新回到 Bound 状态。
StorageClass
StorageClass作为对存储资源的抽象定义,充当 PV 的模板,对用户设置的PVC申请屏蔽后端存储的细节,既减少了用户对于存储资源细节的关注,又减轻了管理员手工管理PV的工作,由系统自动完成PV的创建和绑定,实现了动态的资源供应。StorageClass的定义主要包括名称、后端存储的提供者、后端存储的相关参数配置以及回收策略。
CSI
CSI是什么
CSI是Container Storage Interface(容器存储接口)的简写。CSI的目的是定义行业标准容器存储接口,使存储供应商能够开发一个符合CSI标准的插件并使其可以在多个容器编排系统中工作。Kubernetes将通过CSI接口来跟第三方存储厂商进行通信,来操作存储,从而提供容器存储服务。
为什么要有CSI
其实在没有CSI之前Kubernetes就已经提供了强大的存储卷插件系统,但是这些插件系统实现是Kubernetes代码的一部分,需要随Kubernetes组件二进制文件一起发布,这样就会存在一些问题。
- 如果第三方存储厂商发现有问题需要修复或者优化,即使修复后也不能单独发布,需要与Kubernetes一起发布,对于k8s本身而言,不仅要考虑自身的正常迭代发版,还需要考虑到第三方存储厂商的迭代发版,这里就存在双方互相依赖、制约的问题,不利于双方快速迭代;
- 另外第三方厂商的代码跟Kubernetes代码耦合在一起,还会引起安全性、可靠性问题,还增加了Kubernetes代码的复杂度以及后期的维护成本等等。
基于以上问题,Kubernetes将存储体系抽象出了外部存储组件接口即CSI,Kubernetes通过grpc接口与第三方存储厂商的存储卷插件系统进行通信。
这样一来,对于第三方存储厂商来说,既可以单独发布和部署自己的存储插件,进行正常迭代,而又无需接触Kubernetes核心代码,降低了开发的复杂度。同时,对于Kubernetes来说,这样不仅降低了自身的维护成本,还能为用户提供更多的存储选项。
CSI系统架构
核心概念介绍
volume plugin
扩展各种存储类型的卷的管理能力,实现第三方存储的各种操作能力与Kubernetes存储系统的结合。调用第三方存储的接口或命令,从而提供数据卷的创建/删除、attach/detach、mount/umount的具体操作实现,可以认为是第三方存储的代理人。根据源码所在位置,volume plugin分为in-tree与out-of-tree。
in-tree
在Kubernetes源码内部实现,和Kubernetes一起发布、管理,更新迭代慢、灵活性差;
out-of-tree
代码独立于Kubernetes,由存储厂商实现,有CSI、FlexVolume两种实现;
CSI-plugin
Kubernetes独立拆分出来,实现 CSI 标准规范接口的逻辑控制与调用,是整个 CSI 控制逻辑的核心枢纽;
Node-driver-registrar
一个由官方 Kubernetes维护的辅助容器(sidecar),它使用 kubelet 插件注册机制向 kubelet 注册插件,需要请求 CSI 插件的 Identity 服务来获取插件信息;
external-provisioner
一个由官方 Kubernetes维护的辅助容器(sidecar),主要功能是实现持久卷的创建(Create)、删除(Delete);
external-attacher
一个由官方 Kubernetes维护的辅助容器(sidecar),主要功能是实现持久卷的附着(Attach)、分离(Detach);
external-snapshotter
一个由官方 Kubernetes小组维护的辅助容器(sidecar),主要功能是实现持久卷的快照(VolumeSnapshot)、备份恢复等能力;
external-resizer
一个由官方Kubernetes小组维护的辅助容器(sidecar),主要功能是实现持久卷的弹性扩缩容,需要云厂商插件提供相应的能力;
kube-controller-manager
Kubernetes资源控制器,主要通过 PV Controller, AttachDetach 实现持久卷的绑定(Bound)/解绑(Unbound)、附着(Attach)/分离(Detach);
PV Controller
负责pv、pvc的绑定与生命周期管理(如创建/删除底层存储,创建/删除pv对象,pv与pvc对象的状态变更);
in-tree:创建/删除底层存储、创建/删除pv对象的操作,由PV controller调用volume plugin(in-tree)来完成;
out-tree CSI:创建/删除底层存储、创建/删除pv对象的操作由external-provisioner与csi plugin共同来完成;
AD Controller
AD Cotroller全称Attachment/Detachment 控制器,主要负责创建、删除VolumeAttachment对象,并调用volume plugin来做存储设备的Attach/Detach操作(将数据卷挂载到特定node节点上/从特定node节点上解除挂载),以及更新node.Status.VolumesAttached等;
不同的volume plugin的Attach/Detach操作逻辑有所不同,对于csi plugin(out-tree volume plugin)来说,AD controller的Attach/Detach操作只是修改VolumeAttachment对象的状态,而不会真正的将数据卷挂载到节点/从节点上解除挂载,真正的节点存储挂载/解除挂载操作由kubelet中volume manager调用csi plugin来完成;
volume manager
主要是管理卷的Attach/Detach(与AD controller作用相同,通过kubelet启动参数控制哪个组件来做该操作)、mount/umount等操作;
对于CSI来说,volume manager的Attach/Detach操作只创建/删除VolumeAttachment对象,而不会真正的将数据卷挂载到节点/从节点上解除挂载;csi-attacer组件也不会做挂载/解除挂载操作,只是更新VolumeAttachment对象,真正的节点存储挂载/解除挂载操作由kubelet中volume manager调用调用csi plugin来完成。
Kubernetes创建与挂载Volume
in-tree
-
用户创建PVC;
-
PV Controller watch到PVC的创建,寻找合适的PV与之绑定;
-
当找不到合适的PV时,将调用volume plugin来创建volume,并创建PV对象,之后该PV对象与PVC对象绑定;
-
用户创建挂载PVC的Pod;
-
kube-scheduler watch到Pod的创建,为其寻找合适的Node调度;
-
Pod调度完成后,AD controller/volume manager watch到Pod声明的volume没有进行attach操作,将调用volume plugin来做attach操作;
-
volume plugin进行attach操作,将volume挂载到pod所在node节点,成为如/dev/vdb的设备;
-
attach操作完成后,volume manager watch到Pod声明的volume没有进行mount操作,将调用volume plugin来做mount操作;
-
volume plugin进行mount操作,将Node节点上的得到的/dev/vdb设备挂载到指定目录;
out-of-tree
-
用户创建PVC;
-
PV Controller watch到PVC的创建,寻找合适的PV与之绑定。当寻找不到合适的PV时,让external-provisioner组件开始开始创建存储与PV对象的操作;
-
external-provisioner watch到PVC的创建/更新事件,通过判断PVC的annotation[volume.beta.kubernetes.io/storage-provisioner]是否与自己的provisioner名称相等,用来判断PVC是否需要动态创建存储卷,是则调用csi-plugin ControllerServer来创建存储,并创建PV对象;
-
PV Controller将上一步创建的PV与PVC绑定;
-
用户创建挂载PVC的Pod;
-
kube-scheduler watch到Pod的创建,为其寻找合适的Node调度;
-
Pod调度完成后,AD Controller/volume manager watch到Pod声明的volume没有进行attach操作,将调用csi-attacher来做attach操作,实际上只是创建VolumeAttachement对象;
-
external-attacher组件watch到VolumeAttachment对象的创建,调用csi-plugin进行attach操作;
-
csi-plugin ControllerServer进行attach操作,将volume挂载到Pod所在Node节点,成为如/dev/vdb的设备;
-
attach操作完成后,volume manager watch到Pod声明的volume没有进行mount操作,将调用csi-mounter来做mount操作;
-
csi-mounter调用csi-plugin NodeServer进行mount操作,将Node节点上的/dev/vdb设备挂载到指定目录;