做了一段时间Kubernetes 容器存储,根据文档重新整理一下Kubernetes CSI接口逻辑,并以几大云厂商的CSI Driver 和 kubernetes-csi/csi-driver-nfs 为例,描述一下CSI插件实现的逻辑。

CSI接口逻辑

容器存储接口(CSI)是在 Kubernetes 等容器编排系统(CO)上向容器化工作负载公开任意块和文件存储系统的标准。 使用 CSI,第三方存储提供商可以编写和部署插件,在 Kubernetes 中公开新的存储系统,而无需接触 Kubernetes 的核心代码。

早在 Kubernetes 1.23 CSI接口就已经GA了。

CSI Driver通信方式

Kubelet 与 CSI 驱动程序的通信

  • Kubelet 通过 Unix 域套接字直接向 CSI 驱动程序发出 CSI 调用(如 NodeStageVolume、NodePublishVolume 等),以挂载和卸载卷。
  • Kubelet 通过 kubelet 插件注册机制发现 CSI 驱动程序(以及与之交互所需的 Unix 域套接字)。
  • 因此,所有在 Kubernetes 上部署的 CSI 驱动程序必须在每个支持的节点上使用 kubelet 插件注册机制进行注册。

image.png

控制平面与 CSI 驱动程序的通信

  • Kubernetes 控制平面组件不直接(通过 Unix 域套接字或其他方式)与 CSI 驱动程序进行通信。
  • Kubernetes 控制平面组件仅与 Kubernetes API 进行交互。
  • 因此,要求依赖 Kubernetes API 的操作的 CSI 驱动程序(如卷创建、卷附加、卷快照等)必须监视 Kubernetes API,并针对其触发相应的 CSI 操作。

CSI Driver接口描述

每个存储供应商提供Kubernetes存储插件时都需要根据 CSI接口实现各自的 CSI Driver。

需要按照接口定义API(RPCs),以支持:

  • 动态创建和删除卷。
  • 将卷附加或分离到节点。
  • 将卷挂载/卸载到节点。
  • 使用块存储和可挂载卷。
  • 本地存储提供者(例如,设备映射器、LVM)。
  • 创建和删除快照(快照的来源是一个卷)。
  • 从快照中配置新卷(恢复快照,其中原卷中的数据被快照中的数据替换,超出范围)。

接口功能

CSI插件通常包含三种主要的API接口:Identity Service、Controller Service 和 Node Service:

  • Identity Service 负责提供插件的身份信息。它主要用于以下功能:
    • 获取插件信息 :Identity Service 提供方法以获取插件的名称、版本、类型等信息。这有助于容器编排系统了解所使用的存储插件的基本信息。
    • 支持的功能 :它还允许查询插件支持的功能,例如是否支持快照、克隆等。
  • Controller Service 主要用于管理存储卷的生命周期,主要在集群级别操作,通常由控制平面组件(如Kubernetes控制器)调用。它提供了许多与存储相关的操作,例如:
    • 卷的创建和删除 :允许创建新的存储卷和删除不再需要的存储卷。
    • 卷的扩展 :支持扩展现有卷的大小。
    • 快照管理 :提供创建、删除和恢复快照的功能。
    • 卷的绑定和解绑 :允许将卷绑定到特定的容器或解绑。
  • Node Service 主要处理与具体节点相关的操作,在工作节点上运行,确保应用程序能够访问所需的存储资源。它通常包括以下功能:
    • 卷挂载和卸载 :处理在特定节点上挂载和卸载存储卷的请求。
    • 卷的检查和状态管理 :提供方法检查卷的状态,确保卷在节点上的可用性。
    • 数据传输 :在需要时,可以支持在节点之间的数据传输和复制。

