Kubernetes 存储架构体系

  • Post author:
  • Post category:其他


本文将主要分享以下三方面的内容:

  1. Kubernetes 存储体系架构
  2. Flexvolume 介绍及使用
  3. CSI 介绍及使用

一、Kubernetes 存储体系架构



引例: 在 Kubernetes 中挂载一个 Volume

首先以一个 Volume 的挂载例子来作为引入。

如下图所示,左边的 YAML 模板定义了一个 StatefulSet 的一个应用,其中定义了一个名为 disk-pvc 的 volume,挂载到 Pod 内部的目录是 /data。disk-pvc 是一个 PVC 类型的数据卷,其中定义了一个 storageClassName。

因此这个模板是一个典型的动态存储的模板。右图是数据卷挂载的过程,主要分为 6 步:


  • 第一步

    :用户创建一个包含 PVC的 Pod

  • 第二步

    :PV Controller 会不断观察 ApiServer,如果它发现一个 PVC 已经创建完毕但仍然是未绑定的状态,它就会试图把一个 PV 和 PVC 绑定
  1. PV Controller 首先会在集群内部找到一个适合的 PV 进行绑定,
  2. 如果未找到相应的 PV,


    就调用 Volume Plugin 去做 Provision。Provision 就是从远端上一个具体的存储介质创建一个 Volume


    ,并且在集群中创建一个 PV 对象,然后将此 PV 和 PVC 进行绑定

  • 第三步

    :通过 Scheduler 完成一个调度功能。

我们知道,当一个 Pod 运行的时候,需要选择一个 Node,这个节点的选择就是由 Scheduler 来完成的。


Scheduler 进行调度的时候会有多个参考量,比如 Pod 内部所定义的 nodeSelector、nodeAffinity 这些定义以及 Volume 中所定义的一些标签等。

我们可以在数据卷中添加一些标准,这样使用这个 pv 的 Pod 就会由于标签的限制,被调度器调度到期望的节点上。


  • 第四步

    :如果有一个 Pod 调度到某个节点之后,它所定义的 PV 还没有被挂载(Attach),此时


    AD Controller 就会调用 VolumePlugin,把远端的 Volume 挂载到目标节点中的设备上(如:/dev/vdb)。


  • 第五步:

    当 Volum Manager



    发现一个 Pod 调度到自己的节点上并且 Volume 已经完成了挂载,它就会执行 mount 操作,



    将本地设备(也就是刚才得到的 /dev/vdb)挂载到 Pod 在节点上的一个子目录中。同时它也可能会做一些像格式化、



    是否挂载到 GlobalPath 等这样的附加操作。

  • 第六步

    就:绑定操作,就是将


    已经挂载到本地的 Volume 映射到容器中。

Kubernetes 的存储架构


接下来,我们一起看一下 Kubernetes 的存储架构。


  • PV Controller

    : 负责 PV/PVC 的绑定、生命周期管理,并根据需求进行数据卷的 Provision/Delete 操作;

  • AD Controller

    : 负责存储设备的 Attach/Detach 操作,将设备挂载到目标节点;

  • Volume Manager

    : 管理卷的 Mount/Unmount 操作、卷设备的格式化以及挂载到一些公用目录上的操作;

  • Volume Plugins

    :它主要是对上面所有挂载功能的实现。



PV Controller、AD Controller、Volume Manager 主要是进行操作的调用,而具体操作则是由 Volume Plugins 实现的。


  • Scheduler

    : 实现对 Pod 的调度能力,会根据一些存储相关的的定义去做一些存储相关的调度。

接下来,我们分别介绍上面这几部分的功能。

PV Controller


首先我们先来回顾一下几个基本概念:


  • Persistent Volume (PV)

    : 持久化存储卷,详细定义了预挂载存储空间的各项参数

例如,我们去挂载一个远端的 NAS 的时候,这个 NAS 的具体参数就要定义在 PV 中。


一个 PV 是没有 NameSpace 限制的,它一般由 Admin 来创建与维护。


  • Persistent Volume Claim (PVC)

    :持久化存储声明

