8.4 CSI存储机制详解

Kubernetes从1.9版本开始引入容器存储接口Container Storage Interface(CSI)机制,用于在Kubernetes和外部存储系统之间建立一套标准的存储管理接口,通过该接口为容器提供存储服务。CSI到Kubernetes 1.10版本时升级为Beta版本,到Kubernetes 1.13版本时升级为稳定版本,已逐渐成熟。

8.4.1 CSI的设计背景

Kubernetes通过PV、PVC、StorageClass已经提供了一种强大的基于插件的存储管理机制,但是各种存储插件提供的存储服务都是基于一种被称为“in-tree”(树内)的方式提供的,这要求存储插件的代码必须被放进Kubernetes的主干代码库中才能被Kubernetes调用,属于紧耦合的开发模式。这种“in-tree”方式会带来一些问题:

◎ 存储插件的代码需要与Kubernetes的代码放在同一代码库中,并与Kubernetes的二进制文件共同发布;

◎ 存储插件代码的开发者必须遵循Kubernetes的代码开发规范;

◎ 存储插件代码的开发者必须遵循Kubernetes的发布流程,包括添加对Kubernetes存储系统的支持和错误修复;

◎ Kubernetes社区需要对存储插件的代码进行维护,包括审核、测试等;

◎ 存储插件代码中的问题可能会影响Kubernetes组件的运行,并且很难排查问题;

◎ 存储插件代码与Kubernetes的核心组件(kubelet和kube-controller-manager)享有相同的系统特权权限,可能存在可靠性和安全性问题;

◎ 存储插件代码与Kubernetes代码一样被强制要求开源、公开。

Kubernetes已有的Flex Volume插件机制试图通过为外部存储暴露一个基于可执行程序(exec)的API来解决这些问题。尽管它允许第三方存储提供商在Kubernetes核心代码之外开发存储驱动,但仍然有两个问题没有得到很好的解决:

◎ 部署第三方驱动的可执行文件仍然需要宿主机的root权限,存在安全隐患;

◎ 存储插件在执行mount、attach这些操作时,通常需要在宿主机上安装一些第三方工具包和依赖库,使得部署过程更加复杂,例如部署Ceph时需要安装rbd库,部署GlusterFS时需要安装mount.glusterfs库,等等。

基于以上这些问题和考虑,Kubernetes逐步推出与容器对接的存储接口标准,存储提供方只需基于标准接口进行存储插件的实现,就能使用Kubernetes的原生存储机制为容器提供存储服务了。这套标准被称为CSI(容器存储接口)。在CSI成为Kubernetes的存储供应标准之后,存储提供方的代码就能与Kubernetes代码彻底解耦,部署也与Kubernetes核心组件分离。显然,存储插件的开发由提供方自行维护,就能为Kubernetes用户提供更多的存储功能,也更加安全可靠。基于CSI的存储插件机制也被称为“out-of-tree”(树外)的服务提供方式,是未来Kubernetes第三方存储插件的标准方案。可以到CSI项目官网获取更多信息。

8.4.2 CSI的核心组件和部署架构

图8.5展示了Kubernetes CSI存储插件的核心组件和推荐的容器化部署架构。

图8.5 Kubernetes CSI存储插件的关键组件和推荐的容器化部署架构

其中主要包括两类组件:CSI Controller和CSI Node。

1.CSI Controller

CSI Controller的主要功能是提供存储服务视角对存储资源和存储卷进行管理和操作。在Kubernetes中建议将其部署为单实例Pod,可以使用StatefulSet或Deployment控制器进行部署,设置副本数量为1,保证一种存储插件只运行一个控制器实例。

在这个Pod内部署两个容器,分别提供以下功能。

(1)与Master(kube-controller-manager)通信的辅助sidecar容器。在sidecar容器内又可以包含external-attacher和external-provisioner两个容器,它们的功能分别如下。

◎ external-attacher:监控VolumeAttachment资源对象的变更,触发针对CSI端点的ControllerPublish和ControllerUnpublish操作。

◎ external-provisioner:监控PersistentVolumeClaim资源对象的变更,触发针对CSI端点的CreateVolume和DeleteVolume操作。