Identity Service

  1. GetPluginInfo
    • 功能 :该接口用于获取 CSI 插件的基本信息,包括插件的名称、版本和供应商等信息。
    • 返回内容 :通常返回一个包含插件名称、版本以及供应商信息的结构体。这有助于 Kubernetes 确认使用的插件是什么,以及其是否符合预期的版本要求。
  2. GetPluginCapabilities
    • 功能 :该接口用于查询 CSI 插件所支持的功能和能力。
    • 返回内容 :返回一个能力列表,描述插件支持的操作(例如,卷的创建、删除、扩展等,见 特性)。这些能力信息使 Kubernetes 能够了解插件的功能范围,从而在调度和管理卷时做出更好的决策。
  3. Probe
    • 功能 :该接口用于检查 CSI 插件的可用性和健康状态。
    • 返回内容 :返回一个状态指示,表明插件是否正常运行。Kubernetes 可以通过此接口定期检查插件的状态,以确保其在集群中可用,并能够处理存储请求。

Controller Service

Controller Service和Node Service的接口较多,主要列出几个关键接口

  • CreateVolume
    • 功能 :创建一个新的存储卷。
    • 参数 :通常包括卷的名称、大小、存储类型等信息。
    • 返回内容 :返回创建的卷的唯一标识符(Volume ID),以便后续操作使用。
  • DeleteVolume
    • 功能 :删除指定的存储卷。
    • 参数 :需要提供要删除的卷的唯一标识符(Volume ID)。
    • 返回内容 :通常是一个空的响应,表示删除操作的结果。
  • ControllerPublishVolume
    • 功能 :将存储卷绑定到指定的节点上,以使该节点能够访问该卷。
    • 参数 :包括卷的唯一标识符、节点的标识符等信息。
    • 返回内容 :通常是一个空的响应,表示卷已成功绑定到节点。
  • ControllerUnpublishVolume
    • 功能 :将存储卷从指定的节点上解绑。
    • 参数 :需要提供卷的唯一标识符和节点的标识符。
    • 返回内容 :通常是一个空的响应,表示解绑操作的结果。
  • ValidateVolumeCapabilities
    • 功能 :验证存储卷是否支持特定的能力(如读写模式)。
    • 参数 :包括卷的唯一标识符和要验证的能力列表。
    • 返回内容 :返回一个结果,指示哪些能力是支持的。
  • ListVolumes
    • 功能 :列出所有已创建的存储卷。
    • 返回内容 :返回一个卷的列表,通常包括每个卷的唯一标识符和相关信息。
  • GetCapacity
    • 功能 :查询存储系统的可用容量。
    • 返回内容 :返回可用的存储容量信息,帮助调度器做出决策。

其他还存在一些快照及扩容接口,将在相关功能中描述。

Node Service

  • NodeStageVolume
    • 功能 :准备存储卷以便于挂载,通常涉及到对存储卷的预处理。
    • 参数 :包括卷的唯一标识符、目标路径、存储参数等。
    • 返回内容 :通常是一个空的响应,表示卷已成功准备就绪。
  • NodeUnstageVolume
    • 功能 :清理存储卷的预处理状态,通常在卷卸载之前调用。
    • 参数 :需要提供卷的唯一标识符和目标路径。
    • 返回内容 :通常是一个空的响应,表示卷已成功清理。
  • NodePublishVolume
    • 功能 :将指定的存储卷挂载到节点上的特定路径,以便容器可以访问该卷。
    • 参数 :包括卷的唯一标识符(Volume ID)、目标挂载路径、挂载选项等。
    • 返回内容 :通常是一个空的响应,表示卷已成功挂载。
  • NodeUnpublishVolume
    • 功能 :将存储卷从节点的特定路径上卸载。
    • 参数 :需要提供卷的唯一标识符和目标挂载路径。
    • 返回内容 :通常是一个空的响应,表示卷已成功卸载。
  • NodeGetVolumeStats
    • 功能 :获取指定存储卷的统计信息,如使用情况、容量等。
    • 参数 :需要提供卷的唯一标识符和目标挂载路径。
    • 返回内容 :返回卷的统计信息,包括使用的空间、总空间等。
  • NodeGetCapabilities
    • 功能 :获取节点的能力信息。
    • 返回内容 :返回节点支持的能力列表,例如是否支持挂载、卸载等操作。

其他还存在扩容等接口,将在相关功能中描述。

CSI Driver卷生命周期