它是用户所使用的存储接口,对存储细节无感知,主要是定义一些基本存储的 Size、AccessMode 这些参数在里面,


并且它是属于某个 NameSpace 内部的。


  • StorageClass

    :存储类。

一个动态存储卷会按照 StorageClass 所定义的模板来创建一个 PV,其中定义了创建模板所需要的一些参数和创建 PV 的一个 Provisioner(


就是由谁去创建的


)。


PV Controller 的主要任务就是完成



  • PV、PVC 的生命周期管理


    ,比如创建、删除 PV 对象,负责 PV、PVC 的状态迁移


  • 另一个任务就是绑定 PVC 与 PV 对象


    ,一个 PVC 必须和一个 PV 绑定后才能被应用使用,它们是一一绑定的,一个 PV 只能被一个 PVC 绑定,反之亦然。

接下来,我们看一下一个 PV 的状态迁移图。

创建好一个 PV 以后,我们就处于一个 Available 的状态,


当一个 PVC 和一个 PV 绑定的时候,这个 PV 就进入了 Bound 的状态,此时如果我们把 PVC 删掉,Bound 状态的 PV 就会进入 Released 的状态。



一个 Released 状态的 PV 会根据自己定义的 ReclaimPolicy 字段来决定自己是进入一个 Available 的状态还是进入一个 Deleted 的状态。如果 ReclaimPolicy 定义的是 “recycle” 类型,它会进入一个 Available 状态,如果转变失败,就会进入 Failed 的状态。

相对而言,PVC 的状态迁移图就比较简单。

一个创建好的 PVC 会处于 Pending 状态,当一个 PVC 与 PV 绑定之后,PVC 就会进入 Bound 的状态,


当一个 Bound 状态的 PVC 的 PV 被删掉之后,该 PVC 就会进入一个 Lost 的状态。对于一个 Lost 状态的 PVC,它的 PV 如果又被重新创建,并且重新与该 PVC 绑定之后,该 PVC 就会重新回到 Bound 状态。

下图是一个 PVC 去绑定 PV 时对 PV 筛选的一个流程图。就是说一个 PVC 去绑定一个 PV 的时候,应该选择一个什么样的 PV 进行绑定。


  • 首先

    它会检查 VolumeMode 这个标签,PV 与 PVC 的 VolumeMode 标签必须相匹配。VolumeMode 主要定义的是我们这个数据卷是文件系统 (FileSystem) 类型还是一个块 (Block) 类型。

  • 第二个部分

    是 LabelSelector。当 PVC 中定义了 LabelSelector 之后,我们就会选择那些有 Label 并且与 PVC 的 LabelSelector 相匹配的 PV 进行绑定。

  • 第三个部分

    是 StorageClassName 的检查。如果 PVC 中定义了一个 StorageClassName,则必须有此相同类名的 PV 才可以被筛选中。



这里再具体解释一下 StorageClassName 这个标签,该标签的目的就是说,当一个 PVC 找不到相应的 PV 时,我们就会用该标签所指定的 StorageClass 去做一个动态创建 PV 的操作,同时它也是一个绑定条件,当存在一个满足该条件的 PV 时,就会直接使用现有的 PV,而不再去动态创建。


  • 第四个部分

    是 AccessMode 检查。

AccessMode 就是平时我们在 PVC 中定义的如 “ReadWriteOnce”、”RearWriteMany” 这样的标签。该绑定条件就是要求 PVC 和 PV 必须有匹配的 AccessMode,即 PVC 所需求的 AccessMode 类型,PV 必须具有。


  • 最后

    一个部分是 Size 的检查。

一个 PVC 的 Size 必须小于等于 PV 的 Size,这是因为 PVC 是一个声明的 Volume,实际的 Volume 必须要大于等于声明的 Volume,才能进行绑定。

接下来,我们看一个 PV Controller 的一个实现。

PV Controller 中主要有两个实现逻辑:一个是 ClaimWorker;一个是 VolumeWorker。