另外,社区正在引入具备其他管理功能的sidecar工具,例如:external-snapshotter,用于管理存储快照,目前为Alpha阶段;external-resizer,用于管理存储容量扩容,目前为Beta阶段。

(2)CSI Driver存储驱动容器,由第三方存储提供商提供,需要实现上述接口。

这两个容器通过本地Socket(Unix Domain Socket,UDS),并使用gPRC协议进行通信。sidecar容器通过Socket调用CSI Driver容器的CSI接口,CSI Driver容器负责具体的存储卷操作。

2.CSI Node

CSI Node的主要功能是对主机(Node)上的Volume进行管理和操作。在Kubernetes中建议将其部署为DaemonSet,在需要提供存储资源的各个Node上都运行一个Pod。

在这个Pod中部署以下两个容器。

(1)与kubelet通信的辅助sidecar容器node-driver-registrar,主要功能是将存储驱动注册到kubelet中。

(2)CSI Driver存储驱动容器,由第三方存储提供商提供,主要功能是接收kubelet的调用,需要实现一系列与Node相关的CSI接口,例如NodePublishVolume接口(用于将Volume挂载到容器内的目标路径)、NodeUnpublishVolume接口(用于从容器中卸载Volume),等等。

node-driver-registrar容器与kubelet通过Node主机一个hostPath目录下的unix socket进行通信。CSI Driver容器与kubelet通过Node主机另一个hostPath目录下的unix socket进行通信,同时需要将kubelet的工作目录(默认为/var/lib/kubelet)挂载给CSI Driver容器,用于为Pod进行Volume的管理操作(包括mount、umount等)。

8.4.3 CSI存储插件应用实战

下面以csi-hostpath插件为例,对如何部署CSI插件、用户如何使用CSI插件提供的存储资源进行详细说明。

(1)设置Kubernetes服务启动参数。为kube-apiserver、kube-controller-manager和kubelet服务的启动参数添加如下内容:

这3个特性开关是Kubernetes从1.12版本开始引入的Alpha版本功能,CSINodeInfo和CSIDriverRegistry需要手工创建其相应的CRD资源对象。

Kubernetes 1.10版本所需的CSIPersistentVolume和MountPropagation特性开关已经默认启用,KubeletPluginsWatcher特性开关也在Kubernetes 1.12版本中默认启用,无须在命令行参数中指定。

(2)创建CSINodeInfo和CSIDriverRegistry CRD资源对象。

csidriver.yaml的内容如下:

csinodeinfo.yaml的内容如下:

使用kubectl create命令完成创建:

(3)创建csi-hostpath存储插件相关组件,包括csi-hostpath-attacher、csi-hostpath-provisioner和csi-hostpathplugin(其中包含csi-node-driver-registrar和hostpathplugin)。其中为每个组件都配置了相应的RBAC权限控制规则,对于安全访问Kubernetes资源对象非常重要。

csi-hostpath-attacher.yaml的内容如下:

csi-hostpath-provisioner.yaml的内容如下:

csi-hostpathplugin.yaml的内容如下:

使用kubectl create命令完成创建:

确保3个Pod都正常运行:

至此就完成了CSI存储插件的部署。

(4)应用容器使用CSI存储。应用程序如果希望使用CSI存储插件提供的存储服务,则仍然使用Kubernetes动态存储管理机制。首先通过创建StorageClass和PVC为应用容器准备存储资源,然后容器就可以挂载PVC到容器内的目录下进行使用了。

创建一个StorageClass,provisioner为CSI存储插件的类型,在本例中为csi-hostpath:

创建一个PVC,引用刚刚创建的StorageClass,申请存储空间为1GiB:

查看PVC和系统自动创建的PV,状态为Bound,说明创建成功:

最后,在应用容器的配置中使用该PVC:

在Pod创建成功之后,应用容器中的/data目录使用的就是CSI存储插件提供的存储。

我们通过kubelet的日志可以查看到Volume挂载的详细过程:

8.4.4 CSI存储快照管理