包含STAGE 的完整生命周期

   CreateVolume +------------+ DeleteVolume
 +------------->|  CREATED   +--------------+
 |              +---+----^---+              |
 |       Controller |    | Controller       v
+++         Publish |    | Unpublish       +++
|X|          Volume |    | Volume          | |
+-+             +---v----+---+             +-+
                | NODE_READY |
                +---+----^---+
               Node |    | Node
              Stage |    | Unstage
             Volume |    | Volume
                +---v----+---+
                |  VOL_READY |
                +---+----^---+
               Node |    | Node
            Publish |    | Unpublish
             Volume |    | Volume
                +---v----+---+
                | PUBLISHED  |
                +------------+

Figure 6: The lifecycle of a dynamically provisioned volume, from
creation to destruction, when the Node Plugin advertises the
STAGE_UNSTAGE_VOLUME capability.

动态卷最全的生命周期:

  1. CreateVolume:
    • 在这个阶段,控制器接收创建卷的请求。
    • CSI驱动会处理该请求,通常涉及到在存储后端创建一个新的卷。
    • 一旦卷创建成功,驱动将返回卷的标识信息。
  2. ControllerPublishVolume:
    • 该阶段是将创建的卷与某个特定的节点进行绑定。
    • 控制器会处理请求并执行必要的步骤,以使卷在指定节点上可用。
    • 例如,可能需要在存储后端进行一些配置或设置。
  3. NodeStageVolume:
    • 在这个阶段,节点会准备卷以便于挂载。
    • 这通常涉及到在节点上挂载存储后端的卷,使其可以被访问。
    • 该步骤可能包括创建目录、挂载文件系统等。
  4. NodePublishVolume:
    • 该阶段将卷挂载到容器中,使其可以被应用程序使用。
    • 具体来说,这一步骤会将已经准备好的卷挂载到容器的指定路径。
  5. NodeUnpublishVolume:
    • 在这个阶段,节点会卸载已挂载的卷。
    • 这通常是为了在容器停止运行或需要释放资源时进行的操作。
  6. NodeUnstageVolume:
    • 该步骤是将卷从节点的文件系统中卸载。
    • 这意味着将卷从节点的挂载点解除,使其不再可用。
  7. ControllerUnpublishVolume:
    • 该阶段是解除卷与节点之间的绑定。
    • 控制器会处理请求,确保卷不再与特定节点关联。
  8. DeleteVolume:
    • 最后,删除卷的阶段。
    • 控制器会接收删除请求,并在存储后端删除相应的卷。
    • 这通常是清理资源的最后一步。

云硬盘生命周期示例

上面列举了四个云厂商云硬盘的driver地址,可以从各个云厂商的代码中看到driver每个接口的实现,从而了解各个生命周期实际做了什么事情。各个厂商在实现云硬盘生命周期时都大同小异。

ControllerServer的 CreateVolume 和 DeleteVolume 显而易见是调用云硬盘接口进行创删卷的实现。

ControllerPublishVolume 和 ControllerUnpublishVolume 通过调用云服务器进行了云硬盘的挂卷和卸卷。例如AWS调用了EC2的挂卷接口进行挂卷,华为云使用了ECS的接口进行挂卷。

NodeStageVolume 是将已经挂卷到云服务器的 device 路径挂载(mount)到 StagingTargetPath。一般会在 /dev/disk/by-id//dev/disk/by-path/ 等路径查找到包含相应volume id的挂载盘,具体路径需要看各个云厂商 IaaS的实现。如果不存在文件系统,一般还会进行格式化。NodeUnstageVolume 正好相反。

NodePublishVolume 比较简单,将 StagingTargetPath 挂载到 Pod 相应挂载路径即可。NodeUnpublishVolume 进行 umount。

极简挂卸卷及示例

       +-+  +-+
       |X|  | |
       +++  +^+
        |    |
   Node |    | Node
Publish |    | Unpublish
 Volume |    | Volume
    +---v----+---+
    | PUBLISHED  |
    +------------+

Figure 8: Plugins MAY forego other lifecycle steps by contraindicating
them via the capabilities API. Interactions with the volumes of such
plugins is reduced to `NodePublishVolume` and `NodeUnpublishVolume`
calls.