ClaimWorker 实现的是 PVC 的状态迁移。

通过系统标签 “pv.kubernetes.io/bind-completed” 来标识一个 PVC 的状态。

  • 如果该标签为 True,说明我们的 PVC 已经绑定完成,此时我们只需要去同步一些内部的状态。
  • 如果该标签为 False,就说明我们的 PVC 处于未绑定状态。

这个时候就需要检查整个集群中的 PV 去进行筛选。通过 findBestMatch 就可以去筛选所有的 PV,


也就是按照之前提到的五个绑定条件来进行筛选。如果筛选到 PV,就执行一个 Bound 操作,否则就去做一个 Provision 的操作,自己去创建一个 PV。

再看 VolumeWorker 的操作。它实现的则是 PV 的状态迁移。

通过 PV 中的 ClaimRef 标签来进行判断,如果该标签为空,就说明该 PV 是一个 Available 的状态,此时只需要做一个同步就可以了;如果该标签非空,这个值是 PVC 的一个值,我们就会去集群中查找对应的 PVC,如果存在该 PVC,就说明该 PV 处于一个 Bound 的状态,此时会做一些相应的状态同步,如果找不到该 PVC,就说明该 PV 处于一个绑定过的状态,相应的 PVC 已经被删掉了,这时 PV 就处于一个 Released 的状态。此时再根据 ReclaimPolicy 是否是 Delete 来决定是删掉还是只做一些状态的同步。

以上就是 PV Controller 的简要实现逻辑。

AD Controller


AD Controller 是 Attach/Detach Controller 的一个简称。

它有两个核心对象,即 DesiredStateofWorld 和 ActualStateOfWorld。

  • DesiredStateofWorld 是集群中预期要达到的数据卷的挂载状态;
  • ActualStateOfWorld 则是集群内部实际存在的数据卷挂载状态。

它有两个核心逻辑,desiredStateOfWorldPopulator 和 Reconcile。

  • desiredStateOfWorldPopulator 主要是用来同步集群的一些数据以及 DSW、ASW 数据的更新,它会把集群里面,比如说我们创建一个新的 PVC、创建一个新的 Pod 的时候,我们会把这些数据的状态同步到 DSW 中;
  • Reconcile 则会根据 DSW 和 ASW 对象的状态做状态同步。它会把 ASW 状态变成 DSW 状态,在这个状态的转变过程中,它会去执行 Attach、Detach 等操作。

下面这个表分别给出了 desiredStateOfWorld 以及 actualStateOfWorld 对象的一个具体例子。

  • desiredStateOfWorld 会对每一个 Worker 进行定义,包括 Worker 所包含的 Volume 以及一些试图挂载的信息;
  • actualStateOfWorl 会把所有的 Volume 进行一次定义,包括每一个 Volume 期望挂载到哪个节点上、挂载的状态是什么样子的等等。

下图是 AD Controller 实现的逻辑框图。

从中我们可以看到,AD Controller 中有很多 Informer,Informer 会把集群中的 Pod 状态、PV 状态、Node 状态、PVC 状态同步到本地。

在初始化的时候会调用 populateDesireStateofWorld 以及 populateActualStateofWorld 将 desireStateofWorld、actualStateofWorld 两个对象进行初始化。

在执行的时候,通过 desiredStateOfWorldPopulator 进行数据同步,即把集群中的数据状态同步到 desireStateofWorld 中。reconciler 则通过轮询的方式把 actualStateofWorld 和 desireStateofWorld 这两个对象进行数据同步,在同步的时候,会通过调用 Volume Plugin 进行 attach 和 detach 操作,同时它也会调用 nodeStatusUpdater 对 Node 的状态进行更新。

以上就是 AD Controller 的简要实现逻辑。

Volume Plugins




我们之前提到的 PV Controller、AD Controller 以及 Volume Manager 其实都是通过调用 Volume Plugin 提供的接口,比如 Provision、Delete、Attach、Detach 等去做一些 PV、PVC 的管理。而这些接口的具体实现逻辑是放在 VolumePlugin 中的。