Kubernetes从1.12版本开始引入存储卷快照(Volume Snapshots)功能,到1.17版本时达到Beta阶段。为此,Kubernetes引入了3个主要的资源对象VolumeSnapshotContent、VolumeSnapshot和VolumeSnapshotClass进行管理,它们均为CRD自定义资源对象。

◎ VolumeSnapshotContent:基于某个PV创建的快照,类似于PV的“资源”概念。

◎ VolumeSnapshot:需要使用某个快照的申请,类似于PVC的“申请”概念。

◎ VolumeSnapshotClass:设置快照的特性,屏蔽VolumeSnapshotContent的细节,为VolumeSnapshot绑定提供动态管理,类似于StorageClass的“类型”概念。

为了提供对存储快照的管理,还需在Kubernetes中部署快照控制器(Snapshot Controller),并且为CSI驱动部署一个csi-snapshotter辅助工具sidecar。Snapshot Controller持续监控VolumeSnapshot和VolumeSnapshotContent资源对象的创建,并且在动态供应模式下自动创建VolumeSnapshotContent资源对象。csi-snapshotter辅助工具sidecar则持续监控VolumeSnapshotContent资源对象的创建,一旦出现新的VolumeSnapshotContent或者被删除,就自动调用针对CSI endpoint的CreateSnapshot或DeleteSnapshot方法,完成快照的创建或删除。

接下来对VolumeSnapshotContent、VolumeSnapshot和VolumeSnapshotClass的概念和应用进行说明。

1.VolumeSnapshot和VolumeSnapshotContent的生命周期

VolumeSnapshot和VolumeSnapshotContent的生命周期包括资源供应、资源绑定、对使用PVC的保护机制和资源删除等各个阶段。

(1)资源供应。与PV的资源供应模型类似,快照资源VolumeSnapshotContent也可以以静态供应或动态供应两种方式提供。

◎ 静态供应(Pre-provisioned):集群管理员预先创建好一组VolumeSnapshotContent。

◎ 动态供应(Dynamic):基于VolumeSnapshotClass类型,由系统在用户创建VolumeSnapshot申请时自动创建VolumeSnapshotContent。

(2)资源绑定。快照控制器(Snapshot Controller)负责将VolumeSnapshot与一个合适的VolumeSnapshotContent进行绑定,包括静态供应和动态供应两种情况。VolumeSnapshot与VolumeSnapshotContent的绑定关系为一对一,不会存在一对多的绑定关系。

(3)对使用中PVC的保护机制。当存储快照VolumeSnapshot正在被创建且还未完成时,相关的PVC将会被标记为“正被使用中”,如果用户对PVC进行删除操作,则系统将不会立即删除PVC资源对象,以避免快照还未做完的数据丢失。对PVC的删除操作将会延迟到VolumeSnapshot创建完成(状态为readyToUse)或者被终止(aborted)的情况下完成。

(4)资源删除。对VolumeSnapshot发起删除操作时,对与其绑定的后端VolumeSnapshotContent的删除操作将基于删除策略(DeletionPolicy)的设置而定,可以设置的策略如下。

◎ Delete:自动删除VolumeSnapshotContent资源对象和快照的内容。

◎ Retain:VolumeSnapshotContent资源对象和快照的内容都将保留,需要手工清理。

2.VolumeSnapshot、VolumeSnapshotContent和VolumeSnapshotClass示例

1)VolumeSnapshot(快照申请)示例

(1)申请动态存储快照的VolumeSnapshot:

主要配置参数如下。

◎ volumeSnapshotClassName:存储快照类别的名称,未指定时,系统将使用可用的默认类别进行提供。

◎ persistentVolumeClaimName:作为数据来源的PVC名称。

(2)申请静态存储快照的VolumeSnapshot:

主要配置参数为volumeSnapshotContentName,表示VolumeSnapshotContent名称。

2)VolumeSnapshotContent(快照)示例

(1)在动态供应模式下,系统自动创建的VolumeSnapshotContent内容如下:

volumeHandle字段的值是在后端存储上创建并由CSI驱动在创建存储卷期间返回的Volume的唯一标识符。在动态供应模式下需要该字段,它指定的是快照的来源Volume信息。

