问题
在学习之前,对 PV 的理解是,它是存储的抽象,它将存储从底层基础设施中解耦出来,提供了一种对存储资源的管理方式。对 PVC 的理解是,它是开发人员或者集群使用人员对存储需求的声明,这些人员通过 PVC 来描述他们需要多少存储空间,比如声明他们需要 2GB SSD,而不需要对存储介质等相关细节有所了解。
在上述的了解之上,还有其他一些耳闻,但是总会有这么几个问题,困扰着我,
-
时常听到的 StorageClass 是什么?
-
PVC 需要和 PV 进行绑定,这个绑定的过程是怎么样的?涉及哪些组件,这些组件在绑定过程中的主要功能是什么?
-
怎么去开发一个 CSI 插件?
-
PV 可以是任何类型的存储,比如 NFS、对象存储、各大云厂商的云盘。那么在 Pod 挂载 PV 之后,Pod 可以在这个 PV 上读写数据,读写使用的是内核提供的文件读写接口,就跟写本地文件一样。那么,针对对象存储这种类型的存储来说,是怎么将读写文件的内容,转换为对象存储中 bucket 中的内容?
原理
之前的文章,提到容器持久化的基本原理是将容器内的某个目录和宿主机的目录绑定起来。而 CSI 插件的主要作用就是准备好这个宿主机目录,比如准备好一个额外的磁盘挂载到这个宿主机目录上。之后,kubelet 调用 CRI 接口,由容器自身的实现方式将这个宿主机目录和容器内的某个目录进行绑定,这个是容器侧的实现。
CSI 按照实现方式有两种类型,一种是 in-tree 类型,一种是 out-of-tree 类型。
-
in-tree 类型的 CSI 插件,是指相关代码实现在 kubernetes 核心组件内部,比如在 KCM、Kubelet 中就包含了一些云厂商提供的存储服务的 CSI 插件代码。
-
out-of-tree 是指 CSI 插件的相关代码实现在 Kubernetes 核心组件之外的。
为了方便开发人员开发 out-of-tree 类型的 CSI 插件,Kubernetes 提供了大量的 SideCar 组件来减少开发人员实现的复杂度。在 SideCar 组件配合的情况下,开发人员往往只需要按照 CSI 接口规范实现 CSI Controller Plugin 和 CSI Node Plugin 这两个组件即可,前者主要是管理 Volume 逻辑的实现,比如创建/删除 Volume 等,后者主要是 Volume 在节点上的逻辑实现,比如 Mount/Unmount 等,后者以 DaemonSet 形式部署在每个 node 中。我们自己实现的两个组件,通过 Unix Domani Socket gRPC 和 Sidecar 组件进行交互。
官方已经在推动 in-tree 类型插件的代码从 Kubernetes 核心组件中移出,同时 out-of-tree 也是我们实现 CSI 插件最优的方式,因此本文主要介绍 out-of-tree 类型的插件。
整体流程
Pod 挂载 Volume 的整个流程,主要分为三个阶段:
-
Provision/Delete:创建或删除 Volume。
-
Attach/Detach:将 Volume attach 到节点上面。在使用某些云厂商提供的存储服务时,通常需要先把存储设备 attch 到 Node 节点上。
-
Mount/Unmount:将 Volume 挂载到 Kubelet 提供的目录(宿主机的目录)上。之后,这个目录会通过 bind 的方式被绑定到 Pod 中指定的目录。
当然不是每个 CSI 存储方案都会经历这三个阶段,比如 NFS 就没有 Attach/Detach 阶段。将上述流程进行补充,完整地介绍下整个流程:
-
用户创建了一个包含 PVC 的 Pod。
-
调度阶段。Scheduler 根据 Pod 的配置、节点状态,选好该 Pod 要调度的节点。
-
Provision 阶段。PVController 和 external-provisioner 都会 watch PVC。对于 out-of-tree 类型的 CSI 插件来说,PVController 会先给 PVC 打上
volume.beta.kubernetes.io/storage-provisioner={csi driver name}
这样的 annotation。external-provisioner watch 到符合自己的 PVC 之后,调用 CSI Controller Plugin(自己实现的 Plugin)创建 Volume。之后 external-provisioner 会在集群中创建相应的 PV 对象,并将 PV 对象和 PVC 进行绑定。 -
Attach 阶段。ADController watch 到有 PV 创建。对于 out-of-tree 类型的 CSI 插件来说,ADController 会先调用 in-tree CSI 插件创建 VolumeAttachment 资源。external-attacher watch 到该资源被创建后,会调用 CSI Controller Plugin 将 Volume attach 到节点上。
-
Mount 阶段。Kubelet 中的 Volume Manager 会等待 Volume attach 完成,完成之后 Kubelet 会调用 CSI Node Plugin 把 Volume 挂载到宿主机的
/var/lib/kubelet/pods/${pod uid}/volumes/kubernetes.io****~${CSI Plugin Name}/${PV name}
目录上。 -
启动容器。Kubelet 在启动容器的会通过 bind mount 方式将上述目录映射到容器中。
下面分别详细分析一下 Provision、Attach、Mount 三个阶段。
Provision 阶段
provision 阶段主要由 PVController、external-provisioner 和 CSI Controller Plugin 三个组件共同完成。其中 PVController 和 external-provisioner 会 watch PVC 资源:
- PVController 会 watch PVC,它会判断这个 PVC 是由 in-tree 负责还是由 out-of-tree 负责,如果是 out-of-tree 负责的话,会给 PVC 打上
volume.beta.kubernetes.io/storage-provisioner={csi driver name}
这样的 annotation。 - external-provisioner 也会 watch PVC,它会判断 PVC annotation 中 volume.beta.kubernetes.io/storage-provisioner 的值是否与自己的 CSI Controller 一致。如果一致则调用 CSI Controller Plugin 的 CreateVolume 接口创建 volume。CSI Controller Plugin 通常通过 Storage Provider 获取 volume。
- 当 CSI Controller Plugin 的 CreateVolume 接口返回成功时,external-provisioner 会在集群中创建相应的 PV,同时将创建的 PV 与相应的 PVC 进行绑定。
Attach 阶段
attach 阶段主要由 ADController、external-attacher 和 CSI Controller Plugin 三个组件共同完成。ADController 中维护了两个数据结构:
- DesiredStateOfWorld(DSW):集群中预期的数据卷 attach 状态,包含了 nodes->volumes->pods 的信息。
- ActualStateOfWorld(ASW):集群中实际的数据卷 attach 状态,包含了 volumes->nodes 的信息。
并且通过 3 个组件维护 DSW 和 ASW:
- desiredStateOfWorldPopulator:通过一个 GoRoutine 周期性运行,主要功能是更新 DSW。比如,遍历所有 DSW 中的 Pods,若其已从集群中删除则从 DSW 中移除。遍历实际获取的 Pod,若 DSW 中不存在该 Pod 则添加至 DSW。
- pvcWorker:watch PVC 的 add/update 事件,处理 PVC 相关的 Pod,并实时更新 DSW。
- reconciler:通过一个 GoRoutine 周期性运行,遍历 DSW/ASW 确保 volume attach/detach 完毕,并且不断更新 ASW。
- 如果没有进行 attach,ADController 会调用 in-tree 的 CSIAttacher 创建 VolumeAttachment(VA) 对象,该对象包含了待挂接 PV 信息、节点名称等。
- external-attacher 会 watch VA 资源,它会调用 CSI Controller Plugin 的 ControllerPublishVolume 接口,将 Volume attach 到节点上。
- 当 CSI Controller Plugin 的 ControllerPublishVolume 接口成功调用之后,external-attacher 会将对应的 VA 对象的 Attached 状态设为 true。
- ADController watch 到 VolumeAttachment 对象的 Attached 状态更新为 true,更新 ADController 中的 ASW。
Kubelet 组件和 ADController 都可以做 attach/detach 操作,若 Kubelet 的启动参数中指定了 --enable-controller-attach-detach,则由 Kubelet 来做;否则默认由 ADController 做。以下以AD控制器为例来讲解attach/detach操作。
Mount 阶段
mount 阶段由 kubelet 中的 volumeManager 组件和 CSI Node Plugin 共同完成。
volumeManager 组件有两个核心变量:
- DesiredStateOfWorld(DSW):集群中预期的数据卷挂载状态,包含了 volumes->pods 的信息。
- ActualStateOfWorld(ASW):集群中实际的数据卷挂载状态,包含了 volumes->pods 的信息。
并且包含两个核心循环:desiredStateOfWorldPopulator 和 reconciler,均通过 GoRoutine 运行。
-
desiredStateOfWorldPopulator 循环中会依次执行
findAndAddNewPods
和findAndRemoveDeletedPods
两个函数,通过这两个函数更新 DSW。findAndAddNewPods
:这个函数的主要作用是获取当前 kubelet 管理的所有 Pod,如果 Pod 中的 volume 信息不在 DSW 中,则将 volume 信息记录到 DesiredStateOfWorld 中。findAndRemoveDeletedPods
:这个函数的主要作用是遍历 DSW,然后查询 kubelet 管理的 Pod,是否有相应的 volume,如果没有,则将其从 DSW 中删除。
-
reconciler 循环中会依次执行
unmountVolumes
、mountOrAttachVolumes
和unmountDetachDevices
三个函数。主要作用是从 DSW 中获取相关的 volume 信息,确保 volume 挂载/卸载完毕,并不断更新 ASW。-
unmountVolumes
:遍历 ASW 中的 volume,如果不在 DSW 中,则表示这个 volume 应该是要卸载的,对于 out-of-tree 来说的话,则会调用NodeUnpublishVolume
接口和NodeUnstageVolume
接口,unmount 掉相应的 volume,并在 ASW 中记录。 -
mountOrAttachVolumes
:遍历 DSW 中的 volume,如果不在 ASW 中,则表示这个 volume 是需要挂载的。因此,-
会先将 volume 挂载到一个临时全局目录,对于 out-of-tree 来说的话,则是调用
NodeStageVolume
接口。同时更新 ASW,表示该 volume 已挂载到了临时全局目录。考虑先挂载到临时全局目录的主要原因是,比如块设备在 Linux 上只能挂载一次,而在 K8s 场景中,一个 volume 可能被挂载到同一个 Node 上的多个 Pod 实例中。若块设备格式化后先挂载至 Node 上的一个临时全局目录,然后再使用 Linux 中的 bind mount 技术把这个临时全局目录挂载到 Pod volume 对应的目录上。
-
bind-mount volume 到 Pod volume 对应的目录上(
/var/lib/kubelet/pods/${pod uid}/volumes/kubernetes.io****~${CSI Plugin Name}/${PV name}
),对于 out-of-tree 来说的话,则是调用NodePublishVolume
接口,同时更新 ASW。
-
-
unmountDetachDevices
:确保需要 unmount 的 volumes 被 unmount。遍历一遍 ASW 中的 UnmountedVolumes,若其不在 DSW 中,则表示 volume 已经不被 Pod 所使用了。对于 out-of-tree 来说的话,则会调用NodeUnstageVolume
接口,同时更新 ASW。
-
以 containerd 为例,考虑将某个 volume 挂载到容器内的 /data 目录。
-
kubelet 会将这个 volume 最终挂载到 Pod volume 对应的目录上,比如
/var/lib/kubelet/pods/${pod uid}/volumes/kubernetes.io****~${CSI Plugin Name}/${PV name}
上。 -
之后,kubelet 会通过 CRI 接口启动容器。这个时候 Pod volume 对应的目录又会通过容器运行时的机制将这个目录挂载到容器内的 /data 目录上。
以 containerd 为例,容器内的 /data 目录其实也位于宿主机上,算是容器的可读写层。它通常位于
/var/lib/containerd/overlay
目录下的一个子目录中,具体路径类似于/var/lib/containerd/overlay/<long_hex_string>/merged/data
(其中 long_hex_string 是容器唯一标识符)。那么,containerd 会通过 bind-mount 的方式将 Pod volume 对应的目录挂载到/var/lib/containerd/overlay/<long_hex_string>/merged/data
上,也就是相当于将 Pod volume 挂载到了容器内的 /data 目录。所以,在 containerd 容器运行时中,通过 bind-mount 的方式将 Pod volume 对应的目录挂载到容器内的某个目录,其实还是相当于通过 bind-mount 的方式将 Pod volume 对应的目录挂载到宿主机上的另一个目录,只是这个目录是容器运行时使用的。
Sidecar 组件
Sidecar 组件文档:https://kubernetes-csi.github.io/docs/sidecar-containers.html
上文在介绍流程的时候,已经提到了 external-provisioner、external-attacher 这两个 sidecar 组件。除这两个组件之外,CSI 还提供了其他 sidecar 组件,如下所示(可以参考 CSI Sidecar 组件)。这些 sidecar 组件会分别跟我们要实现的 CSI Controller Plugin 和 CSI Node Plugin 组件进行交互。
组件名 | 交互对象 | |
---|---|---|
1 | external-provisioner | CSI Controller Plugin |
2 | external-attacher | CSI Controller Plugin |
3 | external-resizer | CSI Controller Plugin |
4 | external-snapshotter | CSI Controller Plugin |
5 | external-health-monitor-controller | CSI Controller Plugin |
6 | livenessprobe | CSI Controller Plugin/CSI Node Plugin |
7 | node-driver-registrar | CSI Node Plugin |
8 | external-health-monitor-agent | CSI Node Plugin |
external-provisioner
-
监听 PVC 对象,并调用 CSI Controller Plugin 的
CreateVolume
/DeleteVolume
接口,用来创建/删除一个 volume。前提是 PVC 中指定的 StorageClass 的 provisioner 字段和 CSI Controller Plugin 的GetPluginInfo
接口的返回值一致。 -
支持从快照创建数据源。如果在 PVC 中指定了 Snapshot CRD 的数据源,那么该组件会通过
SnapshotContent
对象获取有关快照的信息,并将此内容在调用CreateVolume
接口的时候传给 CSI driver,CSI driver 需要根据数据源快照来创建 volume。
external-attacher
监听 VolumeAttachment 对象,并调用 CSI Controller Plugin 服务的 ControllerPublishVolume
/ ControllerUnpublishVolume
接口,将 volume attach 到 node 上,或从 node 上删除。
external-resizer
监听 PVC 对象,如果用户请求在 PVC 对象上请求更多存储,该组件会调用 CSI Controller Plugin 服务的 NodeExpandVolume
接口,用来对 volume 进行扩容。
external-snapshotter
该组件需要与 Snapshot Controller 配合使用。Snapshot Controller 会根据集群中创建的 Snapshot 对象创建对应的 VolumeSnapshotContent,而 external-snapshotter 负责监听 VolumeSnapshotContent 对象。当监听到 VolumeSnapshotContent 时,将其对应参数通过 CreateSnapshotRequest
传给 CSI Controller Plugin,调用其 CreateSnapshot
接口。该组件还负责调用 DeleteSnapshot
、ListSnapshots
接口。
external-health-monitor-controller
调用 CSI Controller Plugin 的 ListVolumes
或者 ControllerGetVolume
接口,来检查 CSI volume 的健康情况,并上报在 PVC 的 event 中。
livenessprobe
调用 CSI Controller Plugin/CSI Node Plugin 的 Probe
接口获取它们的健康情况,并通过 Liveness Probe 机制汇报给 kubelet,那么当监测到有异常时重启 pod。
node-driver-registrar
调用 CSI Node Plugin 的 NodeGetInfo
接口,将 CSI Node Plugin 的信息通过 kubelet 的插件注册机制在对应节点的 kubelet 上进行注册。
external-health-monitor-agent
调用 CSI Node Plugin 的 NodeGetVolumeStats
接口,来检查 CSI volume 的健康情况,并上报在 pod 的 event 中。
CSI 的 RPC 接口
CSI RPC 接口文档:https://github.com/container-storage-interface/spec/blob/master/spec.md
上文在介绍流程的时候,已经提到了被调用的 RPC 接口情况,这里对 CSI 的 RPC 接口进行一个总结概述。
CSI 的 RPC 接口主要分为三类:
- CSI Identity
- CSI Controller
- CSI Node
在实现 out-of-tree 类型的 CSI 插件时,我们需要实现的组件主要会有 2 个:CSI Controller Plugin 和 CSI Node Plugin。这两个组件需要共同实现上述的 RPC 接口。其中
- CSI Controller Plugin 主要实现 CSI Identity 和 CSI Controller 两类接口。
- CSI Node Plugin 主要实现 CSI Identity 和 CSI Node 两类接口。
CSI Identity
用于提供 CSI Controller/Node Plugin 的身份信息,CSI Controller/Node Plugin 都需要实现。接口如下:
GetPluginInfo
是必须要实现的,node-driver-registrar 组件会调用这个接口将 CSI 插件注册到 kubelet。GetPluginCapabilities
是用来表明这个 CSI 插件提供了哪些功能。
service Identity {
rpc GetPluginInfo(GetPluginInfoRequest)
returns (GetPluginInfoResponse) {}
rpc GetPluginCapabilities(GetPluginCapabilitiesRequest)
returns (GetPluginCapabilitiesResponse) {}
rpc Probe (ProbeRequest)
returns (ProbeResponse) {}
}
CSI Controller
用于实现创建/删除 volume、attach/detach volume、volume 快照、volume 扩缩容等功能。CSI Controller Plugin 需要实现这组接口。接口如下:
CreateVolume
/DeleteVolume
接口配合 external-provisioner 实现创建/删除 volume 的功能。ControllerPublishVolume
/ControllerUnpublishVolume
接口配合 external-attacher 实现 volume 的 attach/detach 功能。
service Controller {
rpc CreateVolume (CreateVolumeRequest)
returns (CreateVolumeResponse) {}
rpc DeleteVolume (DeleteVolumeRequest)
returns (DeleteVolumeResponse) {}
rpc ControllerPublishVolume (ControllerPublishVolumeRequest)
returns (ControllerPublishVolumeResponse) {}
rpc ControllerUnpublishVolume (ControllerUnpublishVolumeRequest)
returns (ControllerUnpublishVolumeResponse) {}
rpc ValidateVolumeCapabilities (ValidateVolumeCapabilitiesRequest)
returns (ValidateVolumeCapabilitiesResponse) {}
rpc ListVolumes (ListVolumesRequest)
returns (ListVolumesResponse) {}
rpc GetCapacity (GetCapacityRequest)
returns (GetCapacityResponse) {}
rpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest)
returns (ControllerGetCapabilitiesResponse) {}
rpc CreateSnapshot (CreateSnapshotRequest)
returns (CreateSnapshotResponse) {}
rpc DeleteSnapshot (DeleteSnapshotRequest)
returns (DeleteSnapshotResponse) {}
rpc ListSnapshots (ListSnapshotsRequest)
returns (ListSnapshotsResponse) {}
rpc ControllerExpandVolume (ControllerExpandVolumeRequest)
returns (ControllerExpandVolumeResponse) {}
rpc ControllerGetVolume (ControllerGetVolumeRequest)
returns (ControllerGetVolumeResponse) {
option (alpha_method) = true;
}
}
CSI Node
用于实现 mount/umount volume、检查 volume 状态等功能,CSI Node Plugin 需要实现这组接口。接口如下:
NodeStageVolume
用来实现多个 pod 共享一个 volume 的功能,支持先将 volume 挂载到一个临时目录,然后通过NodePublishVolume
将其挂载到 pod 中;NodeUnstageVolume
为其反操作。
service Node {
rpc NodeStageVolume (NodeStageVolumeRequest)
returns (NodeStageVolumeResponse) {}
rpc NodeUnstageVolume (NodeUnstageVolumeRequest)
returns (NodeUnstageVolumeResponse) {}
rpc NodePublishVolume (NodePublishVolumeRequest)
returns (NodePublishVolumeResponse) {}
rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest)
returns (NodeUnpublishVolumeResponse) {}
rpc NodeGetVolumeStats (NodeGetVolumeStatsRequest)
returns (NodeGetVolumeStatsResponse) {}
rpc NodeExpandVolume(NodeExpandVolumeRequest)
returns (NodeExpandVolumeResponse) {}
rpc NodeGetCapabilities (NodeGetCapabilitiesRequest)
returns (NodeGetCapabilitiesResponse) {}
rpc NodeGetInfo (NodeGetInfoRequest)
returns (NodeGetInfoResponse) {}
}
实现讲解
「请看另一篇关于实现的讲解」。
相关链接
CSI 官方资料
CSI Github:https://github.com/kubernetes-csi
CSI Spec:https://github.com/container-storage-interface/spec
CSI Doc:https://kubernetes-csi.github.io/docs/introduction.html
学习链接
https://blog.hdls.me/16255765577465.html
https://www.cnblogs.com/rexcheny/p/10925464.html
https://ata.atatech.org/articles/11000169496?spm=ata.25287382.0.0.6b8d7536pdhiP7