深入剖析 Kubernetes-4 容器持久化存储

1 PV&PVC&StorageClass

1.1 概念

  • PV描述的是持久化存储数据卷。

  • PVC描述的是Pod所希望使用的持久化存储的属性。

  • StorageClass其实就是创建PV的模板。

用户创建的PVC要真正被容器使用起来,必须先和某个符合条件的PV进行绑定。这里要检查的条件包括两部分:

  • PV 和 PVC 的 spec 字段。比如,PV 的存储(storage)大小,就必须满足 PVC 的要求。
  • PV 和 PVC 的 storageClassName 字段必须一样。

PVC 和 PV 的设计,其实跟“面向对象”的思想完全一致

PVC 可以理解为持久化存储的“接口”,它提供了对某种持久化存储的描述,但不提供具体的实现;而这个持久化存储的实现部分则由 PV 负责完成。

在 Kubernetes 中,实际上存在着一个专门处理持久化存储的控制器,叫作 Volume Controller。这个 Volume Controller 维护着多个控制循环,其中有一个循环,扮演的就是撮合 PV 和 PVC 的“红娘”的角色。它的名字叫作 PersistentVolumeController。它会不断地查看当前每一个 PVC,是不是已经处于Bound(已绑定)状态。如果不是,那它就会遍历所有的、可用的 PV,并尝试将其与这个“单身”的 PVC 进行绑定。

将一个 PV 与 PVC 进行“绑定”,其实就是将这个 PV 对象的名字,填在了 PVC 对象的 spec.volumeName 字段上。所以,接下来 Kubernetes 只要获取到这个 PVC 对象,就一定能够找到它所绑定的 PV。

1.2 PV对象如何变成容器里的一个持久化存储

所谓容器的 Volume,其实就是将一个宿主机上的目录,跟一个容器里的目录绑定挂载在了一起。而所谓的“持久化 Volume”,指的就是这个宿主机上的目录,具备“持久性”。而所谓的“持久化 Volume”,指的就是这个宿主机上的目录,具备“持久性”。

Kubernetes 需要做的工作,就是使用远程存储服务,来为容器准备一个持久化的宿主机目录,以供将来进行绑定挂载时使用。

准备“持久化”宿主机目录的过程,我们可以形象地称为“两阶段处理”

  • 第一阶段:Attach

    为虚拟机挂载远程磁盘的操作,对应的正是“两阶段处理”的第一阶段。在Kubernetes 中,我们把这个阶段称为 Attach。

  • 第二阶段:Mount

    将磁盘设备格式化并挂载到 Volume 宿主机目录的操作,对应的正是“两阶段处理”的第二个阶段,我们一般称为:Mount。

注:如果你的 Volume 类型是远程文件存储(比如 NFS)的话,kubelet 的处理过程就会更简单一些。在这种情况下,kubelet 可以跳过“第一阶段”(Attach)的操作,这是因为一般来说,远程文件存储并没有一个“存储设备”需要挂载在宿主机上。kubelet 会直接从“第二阶段”(Mount)开始准备宿主机上的 Volume 目录。

经过了“两阶段处理”,我们就得到了一个“持久化”的 Volume 宿主机目录。所以,接下来,kubelet 只要把这个 Volume 目录通过 CRI 里的 Mounts 参数,传递给Docker,然后就可以为 Pod 里的容器挂载这个“持久化”的 Volume 了。

关于 PV 的“两阶段处理”流程,是靠独立于 kubelet 主控制循环(Kubelet Sync Loop)之外的两个控制循环来实现的

Attach(以及 Dettach)操作,是由 Volume Controller 负责维护的,这个控制循环的名字叫作:AttachDetachController。而它的作用,就是不断地检查每一个 Pod 对应的 PV,和这个 Pod 所在宿主机之间挂载情况。从而决定,是否需要对这个 PV 进行 Attach(或者 Dettach)操作。AttachDetachController是运行在Master节点上的。

Mount(以及 Unmount)操作,必须发生在 Pod 对应的宿主机上,所以它必须是 kubelet 组件的一部分。这个控制循环的名字,叫作:VolumeManagerReconciler,它运行起来之后,是一个独立于 kubelet 主循环的Goroutine。

kubelet 的一个主要设计原则,就是它的主控制循环绝对不可以被 block

1.3 StorageClass

人工管理 PV 的方式就叫作 Static Provisioning。Kubernetes 为我们提供了一套可以自动创建 PV 的机制,即:Dynamic Provisioning。Dynamic Provisioning 机制工作的核心,在于一个名叫 StorageClass 的 API 对象。

