Docker 当我们使用Docker时,设置数据卷(Volume)还是比较简单的,只需要在容器映射指定卷的路径,然后在容器中使用该路径即可。 比如这种:tomcattomcat01:hostname:tomcat01restart:alwaysimage:jdktomcat:v8containername:tomcat81links:mysql:mysqlvolumes:homesoftdockertomcatwebapps:usrlocalapachetomcat8。5。39webappshomesoftdockertomcatlogs:usrlocalapachetomcat8。5。39logsetclocaltime:etclocaltimeenvironment:JAVAOPTS:Dspring。profiles。activeprodTZ:AsiaShanghaiLANG:C。UTF8LCALL:zhCN。UTF8envfile:homesoftdockerenvtomcat。env 为什么要设置Volume?当然是因为我们要持久化数据,要把数据存储到硬盘上。k8s 到了k8s这儿,你会发现事情没那么简单了,涌现出了一堆概念:PvPvcStorageClassProvisioner。。。 先不管这些复杂的概念,我只想存个文件,有没有简单的方式? 有,我们先回顾下基本概念。 我们知道,Container中的文件在磁盘上是临时存放的,当容器崩溃时文件丢失。kubelet会重新启动容器,但容器会以干净的状态重启。所以我们要使用Volume来持久化数据。 Docker也有卷(Volume)的概念,但对它只有少量且松散的管理。Docker卷是磁盘上或者另外一个容器内的一个目录Docker提供卷驱动程序,但是其功能非常有限。 Kubernetes支持很多类型的卷。Pod可以同时使用任意数目的卷类型。 临时卷类型的生命周期与Pod相同,但持久卷可以比Pod的存活期长。当Pod不再存在时,Kubernetes也会销毁临时卷;不过Kubernetes不会销毁持久卷。对于给定Pod中任何类型的卷,在容器重启期间数据都不会丢失。 卷的核心是一个目录,其中可能存有数据,Pod中的容器可以访问该目录中的数据。所采用的特定的卷类型将决定该目录如何形成的、使用何种介质保存数据以及目录中存放的内容。 使用卷时,在。spec。volumes字段中设置为Pod提供的卷,并在。spec。containers〔〕。volumeMounts字段中声明卷在容器中的挂载位置。各个卷则挂载在镜像内的指定路径上。卷不能挂载到其他卷之上,也不能与其他卷有硬链接。Pod配置中的每个容器必须独立指定各个卷的挂载位置。 通过上面的概念我们知道Volume有不同的类型,有临时的,也有持久的,那么我们先说说简单的,即解决我只想存个文件,有没有简单的方式的需求。hostPath hostPath卷能将主机节点文件系统上的文件或目录挂载到你的Pod中。看个示例:apiVersion:v1kind:Podmetadata:name:testwebserverspec:containers:name:testwebserverimage:k8s。gcr。iotestwebserver:latestvolumeMounts:mountPath:varlocalaaaname:mydirmountPath:varlocalaaa1。txtname:myfilevolumes:name:mydirhostPath:确保文件所在目录成功创建。path:varlocalaaatype:DirectoryOrCreatename:myfilehostPath:path:varlocalaaa1。txttype:FileOrCreate 通过hostPath能够简单解决文件在宿主机上存储的问题。 不过需要注意的是: HostPath卷存在许多安全风险,最佳做法是尽可能避免使用HostPath。当必须使用HostPath卷时,它的范围应仅限于所需的文件或目录,并以只读方式挂载。 使用hostPath还有一个局限性就是,我们的Pod不能随便漂移,需要固定到一个节点上,因为一旦漂移到其他节点上去了宿主机上面就没有对应的数据了,所以我们在使用hostPath的时候都会搭配nodeSelector来进行使用。emptyDir emptyDir也是比较常见的一种存储类型。 上面的hostPath显示的定义了宿主机的目录。emptyDir类似隐式的指定。 Kubernetes会在宿主机上创建一个临时目录,这个目录将来就会被绑定挂载到容器所声明的Volume目录上。而Pod中的容器,使用的是volumeMounts字段来声明自己要挂载哪个Volume,并通过mountPath字段来定义容器内的Volume目录 当Pod分派到某个Node上时,emptyDir卷会被创建,并且在Pod在该节点上运行期间,卷一直存在。就像其名称表示的那样,卷最初是空的。尽管Pod中的容器挂载emptyDir卷的路径可能相同也可能不同,这些容器都可以读写emptyDir卷中相同的文件。当Pod因为某些原因被从节点上删除时,emptyDir卷中的数据也会被永久删除。 apiVersion:v1kind:Podmetadata:name:testpdspec:containers:image:k8s。gcr。iotestwebservername:testcontainervolumeMounts:mountPath:cachename:cachevolumevolumes:name:cachevolumeemptyDir:{} 如果执行kubectldescribe命令查看pod信息的话,可以验证前面我们说的内容:EmptyDir(atemporarydirectorythatsharesapodslifetime)。。。Containers:nginx:ContainerID:docker:07b4f89248791c2aa47787e3da3cc94b48576cd173018356a6ec8db2b6041343Image:nginx:1。8。。。Environment:noneMounts:usrsharenginxhtmlfromnginxvol(rw)。。。Volumes:nginxvol:Type:EmptyDir(atemporarydirectorythatsharesapodslifetime)PV和PVCPV(PersistentVolume):持久化卷PVC(PersistentVolumeClaim):持久化卷声明 PV和PVC的关系就像java中接口和实现的关系类似。 PVC是用户存储的一种声明,PVC和Pod比较类似,Pod消耗的是节点,PVC消耗的是PV资源,Pod可以请求CPU和内存,而PVC可以请求特定的存储空间和访问模式。对于真正使用存储的用户不需要关心底层的存储实现细节,只需要直接使用PVC即可。 PV是对底层共享存储的一种抽象,由管理员进行创建和配置,它和具体的底层的共享存储技术的实现方式有关,比如Ceph、GlusterFS、NFS、hostPath等,都是通过插件机制完成与共享存储的对接。 我们来看一个例子: 比如,运维人员可以定义这样一个NFS类型的PVapiVersion:v1kind:PersistentVolumemetadata:name:nfsspec:storageClassName:manualcapacity:storage:1GiaccessModes:ReadWriteManynfs:server:10。244。1。4path: PVC描述的,则是Pod所希望使用的持久化存储的属性。比如,Volume存储的大小、可读写权限等等。apiVersion:v1kind:PersistentVolumeClaimmetadata:name:nfsspec:accessModes:ReadWriteManystorageClassName:manualresources:requests:storage:1Gi 用户创建的PVC要真正被容器使用起来,就必须先和某个符合条件的PV进行绑定。第一个条件是PV和PVC的spec字段。比如,PV的存储(storage)大小,就必须满足PVC的要求。第二个条件,则是PV和PVC的storageClassName字段必须一样 在成功地将PVC和PV进行绑定之后,Pod就能够像使用hostPath等常规类型的Volume一样,在自己的YAML文件里声明使用这个PVC了apiVersion:v1kind:Podmetadata:labels:role:webfrontendspec:containers:name:webimage:nginxports:name:webcontainerPort:80volumeMounts:name:nfsmountPath:usrsharenginxhtmlvolumes:name:nfspersistentVolumeClaim:claimName:nfs 我们前面使用的hostPath和emptyDir类型的Volume并不具备持久化特征,既有可能被kubelet清理掉,也不能被迁移到其他节点上。所以,大多数情况下,持久化Volume的实现,往往依赖于一个远程存储服务,比如:远程文件存储(比如,NFS、GlusterFS)、远程块存储(比如,公有云提供的远程磁盘)等等。StorageClass 前面我们人工管理PV的方式就叫作StaticProvisioning。 一个大规模的Kubernetes集群里很可能有成千上万个PVC,这就意味着运维人员必须得事先创建出成千上万个PV。更麻烦的是,随着新的PVC不断被提交,运维人员就不得不继续添加新的、能满足条件的PV,否则新的Pod就会因为PVC绑定不到PV而失败。在实际操作中,这几乎没办法靠人工做到。所以,Kubernetes为我们提供了一套可以自动创建PV的机制,即:DynamicProvisioning。 DynamicProvisioning机制工作的核心,在于一个名叫StorageClass的API对象。而StorageClass对象的作用,其实就是创建PV的模板。 具体地说,StorageClass对象会定义如下两个部分内容:第一,PV的属性。比如,存储类型、Volume的大小等等。第二,创建这种PV需要用到的存储插件。比如,Ceph等等。 有了这样两个信息之后,Kubernetes就能够根据用户提交的PVC,找到一个对应的StorageClass了。然后,Kubernetes就会调用该StorageClass声明的存储插件,创建出需要的PV。 在下面的例子中,PV是被自动创建出来的。apiVersion:v1kind:PersistentVolumeClaimmetadata:name:claim1spec:accessModes:ReadWriteOnce指定所使用的存储类,此存储类将会自动创建符合要求的PVstorageClassName:fastresources:requests:storage:30GiapiVersion:storage。k8s。iov1kind:StorageClassmetadata:name:fastprovisioner:kubernetes。iogcepdparameters:type:pdssd StorageClass的作用,则是充当PV的模板。并且,只有同属于一个StorageClass的PV和PVC,才可以绑定在一起。StorageClass的另一个重要作用,是指定PV的Provisioner(存储插件)。这时候,如果你的存储插件支持DynamicProvisioning的话,Kubernetes就可以自动为你创建PV了。LocalPV Kubernetes依靠PV、PVC实现了一个新的特性,这个特性的名字叫作:LocalPersistentVolume,也就是LocalPV。 LocalPV实现的功能就非常类似于hostPath加上nodeAffinity,比如,一个Pod可以声明使用类型为Local的PV,而这个PV其实就是一个hostPath类型的Volume。如果这个hostPath对应的目录,已经在节点A上被事先创建好了,那么,我只需要再给这个Pod加上一个nodeAffinitynodeA,不就可以使用这个Volume了吗?理论上确实是可行的,但是事实上,我们绝不应该把一个宿主机上的目录当作PV来使用,因为本地目录的存储行为是完全不可控,它所在的磁盘随时都可能被应用写满,甚至造成整个宿主机宕机。所以,一般来说LocalPV对应的存储介质是一块额外挂载在宿主机的磁盘或者块设备,我们可以认为就是一个PV一块盘。 LocalPV和普通的PV有一个很大的不同在于LocalPV可以保证Pod始终能够被正确地调度到它所请求的LocalPV所在的节点上面,对于普通的PV来说,Kubernetes都是先调度Pod到某个节点上,然后再持久化节点上的Volume目录,进而完成Volume目录与容器的绑定挂载,但是对于LocalPV来说,节点上可供使用的磁盘必须是提前准备好的,因为它们在不同节点上的挂载情况可能完全不同,甚至有的节点可以没这种磁盘,所以,这时候,调度器就必须能够知道所有节点与LocalPV对应的磁盘的关联关系,然后根据这个信息来调度Pod,实际上就是在调度的时候考虑Volume的分布。 例子: 先创建本地磁盘对应的pvapiVersion:v1kind:PersistentVolumemetadata:name:examplepvspec:capacity:storage:5GivolumeMode:FilesystemaccessModes:ReadWriteOncepersistentVolumeReclaimPolicy:DeletestorageClassName:localstoragelocal:path:mntdisksvol1nodeAffinity:required:nodeSelectorTerms:matchExpressions:key:kubernetes。iohostnameoperator:Invalues:node1 其中:lcal。path写对应的磁盘路径必须指定对应的node,用。spec。nodeAffinity来对应的node。spec。volumeMode可以是FileSystem(Default)和Block确保先运行了StorageClass(即下面写的文件) 再写对于的StorageClass文件kind:StorageClassapiVersion:storage。k8s。iov1metadata:name:localstorageprovisioner:kubernetes。ionoprovisionervolumeBindingMode:WaitForFirstConsumer 其中:provisioner是kubernetes。ionoprovisioner,这是因为localpv不支持DynamicProvisioning,所以它没有办法在创建出pvc的时候,自动创建对应pvvolumeBindingMode是WaitForFirstConsumer,WaitForFirstConsumer即延迟绑定,这样可以既保证推迟到调度的时候再进行绑定,又可以保证调度到指定的pod上,其实WaitForFirstConsumer又2种:一种是WaitForFirstConsumer,一种是Immediate,这里必须用延迟绑定模式。 再创建一个pvckind:PersistentVolumeClaimapiVersion:v1metadata:name:examplelocalclaimspec:accessModes:ReadWriteOnceresources:requests:storage:5GistorageClassName:localstorage 这里需要注意的地方就是storageClassName要写出我们之前自己创建的storageClassName的名字:localstorage 之后应用这个文件,使用命令kubectlgetpvc可以看到他的状态是Pending,这个时候虽然有了匹配的pv,但是也不会进行绑定,依然在等待。 之后我们写个pod应用这个pvckind:PodapiVersion:v1metadata:name:examplepvpodspec:volumes:name:examplepvstoragepersistentVolumeClaim:claimName:examplelocalclaimcontainers:name:examplepvcontainerimage:nginxports:containerPort:80name:httpservervolumeMounts:mountPath:usrsharenginxhtmlname:examplepvstorage 这样就部署好了一个localpv在pod上,这样即使pod没有了,再次重新在这个node上创建,写入的文件也能持久化的存储在特定位置。 如何删除这个pv一定要按照流程来,要不然会删除失败删除使用这个pv的pod从node上移除这个磁盘(按照一个pv一块盘)删除pvc删除pv总结 本文我们讨论了kubernetes存储的几种类型,有临时存储如:hostPath、emptyDir,也有真正的持久化存储,还讨论了相关的概念,如:PVC、PV、StorageClass等,下图是对这些概念的一个概括: 、 参考极客时间:深入剖析Kubernetes课程https:kubernetes。iozhdocsconceptsstoragevolumesemptydirhttps:www。qikqiak。comk8strainstoragelocalhttps:www。kubernetes。org。cn4078。htmlhttps:haojianxun。github。io20190110kubernetesE79A84E69CACE59CB0E68C81E4B985E58C96E5AD98E582A8Local20Persistent20VolumeE8A7A3E69E90