根据源码的位置可将 Volume Plugins 分为 In-Tree 和 Out-of-Tree 两类:

  • In-Tree 表示源码是放在 Kubernetes 内部的,和 Kubernetes 一起发布、管理与迭代,缺点及时迭代速度慢、灵活性差;
  • Out-of-Tree 类的 Volume Plugins 的代码独立于 Kubernetes,它是由存储商提供实现的,目前主要有 Flexvolume 和 CSI 两种实现机制,可以根据存储类型实现不同的存储插件。所以我们比较推崇 Out-of-Tree 这种实现逻辑。

从位置上我们可以看到,


Volume Plugins 实际上就是 PV Controller、AD Controller 以及 Volume Manager 所调用的一个库,分为 In-Tree 和 Out-of-Tree 两类 Plugins。


它通过这些实现来调用远端的存储,比如说挂载一个 NAS 的操作 “mount -t nfs ***”,


该命令其实就是在 Volume Plugins 中实现的,


它会去调用远程的一个存储挂载到本地。

从类型上来看,Volume Plugins 可以分为很多种。In-Tree 中就包含了 几十种常见的存储实现,但一些公司的自己定义私有类型,有自己的 API 和参数,公共存储插件是无法支持的,这时就需要 Out-of-Tree 类的存储实现,比如 CSI、FlexVolume。

Volume Plugins 的具体实现会放到后面去讲。这里主要看一下 Volume Plugins 的插件管理。

Kubernetes会在

PV Controller、AD Controller 以及 Volume Manager 中来做插件管理

。通过 VolumePlguinMg 对象进行管理。主要包含 Plugins 和 Prober 两个数据结构。

Plugins 主要是用来保存 Plugins 列表的一个对象,而 Prober 是一个探针,用于发现新的 Plugin,比如 FlexVolume、CSI 是扩展的一种插件,它们是动态创建和生成的,所以一开始我们是无法预知的,因此需要一个探针来发现新的 Plugin。

下图是插件管理的整个过程。

PV Controller、AD Controller 以及 Volume Manager 在启动的时候会执行一个 InitPlugins 方法来对 VolumePluginsMgr 做一些初始化。

它首先会将所有 In-Tree 的 Plugins 加入到我们的插件列表中。同时会调用 Prober 的 init 方法,该方法会首先调用一个 InitWatcher,它会时刻观察着某一个目录 (比如图中的 /usr/libexec/kubernetes/kubelet-plugins/volume/exec/),当这个目录每生成一个新文件的时候,也就是创建了一个新的 Plugins,此时就会生成一个新的 FsNotify.Create 事件,并将其加入到 EventsMap 中;同理,如果删除了一个文件,就生成一个 FsNotify.Remove 事件加入到 EventsMap 中。

当上层调用 refreshProbedPlugins 时,Prober 就会把这些事件进行一个更新,如果是 Create,就将其添加到插件列表;如果是 Remove,就从插件列表中删除一个插件。

以上就是 Volume Plugins 的插件管理机制。

Kubernetes 存储卷调度


我们之前说到 Pod 必须被调度到某个 Worker 上才能去运行。在调度 Pod 时,我们会使用不同的调度器来进行筛选,其中有一些与 Volume 相关的调度器。例如 VolumeZonePredicate、VolumeBindingPredicate、CSIMaxVolumLimitPredicate 等。

VolumeZonePredicate 会检查 PV 中的 Label,比如 failure-domain.beta.kubernetes.io/zone 标签,如果该标签定义了 zone 的信息,VolumeZonePredicate 就会做相应的判断,即必须符合相应的 zone 的节点才能被调度。

比如下图左侧的例子,定义了一个 label 的 zone 为 cn-shenzhen-a。右侧的 PV 则定义了一个 nodeAffinity,其中定义了 PV 所期望的节点的 Label,该 Label 是通过 VolumeBindingPredicate 进行筛选的。