NFS / S3 等卷的挂载极其简单,仅有 NodePublishVolume 和 NodeUnpublishVolume。

对于 S3 来说,参考 awslabs/mountpoint-s3-csi-driver 。在 NodePublishVolume 中使用mount-s3 直接将对象桶挂载到 Pod 挂载路径。

NFS更为简单,参考 kubernetes-csi/csi-driver-nfs 直接进行 mount 挂载即可。

对于这些极简挂卸卷的driver来说,只要不注册相应能力即可。对于Controller(Un)PublishVolume,不注册 ControllerServiceCapability的 PUBLISH_UNPUBLISH_VOLUME;对于 Node(Un)StageVolume不注册 STAGE_UNSTAGE_VOLUME。

另外发布的CSIDriver 中的 attachRequired 也可设置为 false,不再进行挂载动作。 attachRequired 是一个高层的标志,用于指示 Kubernetes 是否需要在 Pod 调度之前执行附加操作,而 PUBLISH_UNPUBLISH_VOLUME 则是在 CSI 驱动程序的能力层面上定义的,表示驱动程序能够处理的具体操作。

CSI Driver 资源及注册

资源

CSIDriver

要安装 CSI 驱动,部署清单必须包含一个 CSIDriver 对象。

CSIDriver Kubernetes API 对象有两个主要用途:

  1. 简化驱动发现 :如果一个 CSI 驱动创建了 CSIDriver 对象,Kubernetes 用户可以轻松发现集群中安装的 CSI 驱动(通过运行 kubectl get CSIDriver)。
  2. 自定义 Kubernetes 行为 :Kubernetes 在处理 CSI 驱动时有一套默认行为(例如,默认调用 Attach/Detach 操作)。这个对象允许 CSI 驱动指定 Kubernetes 应如何与之交互。
apiVersion: storage.k8s.io/v1
kind: CSIDriver
metadata:
  name: mycsidriver.example.com
spec:
  attachRequired: true
  podInfoOnMount: true
  fsGroupPolicy: File # Kubernetes 1.19 添加,此字段在 Kubernetes 1.23 为 GA
  volumeLifecycleModes: # Kubernetes 1.16 添加,此字段为 beta
    - Persistent
    - Ephemeral
  tokenRequests: # Kubernetes 1.20 添加
    - audience: "gcp"
    - audience: "" # 空字符串表示默认使用 kube-apiserver 的 --api-audiences
      expirationSeconds: 3600
  requiresRepublish: true # Kubernetes 1.20 添加
  seLinuxMount: true # Kubernetes 1.25 添加
  • name: 对应 CSI 驱动的全名。
  • attachRequired: 指示 CSI 卷驱动是否需要 attach 操作。
  • podInfoOnMount: 指示该 CSI 卷驱动是否需要在挂载操作中获取额外的 pod 信息。
  • fsGroupPolicy: 控制 CSI 卷驱动是否支持在挂载时更改卷的所有权和权限。
  • volumeLifecycleModes: 指示驱动支持的卷模式,支持持久卷和临时卷。
  • tokenRequests: 指示 Kubelet 是否在 NodePublishVolume 中传递绑定的服务账户令牌。
  • requiresRepublish: 指示 Kubelet 是否定期调用 NodePublishVolume
  • seLinuxMount: 指示 CSI 驱动是否可以为不同的卷使用不同的 SELinux 标签。

CSINode

CSINode 对象用于存储与 Kubernetes 节点相关的 CSI 驱动程序生成的特定信息。CSI 驱动程序不需要直接创建 CSINode 对象。Kubelet 在通过 kubelet 插件注册机制注册 CSI 驱动程序时管理该对象。node-driver-registrar 辅助容器帮助完成此注册。

