pv、pvc、storageClass

  • pv持久化存储卷,定义一个持久化存储在宿主机上的目录,比如一个 NFS 的挂载目录。
  • pvc描述pod希望使用到pv持久化的属性,比如磁盘大小、读写权限、路径
  • pvc如果想被pod使用起来,需要有符合条件的pv绑定,符合条件的原则:
    • volumeMode文件格式一致(VolumeMode有块状设备和文件系统)
    • pv和pvc的读写权限一致
    • pv和pvc的storageClass名称必须一致
    • 通过标签(labels)匹配的方式从PV列表中选择合适的PV绑定
    • pv的capacity的storage大小必须大于等于pvc描述声明使用的大小
  • 在pod的Volume声明要使用的pvc,在container的VolumeMount定义好要挂载的路径,等这个pod创建好以后,kubelet就会把这个pv按照VolumeMount挂载到容器目录上
  • storageClass:本质上是创建pv的模板,会定义两部分内容
    • PV的属性,比如读写权限、volume大小
    • 创建pv用到存储插件,比如ceph
    • 有了2个属性,k8s能够根据提交的PVC,找到一个对应的StorageClass,然后调用该StorageClass声明的存储插件,创建出需要的PV。(动态PV)
    • 只有同属于一个 StorageClass的PV 和PVC,才可以绑定在一起。

k8s volume controller

在这里插入图片描述

  • 内部维护多个reconcile,其中一个PersistentVolumeController负责通过遍历 PVC,将 unbound 的 PVC 与 PV 进行绑定
  • 持久化Volume:Volume在宿主机上的目录,具备“持久性”。即:这个目录里面的内容,既不会因为容器的删除而被清理掉,也不会跟当前的宿主机绑定。这样,当容器被重启或者在其他节点上重建出来之后,它仍然能够通过挂载这个 Volume,访问到这些内容
  • hostPath是node节点上的磁盘目录,pod被删除如果被调度到另一个节点上,则该目录无法使用原有的目录;emptyDir则是pod在宿主机建立一个临时目录,生命周期和pod一致,因此大多数情况下,持久化 Volume 的实现,往往依赖于一个远程存储服务,比如:远程文件存储(比如,NFS、GlusterFS)、远程块存储(比如,公有云提供的远程磁盘)等等。
  • k8s的工作就是使用这些存储服务,来为容器准备一个持久化的宿主机目录,以供将来进行绑定挂载时使用。而所谓“持久化”,指的是容器在这个目录里写入的文件,都会保存在远程存储中,从而使得这个目录具备了“持久性”,持久化形象称为“两阶段处理
    • 第一阶段:Attach,只针对于远程块状设备(比如云服务提供),这步骤会基于远程云提供的API,创建磁盘并挂载到pod所在宿主机(node节点)上
      ○第二阶段:Mount,把磁盘挂载到Volume指定的宿主机挂载点。(如果是NFS类似的远程文件系统,则可以直接进行第二步操作)
    • 对这两个阶段的区分,Volume插件的实现接口上,k8s给到不同的入参:针对阶段1,可用参数为nodeName(宿主机名称),针对阶段2,可用参数为dir(宿主机的目录)
  • k8s为容器持久化一个宿主机目录的两个阶段,是靠独立于kubelet主控制循环之外的2个控制循环实现的
    • Attach是由Volume Controller负责,控制循环是AttachDetachController,作用是检查每一个pod对应的pv,和这个pod所在宿主机的挂载情况,从而决定是否对这个pv进行attach操作
    • Mount发生在Pod对应的宿主机上,是 kubelet 组件的一部分。这个控制循环的名字,叫作:VolumeManagerReconciler,它运行起来之后,是一个独立于 kubelet 主循环的 Goroutine。使用Goroutine的原因是为了与kubelet主循环的解耦,因为挂载操作是一个比较费时间的过程,解耦是为了不影响主循环被block
  • 整体流程:
    • 用户提交请求创建pod,k8s发现这个pod声明使用了PVC,那就靠PersistentVolumeController帮它找一个PV配对
    • 没有现成的PV,就去找对应的StorageClass,帮它新创建一个PV,然后和PVC完成绑定(如果没有声明SC,且集群没有开启DefaultStorageClass的插件,就会尝试找sc为“”的pv进行绑定)
    • 新创建的PV,还只是一个API 对象,需要经过“两阶段处理”变成宿主机上的“持久化 Volume”才真正有用:
      • 第一阶段由运行在master上的AttachDetachController负责,为这个PV完成 Attach 操作,为宿主机挂载远程磁盘;
      • 第二阶段是运行在每个节点上kubelet组件的内部,把第一步attach的远程磁盘 mount 到宿主机目录。这个控制循环叫VolumeManagerReconciler,运行在独立的Goroutine,不会阻塞kubelet主循环

