Pod Volumes
首先来看一下 Pod Volumes 的使用场景:
- 场景一:如果 pod 中的某一个容器在运行时异常退出,被 kubelet 重新拉起之后,如何保证之前容器产生的重要数据没有丢失?
- 场景二:如果同一个 pod 中的多个容器想要共享数据,应该如何去做?
以上两个场景,其实都可以借助 Volumes 来很好地解决,接下来首先看一下 Pod Volumes 的常见类型:
- 本地存储,常用的有 emptydir/hostpath;
- 网络存储:网络存储当前的实现方式有两种,一种是 in-tree,它的实现的代码是放在 K8s 代码仓库中的,随着k8s对存储类型支持的增多,这种方式会给k8s本身的维护和发展带来很大的负担;而第二种实现方式是 out-of-tree,它的实现其实是给 K8s 本身解耦的,通过抽象接口将不同存储的driver实现从k8s代码仓库中剥离,因此out-of-tree 是后面社区主推的一种实现网络存储插件的方式;
- Projected Volumes:它其实是将一些配置信息,如 secret/configmap 用卷的形式挂载在容器中,让容器中的程序可以通过POSIX接口来访问配置数据;
- PV 与 PVC 就是今天要重点介绍的内容。
容器磁盘上的文件的生命周期是短暂的,这就使得在容器中运行重要应用时会出现一些问题。首先,当容器崩溃时,kubelet 会重启它,但是容器中的文件将丢失——容器以干净的状态(镜像最初的状态)重新启动。其次,在 Pod 中同时运行多个容器时,这些容器之间通常需要共享文件。Kubernetes 中的 Volume 抽象就很好的解决了这些问题。
Kubernetes 中的卷有明确的寿命 —— 与封装它的 Pod 相同。所以,卷的生命比 Pod 中的所有容器都长,当这个容器重启时数据仍然得以保存。当然,当 Pod 不再存在时,卷也将不复存在。也许更重要的是,Kubernetes 支持多种类型的卷,Pod 可以同时使用任意数量的卷。
Kubernetes 支持以下类型的卷
1 |
|
hostpath pod删除后目录仍然存在,emptyDir会删除
emptyDir
EmptyDir 是一个空目录,他的生命周期和所属的 Pod 是完全一致的,它用处是把同一 Pod 内的不同容器之间共享工作过程产生的文件。当 Pod 被分配给节点时,首先创建 emptyDir 卷,并且只要该 Pod 在该节点上运行,该卷就会存在。正如卷的名字所述,它最初是空的。
Pod 中的容器可以读取和写入 emptyDir 卷中的相同文件,尽管该卷可以挂载到每个容器中的相同或不同路径上。当出于任何原因从节点中删除 Pod 时,emptyDir 中的数据将被永久删除。emptyDir 的用法有:
- 暂存空间,例如用于基于磁盘的合并排序
- 用作长时间计算崩溃恢复时的检查点
- Web 服务器容器提供数据时,保存内容管理器容器提取的文件除了所需的 path 属性之外,用户还可以为 hostPath 卷指定 type。使用这种卷类型是请注意,因为:
- 由于每个节点上的文件都不同,具有相同配置(例如从 podTemplate 创建的)的 pod 在不同节点上的行为可能会有所不同。
- 当 Kubernetes 按照计划添加资源感知调度时,将无法考虑 hostPath 使用的资源。
- 在底层主机上创建的文件或目录只能由 root 写入。您需要在特权容器中以 root 身份运行进程,或修改主机上的文件权限以便写入 hostPath 卷。
1 |
|
1 |
|
hostPath
hostPath 为静态存储机制 - 主机目录挂载,同一 Pod 内的不同容器之间共享工作过程
hostPath 卷将主机节点的文件系统中的文件或目录挂载到集群中,hostPath 的用途如下所示:
运行需要访问 Docker 内部的容器
- 使用 /var/lib/docker 的 hostPath
在容器中运行 cAdvisor 监控服务
- 使用 /dev/cgroups 的 hostPath
允许 pod 指定给定的 hostPath
- 是否应该在 pod 运行之前存在,是否应该创建,以及它应该以什么形式存在
除了所需的 path 属性之外,用户还可以为 hostPath 卷指定 type。使用这种卷类型是请注意,因为:
- 由于每个节点上的文件都不同,具有相同配置(例如从 podTemplate 创建的)的 pod 在不同节点上的行为可能会有所不同。
- 当 Kubernetes 按照计划添加资源感知调度时,将无法考虑 hostPath 使用的资源。
- 在底层主机上创建的文件或目录只能由 root 写入。您需要在特权容器中以 root 身份运行进程,或修改主机上的文件权限以便写入 hostPath 卷。
1 |
|
nfs
nfs-client-provisioner
是一个 Kubernetes
的简易 NFS
的外部 provisioner
,本身不提供 NFS
,需要现有的 NFS
服务器提供存储。
- 动态
PV
又叫做动态供给,就是在创建PVC
以后,自动创建出PV
PV
以${namespace}-${pvcName}-${pvName}
的命名格式提供(在NFS
服务器上)PV
回收的时候以archieved-${namespace}-${pvcName}-${pvName}
的命名格式(在NFS
服务器上)
在 kubernetes
中要实现动态创建 pv
必须先要创建 StorageClass
,每一个 StorageClass
对应了一个 provisioner
。在 kubernetes
中内置了很多 provisioner
类型可供选择,但是很难受的是没有内置的 nfs
的 provisioner
。对普通用户而言,部署 nfs
是实现后端存储最简单直接的方法。虽然 k8s
并不提供,但其允许提供外部的 provisioner
,而对应迫切需要使用 nfs
的用户,可以使用 nfs-client-provisioner
这个插件完成。
- 获取 NFS 的配置代码
1 |
|
- 在 K8S 中配置 NFS 插件
- 创建一个独立
nfs-namespace.yaml
的命名空间 - 修改
nfs-deployment.yaml
的namespace
为你创建的namespace
- 修改
nfs-rbac.yaml
的namespace
为你创建的namespace
- 创建一个独立
1 |
|
1 |
|
1 |
|
- 创建完成之后再来创建 StorageClass
provisioner
就是刚才deployment
中的PROVISIONER_NAME
- 还有一个比较关键的参数是
archiveOnDelete
,如果你想在删除了pvc
之后还保留数据的话需要把这个参数改为true
,不然你删除了pvc
同时pv
也会删除,然后数据也会丢失。
1 |
|
- 测试是否正常
- 官方仓库提供了对应的测试配置文件,我们只需要执行即可
- 只要有了
pvc
,那么就会动态创建pv
- 测试创建
PVC
,测试创建POD
- 删除测试
PVC
,删除测试POD
1 |
|
1 |
|
1 |
|
1 |
|
PersistentVolume
虽然 pod 也可以直接挂载存储,但是管理不方便,特别是 pod 的数量越来越多。而且 pod 可能是由开发维护的,而存储却是由运维负责。通过 PV、PVC 分开就方便多了。
-
PersistentVolume(PV)
集群中的一块存储,可以由管理员事先供应,或者 使用存储类(Storage Class)来动态供应。 持久卷是集群资源,就像节点也是集群资源一样。PV 持久卷和普通的 Volume 一样,也是使用卷插件来实现的,只是它们拥有独立于任何使用 PV 的 Pod 的生命周期。 此 API 对象中记述了存储的实现细节,无论其背后是 NFS、iSCSI 还是特定于云平台的存储系统。
-
PersistentVolumeClaim(PVC)
用户对存储的请求。概念上与 Pod 类似。 Pod 会耗用节点资源,而 PVC 申领会耗用 PV 资源。Pod 可以请求特定数量的资源(CPU 和内存);同样 PVC 申领也可以请求特定的大小和访问模式 (例如,可以要求 PV 卷能够以 ReadWriteOnce、ReadOnlyMany 或 ReadWriteMany 模式之一来挂载,参见访问模式)
-
StorageClass(SC)
充当 PV 的模板,自动为 PVC 创建 PV。
常见的情况是针对不同的 问题用户需要的是具有不同属性(如,性能)的 PersistentVolume 卷。 集群管理员需要能够提供不同性质的 PersistentVolume,并且这些 PV 卷之间的差别不 仅限于卷大小和访问模式,同时又不能将卷是如何实现的这些细节暴露给用户。
基本概念
静态 PV
静态 Provisioning:由集群管理员事先去规划这个集群中的用户会怎样使用存储,它会先预分配一些存储,也就是预先创建一些 PV;然后用户在提交自己的存储需求(也就是 PVC)的时候,K8s 内部相关组件会帮助它把 PVC 和 PV 做绑定;之后用户再通过 pod 去使用存储的时候,就可以通过 PVC 找到相应的 PV,它就可以使用了。
静态产生方式有什么不足呢?可以看到,首先需要集群管理员预分配,预分配其实是很难预测用户真实需求的。举一个最简单的例子:如果用户需要的是 20G,然而集群管理员在分配的时候可能有 80G 、100G 的,但没有 20G 的,这样就很难满足用户的真实需求,也会造成资源浪费。有没有更好的方式呢?
1 |
|
动态 PV
现在集群管理员不预分配 PV,他写了一个模板文件,这个模板文件是用来表示创建某一类型存储(块存储,文件存储等)所需的一些参数,这些参数是用户不关心的,给存储本身实现有关的参数。用户只需要提交自身的存储需求,也就是PVC文件,并在 PVC 中指定使用的存储模板(StorageClass)。
K8s 集群中的管控组件,会结合 PVC 和 StorageClass 的信息动态,生成用户所需要的存储(PV),将 PVC 和 PV 进行绑定后,pod 就可以使用 PV 了。通过 StorageClass 配置生成存储所需要的存储模板,再结合用户的需求动态创建 PV 对象,做到按需分配,在没有增加用户使用难度的同时也解放了集群管理员的运维工作。
-
StorageClass: 指定一些 PV 的回收策略以及挂载选项
- 最主要的是指定 Provisioner
-
Provisioner: 用来动态创建和管理 PV 的插件
- 每个存储都有不同的插件来管理 PV,不同存储指定 PV 参数和是从存储里清除数据的方式都是不一样的
-
Kubernetes 内置了一些存储的 Provisioner,而一些没有内置的
- 不支持的常用的有: NFS、CerphFS,则需要引入外部的 Provisioner 来完成
-
外部仓库地址
- kubernetes-retired
- kubernetes-incubator
-
关于 PVC 的扩容
- 只有动态供应的 pvc 可以调整大小,供应 pvc 的存储类必须支持调整大小,即手动创建不支持
- kubernetes 中用 NFS 做后端存储支不支持 PVC 扩容
- 目前支持 resize 的存储卷 Azure Disk, Azure File, Glusterfs, Ceph RBD 等
- kubernetes PVC 支持 resize 扩容
如果管理员所创建的所有静态 PV 卷都无法与用户的 PersistentVolumeClaim 匹配, 集群可以尝试为该 PVC 申领动态供应一个存储卷。 这一供应操作是基于 StorageClass 来实现的:PVC 申领必须请求某个 存储类,同时集群管理员必须 已经创建并配置了该类,这样动态供应卷的动作才会发生。 如果 PVC 申领指定存储类为 ""
,则相当于为自身禁止使用动态供应的卷。
要启用基于存储级别的动态存储配置,集群管理员需要启用 API server 上的 DefaultStorageClass 准入控制器。例如,通过确保 DefaultStorageClass 位于 API server 组件的 –admission-control 标志,使用逗号分隔的有序值列表中,可以完成此操作。
1 |
|
绑定
master 中的控制环路监视新的 PVC,寻找匹配的 PV(如果可能),并将它们绑定在一起。如果为新的 PVC 动态调配 PV,则该环路将始终将该 PV 绑定到 PVC。否则,用户总会得到他们所请求的存储,但是容量可能超出要求的数量。一旦 PV 和 PVC 绑定后,PersistentVolumeClaim 绑定是排他性的,不管它们是如何绑定的。PVC 跟 PV 绑定是一对一的映射。
- 根据Labels匹配PV与PVC
- 根据storageClassName类型进行匹配
如果找不到匹配的 PV 卷,PVC 申领会无限期地处于未绑定状态。 当与之匹配的 PV 卷可用时,PVC 申领会被绑定。 例如,即使某集群上供应了很多 50 Gi 大小的 PV 卷,也无法与请求 100 Gi 大小的存储的 PVC 匹配。当新的 100 Gi PV 卷被加入到集群时,该 PVC 才有可能被绑定。
pv状态流转
主要分为三个阶段:
- 第一个阶段(Create阶段)是用户提交完 PVC,由 csi-provisioner 创建存储,并生成 PV 对象,之后 PV controller 将 PVC 及生成的 PV 对象做 bound,bound 之后,create 阶段就完成了;
- 之后用户在提交 pod yaml 的时候,首先会被调度选中某一个 合适的node,等 pod 的运行 node 被选出来之后,会被 AD Controller watch 到 pod 选中的 node,它会去查找 pod 中使用了哪些 PV。然后它会生成一个内部的对象叫 VolumeAttachment 对象,从而去触发 csi-attacher去调用csi-controller-server 去做真正的 attache 操作,attach操作调到云存储厂商OpenAPI。这个 attach 操作就是将存储 attach到 pod 将会运行的 node 上面。第二个阶段 —— attach阶段完成;
- 然后我们接下来看第三个阶段。第三个阶段 发生在kubelet 创建 pod的过程中,它在创建 pod 的过程中,首先要去做一个 mount,这里的 mount 操作是为了将已经attach到这个 node 上面那块盘,进一步 mount 到 pod 可以使用的一个具体路径,之后 kubelet 才开始创建并启动容器。这就是 PV 加 PVC 创建存储以及使用存储的第三个阶段 —— mount 阶段。
总的来说,有三个阶段:第一个 create 阶段,主要是创建存储;第二个 attach 阶段,就是将那块存储挂载到 node 上面(通常为将存储load到node的/dev下面);第三个 mount 阶段,将对应的存储进一步挂载到 pod 可以使用的路径。这就是我们的 PVC、PV、已经通过CSI实现的卷从创建到使用的完整流程。
持久化卷
- Capacity:这个很好理解,就是存储对象的大小;
-
AccessModes:也是用户需要关心的,就是说我使用这个 PV 的方式。它有三种使用方式。
-
- 一种是单 node 读写访问;
- 第二种是多个 node 只读访问,是常见的一种数据的共享方式;
- 第三种是多个 node 上读写访问。
用户在提交 PVC 的时候,最重要的两个字段 —— Capacity 和 AccessModes。在提交 PVC 后,k8s 集群中的相关组件是如何去找到合适的 PV 呢?首先它是通过为 PV 建立的 AccessModes 索引找到所有能够满足用户的 PVC 里面的 AccessModes 要求的 PV list,然后根据PVC的 Capacity,StorageClassName, Label Selector 进一步筛选 PV,如果满足条件的 PV 有多个,选择 PV 的 size 最小的,accessmodes 列表最短的 PV,也即最小适合原则。
-
ReclaimPolicy:这个就是刚才提到的,我的用户方 PV 的 PVC 在删除之后,我的 PV 应该做如何处理?常见的有三种方式。
-
- 第一种方式我们就不说了,现在 K8s 中已经不推荐使用了;
- 第二种方式 delete,也就是说 PVC 被删除之后,PV 也会被删除;
- 第三种方式 Retain,就是保留,保留之后,后面这个 PV 需要管理员来手动处理。
- StorageClassName:StorageClassName 这个我们刚才说了,我们动态 Provisioning 时必须指定的一个字段,就是说我们要指定到底用哪一个模板文件来生成 PV ;
- NodeAffinity:就是说我创建出来的 PV,它能被哪些 node 去挂载使用,其实是有限制的。然后通过 NodeAffinity 来声明对node的限制,这样其实对 使用该PV的pod调度也有限制,就是说 pod 必须要调度到这些能访问 PV 的 node 上,才能使用这块 PV,这个字段在我们下一讲讲解存储拓扑调度时在细说。
持久化卷声明的保护
PVC 保护的目的是确保由 pod 正在使用的 PVC 不会从系统中移除,因为如果被移除的话可能会导致数据丢失。
如果用户删除被某 Pod 使用的 PVC 对象,该 PVC 申领不会被立即移除。 PVC 对象的移除会被推迟,直至其不再被任何 Pod 使用。 此外,如果管理员删除已绑定到某 PVC 申领的 PV 卷,该 PV 卷也不会被立即移除。 PV 对象的移除也要推迟到该 PV 不再绑定到 PVC。
持久化卷类型
- GCEPersistentDisk、AWSElasticBlockStore、AzureFile、AzureDisk
- FlexVolume、Flocker、NFS、iSCSI、RBD、CephFS
- Cinder、Glusterfs、VsphereVolume、Quobyte、FC
- HostPath、VMware、Photon、StorageOS
1 |
|
PV 访问模式
PersistentVolume 可以以资源提供者支持的任何方式挂载到主机上。如下表所示,供应商具有不同的功能,每个 PV 的访问模式都将被设置为该卷支持的特定模式。例如,NFS 可以支持多个读/写客户端,但特定的 NFS PV 可能以只读方式导出到服务器上。每个 PV 都有一套自己的用来描述特定功能的访问模式。
-
ReadWriteOnce
- 该卷可以被单个节点以读/写模式挂载,允许运行在同一节点上的多个 Pod 访问卷
- 在命令行中访问模式缩写为:RWO
-
ReadOnlyMany
- 该卷可以被多个节点以只读模式挂载
- 在命令行中访问模式缩写为:ROX
-
ReadWriteMany
- 该卷可以被多个节点以读/写模式挂载
- 在命令行中访问模式缩写为:RWX
回收策略
- Retain(保留)—— 用户可以手动回收资源。当 PersistentVolumeClaim 对象被删除时,PersistentVolume 卷仍然存在,对应的数据卷被视为”已释放(released),由于卷上仍然存在这前一申领人的数据,该卷还不能用于其他申领。
- Recycle(回收)—— 废弃
- Delete(删除)—— 删除动作会将 PersistentVolume 对象从 Kubernetes 中移除,同时也会从外部基础设施(如 AWS EBS、GCE PD、Azure Disk 或 Cinder 卷)中移除所关联的存储资产。 动态供应的卷会继承其 StorageClass 中设置的回收策略,该策略默认 为
Delete
。
状态
卷可以处于以下的某种状态(命令行会显示绑定到 PV 的 PVC 的名称):
- Available(可用)– 卷是一个空闲资源,尚未绑定到任何申领;
- Bound(已绑定)– 该卷已经绑定到某申领;
- Released(已释放)– 所绑定的申领已被删除,但是资源尚未被集群回收;
- Failed(失败)– 卷的自动回收操作失败。
实例演示
演示 NFS 模式下的持久化方案!
- 安装 NFS 服务器端服务
1 |
|
- 安装 NFS 客户端服务
1 |
|
- 部署 PV
1 |
|
- 创建服务并使用 PVC
通过如下配置文件,我们创建了三个 Nginx 的副本。但是,观察发现只有一个 web-0 的 POD 绑定到了属于自己的 PV 数据存储,而 web-2 显示没有符合自己要求的 PV,而后续的 POD 压根都没有开始创建。
1 |
|
StorageClass
StorageClass 为管理员提供了描述存储 “类” 的方法。 不同的类型可能会映射到不同的服务质量等级或备份策略,或是由集群管理员制定的任意策略。
- provisioner 每个 StorageClass 都有一个制备器(Provisioner),用来决定使用哪个卷插件制备 PV。 该字段必须指定。
- parameters取决于制备器,可以接受不同的参数。
- reclaimPolicy 由 StorageClass 动态创建的 PersistentVolume 会在类的
reclaimPolicy
字段中指定回收策略,可以是Delete
或者Retain
。如果 StorageClass 对象被创建时没有指定reclaimPolicy
,它将默认为Delete
。
使用方式
PV 可以看作可用的存储资源,PVC 则是对存储资源的需求,PV 和 PVC 的互相关系遵循如下图!
由于容器本身是非持久化的,因此需要解决在容器中运行应用程序遇到的一些问题。首先,当容器崩溃时,kubelet 将重新启动容器,但是写入容器的文件将会丢失,容器将会以镜像的初始状态重新开始;第二,在通过一个 Pod 中一起运行的容器,通常需要共享容器之间一些文件。Kubernetes 通过存储卷解决上述的两个问题。
在 Docker 有存储卷的概念卷,但 Docker 中存储卷只是磁盘的或另一个容器中的目录,并没有对其生命周期进行管理。Kubernetes 的存储卷有自己的生命周期,它的生命周期与使用的它 Pod 生命周期一致。因此,相比于在 Pod 中运行的容器来说,存储卷的存在时间会比的其中的任何容器都长,并且在容器重新启动时会保留数据。当然,当 Pod 停止存在时,存储卷也将不再存在。在 Kubernetes 支持多种类型的卷,而 Pod 可以同时使用各种类型和任意数量的存储卷。
资源供应:Kubernetes 支持两种资源的供应模式:静态模式(Staic)和动态模式(Dynamic)。
-
静态模式
-
运维管理人员手动创建多个 PV,每个 PV 在定义的时需要将后端存储的特性进行设置,然后让 PVC 进行选择。
-
静态资源下,通过 PV 和 PVC 完成绑定,并供 Pod 使用的存储管理机制。
-
-
动态模式
-
运维管理人员无需手动创建 PV,而是通过 StorageClass 的设置对后端存储进行描述,标记为某种”类型(Class)”,此时要求 PVC 对存储的类型进行声明,系统将自动完成 PV 的创建及 PVC 的绑定,PVC 可以声明为 Class 为 ““,说明该 PVC 禁止使用动态模式。
-
动态资源下,通过 StorageClass 和 PVC 完成资源动态绑定,系统自动生成 PV,并供 Pod 使用的存储管理机制。
-
资源绑定:在用户定义好 PVC 后,系统将根据 PVC 对存储资源的请求(存储空间和访问模式)在已存在的 PV 中选择一个满足 PVC 要求的 PV,一旦找到,就将 PV 与用户定义的 PVC 进行绑定,然后用户的应用就可以使用这个 PVC 了。如果系统中没有满足 PVC 要求的 PV,PVC 则会无限期处于 Pending 状态,直到等到系统管理员创建了一个符合要求的 PV。PV 一旦绑定在某个 PVC 上,就被这个 PVC 独占,不能再与其他 PVC 进行绑定了。在这种情况下,当 PVC 申请的存储空间比 PV 的少时,整个 PV 的空间都能够为 PVC 所用,可能会造成资源的浪费。如果资源供应使用的是动态模式,则系统在 PVC 找到合适的 StorageClass 后,将会自动创建 PV 并完成 PVC 的绑定。
1 |
|
资源使用:Pod 使用 volume 的定义,将 PVC 挂载到容器内的某个路径进行使用。volume 的类型为 persistentVoulumeClaim,在容器应用挂载了一个 PVC 后,就能被持续独占使用。不过,多个 Pod 可以挂载同一个 PVC,应用程序需要考虑多个实例共同访问一块存储空间的问题。
资源释放:当用户对存储资源使用哪个完毕后,用户可以删除 PVC,与该 PVC 绑定的 PV 将会被标记为已释放,但还不能立刻与其他 PVC 进行绑定。通过之前 PVC 写入的数据可能还留在存储设备上,只有在清除之后该 PV 才能继续使用。
资源回收:对于 PV,管理员可以设定回收策略(Reclaim Policy)用于设置与之绑定的 PVC 释放资源之后,对于遗留数据如何处理。只有 PV 的存储空间完成回收,才能供新的 PVC 绑定和使用。
我们如果部署多个服务使用 PVC 的时候,要么对每个服务划分一个单独的 PVC 资源,要么花一个大的 PVC 资源,然后通过 subPath 字段指定分割资料目录,这样就不会造成我们数据目录的文件混淆的问题了。
1 |
|