(2)在静态供应模式下需要用户手工创建存储快照VolumeSnapshotContent,例如:

主要配置参数如下。

◎ deletionPolicy:删除策略。

◎ source.snapshotHandle:在后端存储上创建的快照的唯一标识符。

◎ volumeSnapshotRef:由系统为VolumeSnapshot完成绑定之后自动设置。

3)VolumeSnapshotClass(快照类别)示例

示例如下:

主要配置参数如下。

◎ driver:CSI存储插件驱动的名称。

◎ deletionPolicy:删除策略,可以被设置为Delete或Retain,将被系统设置为动态创建出的VolumeSnapshotContent资源的删除策略。

◎ parameters:存储插件所需配置的参数,由CSI驱动提供具体的配置参数。

对于未设置VolumeSnapshotClass的VolumeSnapshot(申请),管理员也可以像提供默认StorageClass一样,在集群中设置一个默认的VolumeSnapshotClass,这通过在VolumeSnapshotClass资源对象中设置snapshot.storage.kubernetes.io/is-default-class=true的annotation进行标记,例如:

3. 基于存储快照(Snapshot)创建新的PVC存储卷

Kubernetes对基于存储快照(Snapshot)创建存储卷的支持到1.17版本时达到Beta阶段。要启用该特性,就需要在kube-apiserver、kube-controller-manager和kubelet服务的特性开关中进行启用:--feature-gates=...,VolumeSnapshotDataSource。

然后,就可以基于某个存储快照创建一个新的PVC存储卷了。下面是一个PVC定义的示例,其中通过dataSource字段设置基于名为“new-snapshot-test”的存储快照进行创建:

4.PVC存储卷克隆

CSI类型的存储还支持存储的克隆功能,可以基于某个系统中已存在的PVC克隆为一个新的PVC,通过在dataSource字段中设置来源PVC实现。

一个PVC的克隆定义为已存在的一个存储卷的副本,Pod应用可以像使用标准存储卷一样使用该克隆。唯一的区别是,系统在为克隆PVC提供后端存储资源时,不是新建一个PV,而是复制一个与原PVC绑定PV完全一样的PV。

从Kubernetes API的角度来看,克隆的实现只是增加了在创建新PVC时将现有PVC指定为数据源的能力,并且要求原PVC必须已完成绑定并处于可用状态(Available)。

在使用克隆功能时,需要注意以下事项。

◎ 对克隆的支持仅适用于CSI类型的存储卷。

◎ 克隆仅适用于动态供应模式。

◎ 克隆功能取决于具体的CSI驱动的实现机制。

◎ 克隆要求目标PVC和源PVC处于相同的命名空间中。

◎ 克隆仅支持在相同的StorageClass中完成:①目标Volume与源Volume具有相同的StorageClass;②可以使用默认的存储类别(Default StorageClass),可以省略storageClassName字段。

◎ 克隆要求两个存储卷的存储模式(VolumeMode)相同,同为文件系统模式或块存储模式。

下面是创建一个PVC克隆的示例:

关键配置参数如下。

◎ dataSource:设置来源PVC的名称。

◎ resources.requests.storage:存储空间需求,必须大于或等于源PVC的空间。

克隆成功后,新的名为“clone-of-pvc-1”的PVC将包含与源PVC“pvc-1”完全相同的存储内容,然后Pod就能像使用普通PVC一样使用该克隆PVC了。

另外,克隆PVC与源PVC并没有直接的关联关系,用户完全可以将其当作一个普通的PVC,也可以对其再次进行克隆、快照、删除等操作。

8.4.5 CSI的发展

CSI正在逐渐成为Kubernetes中存储卷的标准接口,越来越多的存储提供商都提供了相应的实现和丰富的存储管理功能。本节对Kubernetes已支持的CSI插件提供商、CSI对裸块设备的支持、CSI对临时存储卷的支持、in-tree插件的迁移等发展进行说明。

目前可用于生产环境的CSI插件列表如表8.5所示。

表8.5 目前可用于生产环境的CSI插件列表

▼续表