它的主要用途包括:

  1. 节点名称映射
    • CSI GetNodeInfo 调用返回存储系统引用节点的名称。Kubernetes 必须在后续的 ControllerPublishVolume 调用中使用该名称。因此,当新的 CSI 驱动程序注册时,Kubernetes 会在 CSINode 对象中存储存储系统节点 ID 以供将来参考。
  2. 驱动程序可用性
    • kubelet 可以与 kube-controller-manager 和 Kubernetes 调度程序通信,指示驱动程序在节点上是否可用(已注册)。
  3. 卷拓扑
    • CSI GetNodeInfo 调用返回一组标识节点拓扑的键/值标签。Kubernetes 使用这些信息进行拓扑感知的配置。它将键/值存储为 Kubernetes 节点对象的标签。为了回忆哪些 Node 标签键属于特定的 CSI 驱动程序,kubelet 将这些键存储在 CSINode 对象中以供将来参考。
apiVersion: storage.k8s.io/v1
kind: CSINode
metadata:
  name: node1
spec:
  drivers:
  - name: mycsidriver.example.com
    nodeID: storageNodeID1
    topologyKeys: ['mycsidriver.example.com/regions', 'mycsidriver.example.com/zones']
  • drivers - 运行在节点上的 CSI 驱动程序列表及其属性。
  • name - 此对象所指的 CSI 驱动程序。
  • nodeID - 驱动程序确定的节点分配标识符。
  • topologyKeys - 驱动程序支持的分配给节点的拓扑键列表。

node-driver-registrar

node-driver-registrar 是一个Sidecar容器,它通过 Kubelet 插件注册机制 注册 CSI 驱动程序。Kubelet 负责发出 CSI 的 NodeGetInfoNodeStageVolume 和 NodePublishVolume 调用。node-driver-registrar 使 Kubelet 知道哪个 Unix 域套接字用于发出这些调用。

node-driver-registrar 使用两个 UNIX 域套接字:

  • 注册套接字 :用于将驱动程序注册到 Kubelet,通常位于 /var/lib/kubelet/plugins_registry/<drivername.example.com>-reg.sock
  • CSI 驱动程序套接字 :Kubelet 用于与 CSI 驱动程序交互,通常位于 /var/lib/kubelet/plugins/<drivername.example.com>/csi.sock

必需参数

  • --csi-address:CSI 驱动程序套接字在 pod 内的路径。
  • --kubelet-registration-path:Kubelet 用于发出 CSI 操作的套接字路径

CSI Controller Sidecar

Kubernetes CSI Sidecar 容器是一组标准容器,旨在简化 Kubernetes 上 CSI 驱动程序的开发和部署。

以下均是一些controller容器,需要创建服务账户,并授予其足够的权限。都可以和ControllerServer放在同一Pod中运行。见部署方式

external-provisioner

external-Provisioner 是一个 sidecar 容器,它监视 Kubernetes API 服务器中的 PersistentVolumeClaim 对象,并调用指定的 CSI Driver 的 CreateVolume 方法来配置新的卷。卷的配置是通过创建新的 PersistentVolumeClaim 对象触发的,前提是 PVC 引用一个 Kubernetes StorageClass,并且 StorageClass 中的 provisioner 字段的名称与指定的 CSI 端点在 GetPluginInfo 调用中返回的名称匹配。

成功配置新卷后,外部 provisioner 会创建一个 Kubernetes PersistentVolume 对象来表示该卷。

当绑定到该驱动的 PersistentVolume 的 PersistentVolumeClaim 对象被删除,并且其回收策略为 delete 时,外部 provisioner 会触发 DeleteVolume 操作以删除指定 CSI 端点中的卷。

external-attacher

external-attacher 是一个sidecar 容器,负责监视由控制器管理器创建的 VolumeAttachment 对象,并通过调用 CSI 驱动的 ControllerPublish 和 ControllerUnpublish 函数,将卷附加到节点。由于 Kubernetes 控制器管理器中的内部附加/分离控制器没有直接与 CSI 驱动的接口,因此需要使用 external-attacher。

external-resizer

CSI external-resizer 是一个sidecar容器,监视 Kubernetes API 服务器中的 PersistentVolumeClaim 更新,并在用户请求扩展存储时触发 ControllerExpandVolume 操作。

external-snapshotter

CSI Snapshotter 是 Kubernetes 中实现的容器存储接口(CSI)的一部分,实现了卷快照和卷组快照功能。