StorageClass 对象会定义如下两个部分内容:

  • PV 的属性。比如,存储类型、Volume 的大小等等。
  • 创建这种 PV 需要用到的存储插件。比如,Ceph 等等。

需要注意的是,StorageClass 并不是专门为了 Dynamic Provisioning 而设计的。你可以在PVC和PV中定义相同的即便不存在的StorageClass,Kubernetes 会将他们绑定起来,这个时候进行的是Static Provisioning。

如果你的集群已经开启了名叫 DefaultStorageClass 的 Admission Plugin,它就会为 PVC 和 PV 自动添加一个默认的 StorageClass;否则,PVC 的 storageClassName的值就是“”,这也意味着它只能够跟 storageClassName 也是“”的 PV 进行绑定。

1.4 总结

image-20221101232543771

PVC 描述的,是 Pod 想要使用的持久化存储的属性,比如存储的大小、读写权限等。
PV 描述的,则是一个具体的 Volume 的属性,比如 Volume 的类型、挂载目录、远程存储服务器地址等。
StorageClass 的作用,则是充当 PV 的模板。并且,只有同属于一个 StorageClass的 PV 和 PVC,才可以绑定在一起。

StorageClass 的另一个重要作用,是指定 PV 的 Provisioner(存储插件)。这时候,如果你的存储插件支持 Dynamic Provisioning 的话,Kubernetes 就可以自动为你创建 PV 了。

2 Local Persistent Volume

本地持久化存储: Kubernetes 直接使用宿主机上的本地磁盘目录,而不依赖于远程存储服务,来提供“持久化”的容器 Volume。

Local Persistent Volume 并不适用于所有应用。事实上,它的适用范围非常固定,比如:高优先级的系统应用,需要在多个不同节点上存储数据,并且对 I/O 较为敏感。相比于正常的 PV,一旦这些节点宕机且不能恢复时,Local Persistent Volume 的数据就可能丢失。这就要求使用 Local Persistent Volume 的应用必须具备数据备份和恢复的能力,允许你把这些数据定时备份在其他位置。

Local Persistent Volume需要支持一个非常重要的特性:延迟绑定,延迟到调度Pod的时候,等到第一个声明使用该 PVC 的 Pod 出现在调度器之后,调度器再综合考虑所有的调度规则,当然也包括每个 PV 所在的节点位置,来统一决定,这个 Pod 声明的 PVC,到底应该跟哪个 PV 进行绑定。

在创建对应的StorageClass时,定义volumeBindingMode=WaitForFirstConsumer,同时暂不支持Dynamic Provisioning,需要手动创建PV,StorageClass的provisioner字段指定为kubernetes.io/no-provisioner。

3 CSI插件

3.1 CSI存储组件

image-20221103231624908

存储插件实际担任的角色,仅仅是 Volume 管理中的“Attach 阶段”和“Mount 阶段”的具体执行者。而像 Dynamic Provisioning 这样的功能,就不是存储插件的责任,而是Kubernetes 本身存储管理功能的一部分。

CSI 插件体系的设计思想,就是把这个 Provision 阶段,以及 Kubernetes 里的一部分存储管理功能,从主干代码里剥离出来,做成了几个单独的组件。这些组件会通过Watch API 监听 Kubernetes 里与存储相关的事件变化,比如 PVC 的创建,来执行具体的存储管理动作。而这些管理动作,比如“Attach 阶段”和“Mount 阶段”的具体操作,实际上就是通过调用 CSI 插件来完成的。

image-20221103232025966

这套存储插件体系多了三个独立的外部组件(External Components),即:Driver Registrar、External Provisioner 和 External Attacher,对应的正是从Kubernetes 项目里面剥离出来的那部分存储管理功能。需要注意的是,External Components 虽然是外部组件,但依然由 Kubernetes 社区来开发和维护。

图中最右侧的部分,就是需要我们编写代码来实现的 CSI 插件。一个 CSI 插件只有一个二进制文件,但它会以 gRPC 的方式对外提供三个服务(gRPC Service),分别叫作:CSI Identity、CSI Controller 和 CSI Node

Driver Registrar 组件,负责将插件注册到 kubelet 里面。在具体实现上,Driver Registrar 需要请求 CSI 插件的Identity 服务来获取插件信息。

External Provisioner 组件,负责的正是 Provision 阶段。在具体实现上,External Provisioner 监听(Watch)了 APIServer 里的 PVC 对象。当一个 PVC 被创建时,它就会调用 CSI Controller 的 CreateVolume 方法,为你创建对应 PV。如果使用公有云提供的磁盘或块设备的话,这一步就需要调用公有云的API创建这个PV所描述的磁盘或块设备了。