▼续表

▼续表

实验性的CSI插件列表如表8.6所示。

表8.6 实验性的CSI插件列表

各CSI存储插件都提供了容器镜像,与external-attacher、external-provisioner、node-driver-registrar等sidecar辅助容器一起完成存储插件系统的部署,部署配置详见官网中各插件的链接。

1.CSI对裸块设备(Raw Block Volume)的支持

Kubernetes对CSI类型存储卷的裸块设备(Raw Block Volume)的支持到1.18版本时达到稳定阶段。

用户仍然只需在PV和PVC资源对象中设置裸块设备的存储模式(volumeMode=Block)即可,无须关心后端存储是否为CSI类型的插件。

2.CSI对临时存储卷(CSI Ephemeral Volume)的支持

Kubernetes对CSI以临时存储卷形式为Pod提供存储资源的支持到1.16版本时达到Beta阶段。临时存储卷不再是持久化的,即不使用PV资源,就像EmptyDir一样为Pod提供临时存储空间。目前仅有部分CSI驱动支持临时存储卷,例如ArStor CSI、Cinder、democratic-csi、Google Cloud Storage、HPE、Intel PMEM-CSI、Secrets Store CSI Driver等,请参考CSI驱动列表的说明来查看是否提供支持。

从概念上来说,CSI临时存储卷类似于ConfigMap、DownwardAPI、Secret等类型的存储卷,其存储在Node本地进行管理,随着Pod的创建而一同创建。在通常情况下,CSI临时存储卷的创建不容易失败,否则会卡住Pod的启动过程。这种类型的存储卷不支持基于存储容量感知的调度策略,也不受Pod资源使用的限制,因为只能由存储驱动自行管理如何使用资源,Kubernetes无法再对存储资源进行管理。

为了启用这个特性,需要为kube-apiserver、kube-controller-manager和kubelet服务设置启动参数--feature-gates=CSIInlineVolume=true进行开启,该特性开关从Kubernetes 1.16版本开始默认启用。

在下面的例子中使用了inline.storage.kubernetes.io驱动为Pod提供临时存储:

其中,volumeAttributes字段指定CSI驱动提供的存储卷信息。csi部分的配置由CSI驱动提供商进行具体配置,在资源对象层面没有统一的标准定义,请参考各驱动提供商的文档进行设置。

另外,集群管理员还可以使用PodSecurityPolicy(参见6.6节的说明)设置允许启用的CSI驱动列表,这可以在Pod的定义中通过allowedCSIDrivers字段进行设置。

3. 通用临时存储卷(Generic Ephemeral Volume)和CSI存储容量跟踪特性

CSI的临时存储卷(Ephemeral Volume)使用CSI驱动提供了一种存储扩展机制,但是为了实现类似于EmptyDir这种轻量级的本地临时存储,必须修改CSI驱动程序。对于需要在某些节点上消耗大量资源的存储卷,或者仅在特定节点上可用的特殊存储,通过修改CSI插件无法实现统一标准的配置机制,因此,Kubernetes从1.19版本开始引入了两个新的功能特性,目前均为Alpha阶段:

◎ 通用临时卷(Generic Ephemeral Volume);

◎ CSI存储容量跟踪(Storage Capacity Tracking)。

新特性可以实现与EmptyDir类似的功能,但更加灵活,其优势包括:

◎ 存储资源可以是本地存储或者是网络存储。

◎ 存储卷的空间可以被设置为固定的大小,Pod无法超限使用。

◎ 任何支持提供PV的CSI插件均可使用临时存储卷,并且可以实现CSI的GetCapacity调用(用于存储容量跟踪)。

◎ 在存储卷中可以有初始化的数据,由驱动提供。

◎ 支持对存储卷的典型操作,例如快照、克隆、扩容、存储空间跟踪等(假设驱动提供支持)。

◎ Kubernetes的调度器(Scheduler)基于配置即可选择适合Pod存储需求的Node,即不再需要通过自定义的扩展调度器或Webhook进行实现。

这些新特性可以支持更多的应用场景,如下所述。