external-snapshotter 主要由以下组件构成:

  • Volume SnapshotVolume Group Snapshot CRDs:自定义资源定义,用于定义快照对象。
  • 快照控制器 :负责处理快照的创建和管理逻辑。
  • 验证 webhook:用于验证快照对象的合法性。

部署方式

CSI驱动程序通常在Kubernetes中作为两个组件部署:控制器组件和每个节点组件。

控制器插件

  • 部署方式 :控制器组件可以作为Deployment或StatefulSet部署在集群中的任何节点上。它包括实现CSI控制器服务的CSI驱动程序和一个或多个侧车容器(sidecar containers)。
  • 功能 :控制器侧车容器通常与Kubernetes对象交互,并调用驱动程序的CSI控制器服务。它通常不需要直接访问主机,并且可以通过Kubernetes API和外部控制平面服务执行所有操作。
  • 高可用性 :可以部署多个控制器组件以实现高可用性(HA),但建议使用领导选举来确保同时只有一个活动控制器。
  • 侧车容器 :控制器侧车包括external-provisioner、external-attacher、external-snapshotter和external-resizer等。包括侧车的部署可能是可选的。

节点插件

  • 部署方式 :节点组件应通过DaemonSet在集群中的每个节点上部署。它包括实现CSI节点服务的CSI驱动程序和node-driver-registrar侧车容器。
  • Kubelet通信 :Kubernetes的kubelet在每个节点上运行,负责调用CSI节点服务。这些调用挂载和卸载存储卷,使其可供Pod使用。
  • 卷挂载 :节点插件需要直接访问主机,以使块设备和/或文件系统挂载可用于Kubernetes kubelet。

部署步骤

  1. 允许特权Pod:Kubernetes集群必须允许特权Pod(即API服务器和kubelet的--allow-privileged标志必须设置为true)。
  2. 启用挂载传播 :CSI依赖于挂载传播功能,以允许一个容器挂载的卷与同一Pod中的其他容器或同一节点上的其他Pod共享。

举个例子

还是以aws ebs插件举例。可以看 aws-ebs-csi-driver/deploy 中的Kubernetes资源配置。

clusterrole-attacher.yaml
clusterrolebinding-attacher.yaml
clusterrolebinding-csi-node.yaml
clusterrolebinding-provisioner.yaml
clusterrolebinding-resizer.yaml
clusterrolebinding-snapshotter.yaml
clusterrole-csi-node.yaml
clusterrole-provisioner.yaml
clusterrole-resizer.yaml
clusterrole-snapshotter.yaml
controller.yaml
csidriver.yaml
kustomization.yaml
node.yaml
poddisruptionbudget-controller.yaml
rolebinding-leases.yaml
role-leases.yaml
serviceaccount-csi-controller.yaml
serviceaccount-csi-node.yaml

其中大部分都是RBAC权限控制的配置。其他进行一下简要说明:

  • CSIDriver.yaml: 用于创建 CSIDriver 资源;
  • node.yaml: 是DaemonSet,将会在每个节点上都部署该Pod。里面包含了 node-driver-registrar 注册容器,livenessprobe 健康检查容器,还有 aws-ebs-csi-driver 本身。
    • aws-ebs-csi-driver 传入参数为 node,可以结合 driver.go 代码中的 NewDriver 可以看到传入 node 时,仅开启了 NodeService,因此 node.yaml 不存在任何 ControllerService的接口。
  • controller.yaml: 是Deployment,默认两个副本数。其中包含了 external-provisioner / external-attacher / external-resizer / external-snapshotter 四个管理容器,livenessprobe 健康检查容器,还有 aws-ebs-csi-driver 本身。
    • aws-ebs-csi-driver 在此没传入模式,代码中默认使用all模式,同时启用 ControllerService 和 NodeService,但由于没有注册容器,此处的NodeService应该是不生效的。

特性

Kubernetes CSI 包含很多子特性,可以从 Features 找到说明。

参考文献

  1. container-storage-interface/spec · GitHub
  2. Kubernetes CSI Developer Documentation