External Attacher 组件,负责的正是“Attach 阶段”。在具体实现上,它监听了 APIServer 里 VolumeAttachment 对象的变化。一旦出现了 VolumeAttachment 对象,External Attacher 就会调用 CSI Controller 服务的 ControllerPublish 方法,完成它所对应的 Volume 的 Attach 阶段。

Volume 的“Mount 阶段”,并不属于 External Components 的职责。当 kubelet 的VolumeManagerReconciler 控制循环检查到它需要执行 Mount 操作的时候,会通过pkg/volume/csi 包,直接调用 CSI Node 服务完成 Volume 的“Mount 阶段”。

在实际使用 CSI 插件的时候,我们会将这三个 External Components 作为 sidecar 容器和CSI 插件放置在同一个 Pod 中。由于 External Components 对 CSI 插件的调用非常频繁,所以这种 sidecar 的部署方式非常高效

3.2 总结

CSI 的设计思想,把插件的职责从“两阶段处理”,扩展成了 Provision、Attach 和 Mount 三个阶段。其中,Provision 等价于“创建磁盘”,Attach 等价于“挂载磁盘到虚拟机”,Mount 等价于“将该磁盘格式化后,挂载在
Volume 的宿主机目录上”。

4 CSI插件编写指南

4.1 CSI插件代码结构

image-20221103233659175

4.2 部署CSI插件常用原则

  • 通过DaemonSet在每个节点上都启动一个 CSI 插件,来为 kubelet 提供 CSI Node 服务。

    在上述 DaemonSet 的定义里面,除了 CSI 插件,我们还以 sidecar 的方式运行着driver-registrar 这个外部组件。它的作用,是向 kubelet 注册这个 CSI 插件。这个注册过程使用的插件信息,则通过访问同一个 Pod 里的 CSI 插件容器的 Identity 服务获取到。

  • 通过 StatefulSet 在任意一个节点上再启动一个 CSI 插件,为 External Components 提供 CSI Controller 服务。

    作为 CSI Controller 服务的调用者, External Provisioner 和 External Attacher 这两个外部组件,就需要以 sidecar 的方式和这次部署的 CSI 插件定义在同一个 Pod 里。

4.3 总结

对于一个部署了 CSI 存储插件的 Kubernetes 集群来说:

当用户创建了一个 PVC 之后,你前面部署的 StatefulSet 里的 External Provisioner 容器,就会监听到这个 PVC 的诞生,然后调用同一个 Pod 里的 CSI 插件的 CSI Controller服务的 CreateVolume 方法,为你创建出对应的 PV。

这时候,运行在 Kubernetes Master 节点上的 Volume Controller,就会通过PersistentVolumeController 控制循环,发现这对新创建出来的 PV 和 PVC,并且看到它们声明的是同一个 StorageClass。所以,它会把这一对 PV 和 PVC 绑定起来,使 PVC 进入 Bound 状态。

然后,用户创建了一个声明使用上述 PVC 的 Pod,并且这个 Pod 被调度器调度到了宿主机 A 上。这时候,Volume Controller 的 AttachDetachController 控制循环就会发现,上述 PVC 对应的 Volume,需要被 Attach 到宿主机 A 上。所以,AttachDetachController 会创建一个 VolumeAttachment 对象,这个对象携带了宿主机A 和待处理的 Volume 的名字。

这样,StatefulSet 里的 External Attacher 容器,就会监听到这个 VolumeAttachment对象的诞生。于是,它就会使用这个对象里的宿主机和 Volume 名字,调用同一个 Pod 里的 CSI 插件的 CSI Controller 服务的ControllerPublishVolume 方法,完成“Attach 阶段”。

上述过程完成后,运行在宿主机 A 上的 kubelet,就会通过 VolumeManagerReconciler(一个独立于 kubelet 主循环的 Goroutine)控制循环,发现当前宿主机上有一个 Volume 对应的存储设备(比如磁盘)已经被 Attach到了某个设备目录下。于是 kubelet 就会调用同一台宿主机上的 CSI 插件的 CSI Node 服务的 NodeStageVolume 和 NodePublishVolume 方法,完成这个 Volume 的“Mount阶段”。

至此,一个完整的持久化 Volume 的创建和挂载流程就结束了。

posted @ 2022-11-01 23:29  hunter-w  阅读(140)  评论(0编辑  收藏  举报