为何要使用pv、pvc

  • 如果只是为了职责划分,PV、PVC 体系确实不见得比直接在 Pod 里声明 Volumes 字段有什么优势,但如果要支持更多种的持久化数据卷,pv就有必要了
  • 很多时候,用户希望能直接用到宿主机上的磁盘目录,不依赖于远程服务,原因有
    • 本地SSD性能会更好
    • 如果要在私有环境部署k8s集群,则只能使用本地磁盘
  • 在k8s v1.10以后,依靠pv、pvc体系逐渐形成了Local Persistent Volume特性,但是
    • 这个特性的使用范围比较固定,如高优先级的系统应用,需要在多个不同节点上存储数据,并且对 I/O 较为敏感。典型的应用包括:
      • 分布式数据存储比如 MongoDB、Cassandra 等
      • 分布式文件系统比如 GlusterFS、Ceph 等
      • 需要在本地磁盘上进行大量数据缓存的分布式应用
    • 一旦Local节点宕机且不能恢复时,Local Persistent Volume的数据就可能丢失。这就要求使用Local Persistent Volume的应用必须具备数据备份和恢复的能力,允许你把这些数据定时备份在其他位置
  • Local Persistent Volume难点
    • 如何把本地磁盘抽象成 PV:也许用hostPath+NodeAffinity能够一定程度实现,但有很大的问题在于,不应该把一个宿主机上的目录当作 PV 使用,因为
      • 本地目录的存储行为完全不可控,它所在的磁盘随时都可能被应用写满,甚至造成整个宿主机宕机
      • 不同的本地目录之间也缺乏哪怕最基础的 I/O 隔离机制
      • 所以,一个 Local Persistent Volume 对应的存储介质,一定是一块额外挂载在宿主机的磁盘或者块设备(“额外”的意思是,它不应该是宿主机根目录所使用的主硬盘
    • 调度器如何保证Pod始终能被正确地调度到它所请求的Local Persistent Volume所在的节点上
      • 常规PV是先把pod调度到某个节点上,然后经过“两阶段”处理持久化宿主机上的Volume指定挂载点,然后完成Volume与容器的绑定挂载
      • 但对于LPV来说,这些信息都是运维人员提前准备好的,所以调度器必须知道Node节点与LPV的对应关系,根据这层信息进行调度(通过NodeAffinity实现),称为“在调度的时候考虑 Volume 分布”。在 k8s的调度器里,有一个叫VolumeBindingChecker 的过滤条件专门负责这个事情
  • 在私有部署环境中,有两个方法解决这个步骤
    • 给宿主机外挂并格式化一个可用的本地磁盘
    • 在宿主机上挂载几个 RAM Disk(内存盘)来模拟本地磁盘

FlexVolume与CSI

flexVolume

  • volume的两阶段处理,在具体的控制循环中,实际上调用的是k8s的 pkg/volume 目录下的存储插件(Volume Plugin)。在这个例子里,就是pkg/volume/flexvolume这个目录里的代码(flexVolume存储插件的入口)

  • 以Mount为例,FlexVolume的SetUpAt()实际上只做了一件事,就是封装出了一行命令(即:NewDriverCall),由 kubelet 在“Mount 阶段”去执行。

  • flexVolume中,执行命令为

    # /usr/libexec/kubernetes/kubelet-plugins/volume/exec/k8s~nfs/nfs是插件的可执行文件的路径。这个名叫 nfs 的文件,正是你要编写的插件的实现,k8s~nfs是从volume的driver字段里解析出来的,格式为vendor/driver
    # <mount dir>:kubelet调用SetUpAt() 方法传递来的 dir 的值,表示当前正在处理的Volume在宿主机上的目录
    # <json param>:pv里定义的options字段的值以及在SetUpAt()方法中加上的Pod名字、Namespace 等元数据(Metadata)
    
    /usr/libexec/kubernetes/kubelet-plugins/volume/exec/k8s~nfs/nfs mount <mount dir> <json param>
    
  • 当编写完了FlexVolume的实现之后,要把它的可执行文件放在每个节点的插件目录下

  • 在Mount阶段,kubelet的VolumeManagerReconcile控制循环的一次调谐操作流程为:

    kubelet --> 
    pkg/volume/flexvolume.SetUpAt() -->
    /usr/libexec/kubernetes/kubelet-plugins/volume/exec/k8s~nfs/nfs mount <mount dir> <json param>
    
  • flexVolume存储插件的局限性

    • NFS FlexVolume不能支持 Dynamic Provisioning。除非为它编写一个专门的 External Provisioner
    • FlexVolume每一次对插件可执行文件的调用,都是一次完全独立的操作,在mount操作时生成的一些挂载信息,只能通过保存到临时文件的方式给到后续的unmount操作使用,无法保存在一些环境变量中

CSI

  • k8s中通过存储插件管理容器持久化
    在这里插入图片描述
  • 存储插件只是Attach、Mount操作的执行者,而Dynamic Provisioning就不是存储插件的责任,是k8s本身存储管理功能的一部分
  • CSI 插件体系的设计思想是:把Provision阶段,以及k8s里的一部分存储管理功能,从主干代码里剥离出来,做成了几个单独的组件,组件会watch存储相关的事件变化,并执行对应的管理操作(通过调用CSI插件完成)
  • CSI插件体系思路
    在这里插入图片描述
  • 相比于存储插件,CSI多了3个外部组件(主要负责的是Volume的创建以及在宿主机上挂载Volume),具有存储管理功能(仍被K8s社区开发维护)
    • Driver Registrar:负责将插件注册到kubelet里,即将可执行文件放在插件目录下(每个插件实现完成后,要将可执行文件在节点的插件目录下),具体实现上,Driver Regi6strar需要请求CSI插件的Identity服务来获取插件信息
    • External Provisioner:负责Provision阶段。具体实现上,External Provisioner watch APIServer里的PVC对象。当一个PVC被创建时,它就会调用CSI Controller的CreateVolume方法创建PV
      • 如果使用的是公有云提供的磁盘(或者块设备),那就需要调用公有云提供的API创建PV所描述的磁盘,但CSI插件是独立于K8s之外的,所以没有k8s定义的pV类型,而是用CSI自定义的Volume类型
    • External Attacher:负责Attach阶段,watch APIServer里VolumeAttachment对象的变化。VolumeAttachment对象是k8s中确认一个Volume可以进入“Attach 阶段”的重要标志,当出现了VolumeAttachment对象,External Attacher就会调用CSI Controller服务的ControllerPublish方法,完成它所对应的Volume的Attach阶段。
  • Volume的“Mount 阶段”不属于External Components的职责。当kubelet的VolumeManagerReconciler控制循环检查到volume需要执行Mount操作的时候,会通过 pkg/volume/csi包,直接调用CSI Node服务完成 Volume 的“Mount 阶段
  • 最右侧是自定义组件,需要自己代码实现的CSI插件,会以gRPC方式对外提供3个服务,实际使用CSI插件的时候,会将这三个External Components作为sidecar容器和CSI插件放置在同一个Pod中。由于 External Components 对CSI插件的调用非常频繁,所以这种sidecar的部署方式非常高效。3个对外服务为:
    • CSI Identity:负责暴露这件插件本身的信息
    service Identity { 
        // return the version and name of the plugin 
        rpc GetPluginInfo(GetPluginInfoRequest) returns (GetPluginInfoResponse) {} 
        // reports whether the plugin has the ability of serving the Controller interface 
        rpc GetPluginCapabilities(GetPluginCapabilitiesRequest) returns (GetPluginCapabilitiesResponse) {} 
        // called by the CO just to check whether the plugin is running or not 
        rpc Probe (ProbeRequest) returns (ProbeResponse) {}
    }
    
    • CSI Controller:对 CSI Volume(对应 Kubernetes 里的 PV)的管理接口
    service Controller {
      // provisions a volume
      rpc CreateVolume (CreateVolumeRequest)
        returns (CreateVolumeResponse) {}
        
      // deletes a previously provisioned volume
      rpc DeleteVolume (DeleteVolumeRequest)
        returns (DeleteVolumeResponse) {}
        
      // make a volume available on some required node
      rpc ControllerPublishVolume (ControllerPublishVolumeRequest)
        returns (ControllerPublishVolumeResponse) {}
        
      // make a volume un-available on some required node
      rpc ControllerUnpublishVolume (ControllerUnpublishVolumeRequest)
        returns (ControllerUnpublishVolumeResponse) {}
    
      // make a snapshot
      rpc CreateSnapshot (CreateSnapshotRequest)
        returns (CreateSnapshotResponse) {}
        
      // Delete a given snapshot
      rpc DeleteSnapshot (DeleteSnapshotRequest)
        returns (DeleteSnapshotResponse) {}
      ...
    }
    
    • CSI Controller服务的实际调用者,并不是k8s,而是External Provisioner和External Attacher分别通过监听PVC和VolumeAttachement对象,来跟k8s进行协作。
    • CSI Controller服务里定义的操作有个共同特点,那就是它们都无需在宿主机上进行,而真正需要在宿主机上操作的是通过External Attacher调用的CSI Node中的逻辑

存储插件与CSI的区别

  • 存储插件的思想是两阶段处理,即Attach和Mount,而CSI的思想扩展为3阶段,即Provision、Attach和Mount
  • PV Attach方式
    • 存储插件是由Master节点上的AttachDetachController reconcile pv和挂载pv的pod所在node节点的状态,判断是否进行Attach操作
    • CSI在AttachDetachController操作时实际上会创建一个VolumeAttachment对象,从而触发External Attacher watch VolumeAttachment对象改变调用的ControllerPublishVolume方法
  • PV Mount方式
    • 存储插件是由节点上kubelet的VolumeManagerReconciler循环控制
    • CSI是VolumeManagerReconciler需要进行“Mount”操作时实际上会执行到 pkg/volume/csi 目录中,直接向CSI Node服务发起调用NodePublishVolume方法的请求

CSI插件编写

  • 有了CSI插件后,持久化只需要创建一个StorageClass
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: do-block-storage
  namespace: kube-system
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: com.digitalocean.csi.dobs
  • storageClass中需要注意的是provisioner=com.digitalocean.csi.dobs这个字段。这个字段告诉了k8s要用名叫com.digitalocean.csi.dobs的CSI插件来处理这个StorageClass 相关的所有操作

Identity

service Identity {
    // return the version and name of the plugin 
    rpc GetPluginInfo(GetPluginInfoRequest) returns (GetPluginInfoResponse) {} 
    // reports whether the plugin has the ability of serving the Controller interface 
    rpc GetPluginCapabilities(GetPluginCapabilitiesRequest) returns (GetPluginCapabilitiesResponse) {} 
    // called by the CO just to check whether the plugin is running or not 
    rpc Probe (ProbeRequest) returns (ProbeResponse) {}
}
  • k8s通过CSI Identity知道插件的名称,Identity通过定义标准的gRpc server响应External Component的请求
  • 最重要的函数是GetPluginInfo,返回了插件的版本和名称
    CSI要求插件名称遵守反向DNS格式
反向DNS(Reverse DNS)是DNS(Domain Name System)的一种使用方式,它允许通过IP地址查找与之对应的域名。
在反向DNS中,IP地址被反向解析为域名。这对于网络管理和安全审计非常有用,可以帮助识别特定IP地址背后的主机或服务器。

反向DNS的工作方式是,将IPv4或IPv6地址转换为特定的域名格式。IPv4地址的反向DNS域名格式是将IP地址的每个字节逆序排列,并添加 ".in-addr.arpa" 后缀。例如,IP地址为 192.0.2.123 的反向DNS域名将是 "123.2.0.192.in-addr.arpa"。

对于IPv6地址,反向DNS域名的格式稍有不同,但原理类似。

使用反向DNS,可以进行以下操作:

通过IP地址查找与之关联的域名,从而确定主机的身份。
验证发出请求的服务器的真实性。
阻止垃圾邮件和恶意活动,通过检查发件人IP地址的反向DNS来验证其合法性。
  • GetPluginCapabilities方法返回是CSI插件的能力
  • Probe接口,检查CSI插件是否正常工作

controller

  • 主要负责provision和attach阶段
  • Provision(调用者是external provision)
    • CreateVolume:如果使用的是其他类型的块存储(比如 Cinder、Ceph RBD 等),对应的操作也是类似地调用创建存储卷的 API,创建存储卷
    • DeleteVolume
  • attach(调用者是external attacher)
    • ControllerPublishVolume:将前面创建的存储卷,挂载到指定的虚拟机上
    • ControllerUnpublishVolume

CSI Node

  • kubelet的VolumeManagerReconciler控制循环会直接调用 CSI Node 服务来完成 Volume 的“Mount 阶段”,这个阶段被细分为两个接口
    • NodeStageVolume:MountDevice调用该接口,格式化Volume在宿主机上对应的存储设备,然后挂载到一个临时目录(Staging 目录)上
    • NodePublishVolume:SetUp操作则会调用 CSI Node该接口将 Staging 目录,绑定挂载到 Volume 对应的宿主机目录上

部署CSI插件的原则

  • 通过DaemonSet在每个节点上都启动一个CSI插件,来为kubelet提供CSI Node服务,除了CSI插件,还以sidecar的方式运行着driver-registrar这个外部组件(向kubelet注册这个CSI插件,注册过程是通过访问同一个Pod里的CSI插件容器的Identity服务获取到的)
    • CSI插件运行在一个容器里,那么CSI Node服务在“Mount 阶段”执行的挂载操作,实际上是发生在这个容器的 Mount Namespace 里的。可是真正希望执行挂载操作的对象都是宿主机/var/lib/kubelet目录下的文件和目录。所以在定义DaemonSet Pod 的时候,需要把宿主机的 /var/lib/kubelet 以Volume的方式挂载进 CSI 插件容器的同名目录下,然后设置这个Volume的mountPropagation=Bidirectional,即开启双向挂载传播,从而将容器在这个目录下进行的挂载操作“传播”给宿主机,
  • 通过StatefulSet在任意一个节点上再启动一个CSI插件,为External Components提供 CSI Controller服务,而作为CSI controller的调用者,External Provisioner和External Attacher 这两个外部组件需要以sidecar的方式和这次部署的CSI cotronller定义在同一个 Pod 里
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