为了方便开发人员更加容易的使用存储才出现的概念。一般咱们在一个POD中定义使用存储是这样的方式,咱们以hostpath类型来讲:html
apiVersion: v1 kind: Pod metadata: name: mypod spec: containers: - image: nginx name: mynginx volumeMounts: - mountPath: /usr/share/nginx/html name: html volumes: - name: html # 名称 hostPath: # 存储类型 path: /data # 物理节点上的真实路径 type: Directory # 若是该路径不存在讲如何处理,Directory是要求目录必须存在
其实经过上面能够看出来,不管你使用什么类型的存储你都须要手动定义,指明存储类型以及相关配置。这里的hostpath类型仍是比较简单的,若是是其余类型的好比分布式存储,那么这对开发人员来讲将会是一种挑战,由于毕竟真正的存储是由存储管理员来设置的他们会更加了解,那么有没有一种方式让咱们使用存储更加容易,对上层使用人员屏蔽底层细节呢?答案是确定的,这就是PV、PVC的概念。不过须要注意的是咱们在集群中一般不使用hostPath、emptyDir这种类型,除非你只是测试使用。node
PV全称叫作Persistent Volume,持久化存储卷。它是用来描述或者说用来定义一个存储卷的,这个一般都是有运维或者数据存储工程师来定义。好比下面咱们定义一个NFS类型的PV:nginx
apiVersion: v1 kind: PersistentVolume metadata: # PV创建不要加名称空间,由于PV属于集群级别的 name: nfs-pv001 # PV名称 labels: # 这些labels能够不定义 name: nfs-pv001 storetype: nfs spec: # 这里的spec和volumes里面的同样 storageClassName: normal accessModes: # 设置访问模型 - ReadWriteMany - ReadWriteOnce - ReadOnlyMany capacity: # 设置存储空间大小 storage: 500Mi persistentVolumeReclaimPolicy: Retain # 回收策略 nfs: path: /work/volumes/v1 server: stroagesrv01.contoso.com
accessModes:支持三种类型git
ReadWriteMany 多路读写,卷能被集群多个节点挂载并读写github
ReadWriteOnce 单路读写,卷只能被单一集群节点挂载读写算法
ReadOnlyMany 多路只读,卷能被多个集群节点挂载且只能读docker
这里的访问模型总共有三种,可是不一样的存储类型支持的访问模型不一样,具体支持什么须要查询官网。好比咱们这里使用nfs,它支持所有三种。可是ISCI就不支持ReadWriteMany;HostPath就不支持ReadOnlyMany和ReadWriteMany。api
persistentVolumeReclaimPolicy:也有三种策略,这个策略是当与之关联的PVC被删除之后,这个PV中的数据如何被处理tomcat
Retain 当删除与之绑定的PVC时候,这个PV被标记为released(PVC与PV解绑但尚未执行回收策略)且以前的数据依然保存在该PV上,可是该PV不可用,须要手动来处理这些数据并删除该PV。服务器
Delete 当删除与之绑定的PVC时候
Recycle 这个在1.14版本中以及被废弃,取而代之的是推荐使用动态存储供给策略,它的功能是当删除与该PV关联的PVC时,自动删除该PV中的全部数据
注意:PV必须先与POD建立,并且只能是网络存储不能属于任何Node,虽然它支持HostPath类型但因为你不知道POD会被调度到哪一个Node上,因此你要定义HostPath类型的PV就要保证全部节点都要有HostPath中指定的路径。
PVC是用来描述但愿使用什么样的或者说是知足什么条件的存储,它的全称是Persistent Volume Claim,也就是持久化存储声明。开发人员使用这个来描述该容器须要一个什么存储。好比下面使用NFS的PVC:
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: nfs-pvc001 namespace: default labels: # 这些labels能够不定义 name: nfs-pvc001 storetype: nfs capacity: 500Mi spec: storageClassName: normal accessModes: # PVC也须要定义访问模式,不过它的模式必定是和现有PV相同或者是它的子集,不然匹配不到PV - ReadWriteMany resources: # 定义资源要求PV知足这个PVC的要求才会被匹配到 requests: storage: 500Mi # 定义要求有多大空间
这个PVC就会和上面的PV进行绑定,为何呢?它有一些原则:
PV和PVC中的spec关键字段要匹配,好比存储(storage)大小。
PV和PVC中的storageClassName字段必须一致,这个后面再说。
上面的labels中的标签只是增长一些描述,对于PVC和PV的绑定没有关系
应用了上面的PV和PVC,能够看到自动绑定了。
apiVersion: apps/v1 kind: Deployment metadata: name: tomcat-deploy spec: replicas: 1 selector: matchLabels: appname: myapp template: metadata: name: myapp labels: appname: myapp spec: containers: - name: myapp image: tomcat:8.5.38-jre8 ports: - name: http containerPort: 8080 protocol: TCP volumeMounts: - name: tomcatedata mountPath : "/data" volumes: - name: tomcatedata persistentVolumeClaim: claimName: nfs-pvc001
这里经过volumes来声明使用哪一个PVC,能够看到和本身定义持久化卷相似,可是这里更加简单了,直接使用PVC的名字便可。在容器中使用/data目录就会把数据写入到NFS服务器上的目录中。
当咱们删除那个PVC的时候,该PV变成Released状态,因为咱们的策略是Retain,因此若是想让这个PV变为可用咱们就须要手动清理数据并删除这个PV。这里你可能会以为矛盾,你让这个PV变为可用,为何还要删除这个PV呢?其实所谓可用就是删除这个PV而后创建一个同名的。
能够看出来PVC就至关因而容器和PV之间的一个接口,使用人员只须要和PVC打交道便可。另外你可能也会想到若是当前环境中没有合适的PV和个人PVC绑定,那么我建立的POD不就失败了么?的确是这样的,不过若是发现这个问题,那么就赶快建立一个合适的PV,那么这时候持久化存储循环控制器会不断的检查PVC和PV,当发现有合适的能够绑定以后它会自动给你绑定上而后被挂起的POD就会自动启动,而不须要你重建POD。
咱们知道所谓容器挂载卷就是将宿主机的目录挂载到容器中的某个目录。而持久化则意味着这个目录里面的内容不会由于容器被删除而清除,也不会和当前宿主机有什么直接关系,而是一个外部的。这样当POD重建之后或者在其余主机节点上启动后依然能够访问这些内容。不过以前说过hostPath和emptyDir则推荐使用,由于前者和当前宿主机有必然联系然后者就是一个随POD删除而被删除的临时目录。
挂载过程会有不一样,这取决于远程存储的类型,它是块设备存储仍是文件设备存储。可是无论怎么样POD有这样一个目录/var/lib/kubelet/pods/<Pod 的 ID>/volumes/kubernetes.io~<Volume 类型 >/<Volume 名字 >
这个目录是POD被调度到该节点以后,由kubelet为POD建立的。由于它必定会被建立,由于系统中的默认secret就会被挂载到这里。以后就要根据存储设备类型的不一样作不一样处理。
以nfs这种文件设备存储来讲。咱们依然启动以前的容器继续使用以前的PVC。
因为这个POD运行在node01节点,咱们登录node01节点,查看这个目录
/var/lib/kubelet/pods/<Pod 的 ID>/volumes/kubernetes.io~<Volume 类型 >/<Volume 名字 >
当你建立POD的时候它因为它被调度到node01节点,因此会建立这个目录,并且根据YAML中的定义就也会在这个目录中建立你在volumesMount中定义的目录,以下图:
经过命令查看在本地宿主机的挂载状况
因为建立了必要的目录,那么kubelet就直接使用mount命令把nfs目录挂载到这个目录上volumes/kubernetes.io~<type>/<Volume 名字>
,注意这时候仅仅是把这个远程存储挂载到宿主机目录上,要想让容器使用还须要作调用相关接口来把这个宿主机上的目录挂载到容器上。因此当准备好以后启动容器的时候就是利用CRI里的mounts参数把这个宿主机的目录挂载到容器中指定的目录上,就至关于执行docker run -v
。
不过须要注意的是因为nfs文件存储不是一个块设备,因此宿主机系统须要扮演的就是nfs客户端角色,kubelet就是调用这个客户端工具来完成挂载的。
块存储设备你能够理解为一个磁盘。这个的处理要稍微复杂一点,就好像你为Linux服务器添加一块磁盘同样,你得先安装而后分区格式化以后挂载到某个目录使用。
/var/lib/kubelet/pods/<Pod 的 ID>/volumes/kubernetes.io~<Volume 类型 >/<Volume 名字 >
这个目录依然会建立。当POD被调度到该节点上会作以下操做
首先要安装一个块设备存储到宿主机(不是物理安装,而是经过API来安装),如何安装取决于不一样块存储设备的API,不少云厂商有这种块存储设备好比Google的GCE。
格式化磁盘,
把格式化好的磁盘设备挂载到宿主机上的目录
启动容器挂载宿主机上的目录到容器中
相对于文件设备存储来讲块设备要稍微复杂一点,不过上面这些过程都是自动的有kubelet来完成。
负责把PVC绑定到PV的是一个持久化存储卷控制循环,这个控制器也是kube-manager-controller的一部分运行在master上。而真正把目录挂载到容器上的操做是在POD所在主机上发生的,因此经过kubelet来完成。并且建立PV以及PVC的绑定是在POD被调度到某一节点以后进行的,完成这些操做,POD就能够运行了。下面梳理一下挂载一个PV的过程:
用户提交一个包含PVC的POD
调度器把根据各类调度算法把该POD分配到某个节点,好比node01
Node01上的kubelet等待Volume Manager准备存储设备
PV控制器调用存储插件建立PV并与PVC进行绑定
Attach/Detach Controller或Volume Manager经过存储插件实现设备的attach。(这一步是针对块设备存储)
Volume Manager等待存储设备变为可用后,挂载该设备到/var/lib/kubelet/pods/<Pod 的 ID>/volumes/kubernetes.io~<Volume 类型 >/<Volume 名字 >
目录上
Kubelet被告知卷已经准备好,开始启动POD,经过映射方式挂载到容器中
PV是运维人员来建立的,开发操做PVC,但是大规模集群中可能会有不少PV,若是这些PV都须要运维手动来处理这也是一件很繁琐的事情,因此就有了动态供给概念,也就是Dynamic Provisioning。而咱们上面的建立的PV都是静态供给方式,也就是Static Provisioning。而动态供给的关键就是StorageClass,它的做用就是建立PV模板。
建立StorageClass里面须要定义PV属性好比存储类型、大小等;另外建立这种PV须要用到存储插件。最终效果是,用户提交PVC,里面指定存储类型,若是符合咱们定义的StorageClass,则会为其自动建立PV并进行绑定。
咱们这里演示一下NFS的动态PV建立
kubernetes自己支持的动态PV建立不包括nfs,因此须要使用额外插件实现。nfs-client
我这里就按照网站的例子来建立,里面的内容毫无修改,固然你须要本身准备NFS服务器。因为用于提供动态建立PV的程序是运行在POD中,因此你须要保证你的Kubernetes节点到NFS的网络通畅,我这里就在个人Kubernetes集群的某个节点上创建的NFS服务。下面是PVC文件
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mytomcat-pvc spec: storageClassName: managed-nfs-storage accessModes: - ReadWriteMany resources: requests: storage: 500Mi
当你应用这个PVC的时候,因为例子中的storageClassName也是managed-nfs-storage(固然这个名字你能够修改)就会去自动建立PV。
下图是在Node02这个节点上看到的
基于这种形式,咱们只须要根据咱们有的存储系统来定义StorageClass,经过名称来标识不一样种类的存储,好比SSD、block-device这种名称,而不须要定义具体大小。那么使用人员就能够根据须要经过StorageClass的名字来使用,从而实现动态建立PV的过程。
这里有个要求就是你的存储系统须要提供某种接口来让controller能够调用并传递进去PVC的参数去建立PV,不少云存储都支持。但是也有不支持的,好比NFS就不支持因此咱们须要一个单独的插件来完成这个工做。也就是例子中使用的quay.io/external_storage/nfs-client-provisioner
镜像,可是建立PV也须要相关权限,也就是例子中rabc.yaml部分。在定义StorageClass中有一个叫作provisioner: fuseim.pri/ifs
这个就是插件的名称,这个名称其实也就是官方例子中deployment中设置的名字,这个名字你能够修改。
固然咱们说过有些自己就支持,好比下面的kubernetes官网中的一个AWS的例子:
kind: StorageClass apiVersion: storage.k8s.io/v1 metadata: name: slow provisioner: kubernetes.io/aws-ebs parameters: type: io1 iopsPerGB: "10" fsType: ext4
kubernetes.io/aws-ebs
就是kubernetes内置的存储插件名称,若是你使用aws就用这个名称就好。由于kubernetes就会去调用AWS的API来建立存储而后在建立PV。
这里你可能会有个疑问,为何开篇的例子里面也用了storageClassName: normal
,但是咱们并无定义任何StorageClass。其实虽然咱们使用了,可是系统上并无一个叫作normal的存储类,这时候仍是静态绑定,只是绑定的时候它会考虑你的PV和PVC中的存储类名称是否一致。固然若是是静态绑定你能够不写storageClassName
,由于若是开起一个的叫作DefaultStorageClass
plugin插件就会默认有这样一个存储类,它会自动添加到你的任何没有明确声明storageClassName
的PV和PVC中。
本地持久化存储(Local Persistent Volume)就是把数据存储在POD运行的宿主机上,咱们知道宿主机有hostPath和emptyDir,因为这两种的特定不适用于本地持久化存储。那么本地持久化存储必须能保证POD被调度到具备本地持久化存储的节点上。
为何须要这种类型的存储呢?有时候你的应用对磁盘IO有很高的要求,网络存储性能确定不如本地的高,尤为是本地使用了SSD这种磁盘。
但这里有个问题,一般咱们先建立PV,而后建立PVC,这时候若是二者匹配那么系统会自动进行绑定,哪怕是动态PV建立,也是先调度POD到任意一个节点,而后根据PVC来进行建立PV而后进行绑定最后挂载到POD中,但是本地持久化存储有一个问题就是这种PV必需要先准备好,并且不必定集群全部节点都有这种PV,若是POD随意调度确定不行,如何保证POD必定会被调度到有PV的节点上呢?这时候就须要在PV中声明节点亲和,且POD被调度的时候还要考虑卷的分布状况。
定义PV
apiVersion: v1 kind: PersistentVolume metadata: name: example-pv spec: capacity: storage: 5Gi volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Delete storageClassName: local-storage local: # local类型 path: /data/vol1 # 节点上的具体路径 nodeAffinity: # 这里就设置了节点亲和 required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - node01 # 这里咱们使用node01节点,该节点有/data/vol1路径
若是你在node02上也有/data/vol1这个目录,上面这个PV也必定不会在node02上,由于下面的nodeAffinity设置了主机名就等于node01。
另外这种本地PV一般推荐使用的是宿主机上单独的硬盘设备,而不是和操做系统共有一块硬盘,虽然能够这样用。
定义存储类
kind: StorageClass apiVersion: storage.k8s.io/v1 metadata: name: local-storage provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer
这里的volumeBindingMode: WaitForFirstConsumer
很关键,意思就是延迟绑定,当有符合PVC要求的PV不当即绑定。由于POD使用PVC,而绑定以后,POD被调度到其余节点,显然其余节点颇有可能没有那个PV因此POD就挂起了,另外就算该节点有合适的PV,而POD被设置成不能运行在该节点,这时候就无法了,延迟绑定的好处是,POD的调度要参考卷的分布。当开始调度POD的时候看看它要求的LPV在哪里,而后就调度到该节点,而后进行PVC的绑定,最后在挂载到POD中,这样就保证了POD所在的节点就必定是LPV所在的节点。因此让PVC延迟绑定,就是等到使用这个PVC的POD出如今调度器上以后(真正被调度以前),而后根据综合评估再来绑定这个PVC。
定义PVC
kind: PersistentVolumeClaim apiVersion: v1 metadata: name: local-claim spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi storageClassName: local-storage
能够看到这个PVC是pending状态,这也就是延迟绑定,由于此时尚未POD。
定义POD
apiVersion: apps/v1 kind: Deployment metadata: name: tomcat-deploy spec: replicas: 1 selector: matchLabels: appname: myapp template: metadata: name: myapp labels: appname: myapp spec: containers: - name: myapp image: tomcat:8.5.38-jre8 ports: - name: http containerPort: 8080 protocol: TCP volumeMounts: - name: tomcatedata mountPath : "/data" volumes: - name: tomcatedata persistentVolumeClaim: claimName: local-claim
这个POD被调度到node01上,由于咱们的PV就在node01上,这时候你删除这个POD,而后在重建该POD,那么依然会被调度到node01上。
总结:本地卷也就是LPV不支持动态供给的方式,延迟绑定,就是为了综合考虑全部因素再进行POD调度。其根本缘由是动态供给是先调度POD到节点,而后动态建立PV以及绑定PVC最后运行POD;而LPV是先建立与某一节点关联的PV,而后在调度的时候综合考虑各类因素并且要包括PV在哪一个节点,而后再进行调度,到达该节点后在进行PVC的绑定。也就说动态供给不考虑节点,LPV必须考虑节点。因此这两种机制有冲突致使没法在动态供给策略下使用LPV。换句话说动态供给是PV跟着POD走,而LPV是POD跟着PV走。