(1)例如Memcached使用持久性内存。最新版本的Memcached软件添加了对持久性内存的支持,以替代使用标准系统内存(DRAM)。部署Memcached应用程序时,可以通过通用临时卷的配置来申请一部分PMEM内存空间进行使用。PMEM的CSI驱动程序由Intel提供了开源实现(目前为Alpha阶段)。

(2)将本地LVM存储作为暂存空间。当应用程序需要保存的数据超过了系统内存RAM的大小,而EmptyDir又无法满足存储需求(例如性能)时,可以通过申请通用临时卷来实现。一个开源的实现为TopoLVM。

(3)对含有数据的存储卷进行只读访问。有时,一个存储卷或PV在创建出来时就包含数据文件,例如:从一个存储快照(Snapshot)中恢复的卷;一个新的克隆卷;使用通用数据填充器(Generic Data Populators)生成的卷。这些存储卷可以被挂载为只读访问模式。

要启用通用临时卷的特性,就需要设置Kubernetes各服务的启动参数--feaure-gates=GenericEphemeralVolume=true进行启用,目前为Alpha阶段。

下面是一个使用通用临时卷的Pod示例:

其中,在ephemeral字段下通过volumeClaimTemplate定义了Pod需要的PVC参数,可以设置的参数与一个标准的PVC资源对象相同,包括Label、Annotation、存储类别、资源需求等。

该Pod被创建时,系统将自动创建一个符合要求的PVC资源对象,与Pod处于相同的命名空间中,并且设置该PVC的owner为该Pod,确保在Pod被删除时,PVC也会自动被删除。PVC的名称则由Pod名称和Volume名称组合而成,以“-”符号连接。上例中由系统自动创建的PVC名称将为“my-app-scratch-volume”。当PVC被创建时,系统将会驱动后台PV的创建。如果设置了StorageClass,系统将使用动态供应模式创建PV,并自动与PVC进行绑定。

需要注意的是,用户可以创建Pod时,默认也可以创建通用临时卷,如果需要进行安全限制,则集群管理员可以进行如下设置:

◎ 禁用GenericEphemeralVolume特性;

◎ 使用PodSecurityPolicy定义允许创建的Volume类型列表。

要启用CSI存储容量跟踪(Storage Capacity Tracking)特性,就需要设置Kubernetes各服务的启动参数--feaure-gates=CSIStorageCapacity=true及--runtime-config=storage.k8s.io/v1alpha1=true进行启用,目前为Alpha阶段。对存储容量跟踪的支持由CSI驱动提供,CSI驱动应向Kubernetes Master报告存储的使用情况,以便调度器(Scheduler)根据Pod的存储需求进行合理调度。

通过在CSIDriver资源对象中设置storageCapacity=true,可以标识CSI驱动能够提供存储的容量跟踪功能,并通过CSIStorageCapacity资源对象将存储容量的使用情况反馈给Kubernetes Master。在每个CSIStorageCapacity资源对象中都包含一个StorageClass的容量信息,以及定义哪些Node可以访问该存储资源。

一旦有了这些信息,调度器就可以进行简单的逻辑判断,选择拥有足够存储空间的Node对Pod进行调度了。对于CSI临时存储卷(Ephemeral Volume),调度器不会考虑存储容量的问题,这是基于这样的前提假设:临时存储仅被特殊的CSI驱动在某个Node本地使用,不会消耗太多资源。

对于通用临时存储卷和存储容量跟踪的特性,Kubernetes社区仍在进行大量的讨论和设计,有兴趣的读者可以持续跟踪或参与Storage SIG特别兴趣小组的讨论。

4. 将in-tree插件迁移到CSI驱动(CSI Volume Migration)

CSI的后续工作还包括将Kubernetes内置的 in-tree存储卷插件迁移为CSI驱动。Kubernetes正在逐步开发CSI Migration机制,将正在使用的in-tree插件重定向到外部CSI驱动,无须改变当前用户配置的StorageClass、PV、PVC等资源。该机制从Kubernetes 1.14版本开始引入,到1.17版本时达到Beta阶段,这需要各存储提供商和Kubernetes社区共同开发和完善。