CSI学习笔记-01: 基础知识
文章目录
1. CSI Spec
在探究如何在 Kubernetes 上开发 CSI 插件之前,我们需要先简单了解 CSI Spec 的相关内容。
CSI 全称 Container Storage Interface 是从 Kubernetes 项目中孵化出的一套行业标准接口,借助 CSI 容器编排引擎 CO( Kubernetes 是其中一种)可以将任意存储系统暴露给自己的容器工作负载并实现对存储卷的生命周期管理。
在日常交流中 CSI 虽然总是被默认为是 Kubernetes 存储管理的一部分,但实际上它是一个通用性的规范,包括 Cloud Foundry、Mesos、Nomad 等其他容器编排引擎同样支持 CSI。
CSI/Spec 项目下我们可以查到 CSI 规范的详细内容,以及不同容器平台的实现,其中 Kubernetes 相关实现在 Kubernetes CSI。
CSI 规范最核心的内容是:约定 CO 和 CSI 通过 RPC 进行交互,并且定义了 Identity、Controller、Node 三个 RPC 服务。
根据实现 RPC 服务的不同,CSI 将插件分成 Controller Plugin 和 Node Plugin 两类,用户可以根据实际情况将 Controller Server 和 Node Server 运行在不同进程或者同一个进程中,但每个进程都需要实现 Identity 服务,用来告知 CO 本进程具有哪些能力。
规范文档中详细定义了每一个 RPC 接口的参数、返回值以及错误码,以下是几个关键接口:
Controller 服务相关接口:
- CreateVolume:CO 调用该接口开始创建一个卷,该接口在 Volume 生命周期最开始调用。
- ControllerPublishVolume:CO 将工作负载调度到指定节点时,将调用该接口。需要注意,该方法不保证调度节点和执行节点一致。
Node 服务相关接口:
- NodeStageVolume:CO 在 Volume 被任意工作负载使用前调用该接口,并且保证调度节点和执行节点一致。
- NodePublishVolume:CO 在 NodeStageVolume 方法返回后紧接着调用方法,并且同样保证调度节点和执行节点一致。
CSI 规定了接口的请求参数、返回值、错误码、调用顺序以及执行节点,但并没有规定对应的处理逻辑,开发者可以根据实际情况进行设计。
通常 Volume 完整生命周期中,CO 和 CSI 的交互情况如下图:
CSI 规范并不要求实现所有 RPC 接口,所以可能出现以下简化的交互。
需要注意:CSI 对 RPC 调用时参数的大小进行了限制,用户可以通过在所述字段的描述中指定不同的大小限制来覆盖默认值,默认情况下:string 类型不超过128 bytes,map<string, string> 不超过 4 KiB
2. Kubernetes CSI
Kubernetes 包含 In-Tree/Out-Of-Tree 形式的存储插件,其中 In-Tree 是在 Kubernetes 源码内部实现并和 Kubernetes 一起发布、管理(如 AWS EBS)。 Out-Of-Tree 插件是独立于 Kubernetes 的,主要包括 FlexVolume 和 CSI 两种实现方式,其中 FlexVolume 是v1.8版本的新功能,并且在v1.23版本中将会被废弃。 考虑到 FlexVolume 插件在设计模式上的缺陷,当前 CSI 是绝对主流方案。
在 Kubernetes 中 CSI 主要的交互对象是 kubelet 和 apiserver。
用户需要在分别实现 CSIControllerService 和 CSINodeService 。前者通过 Deployment/StatefulSet 方式部署,负责和 apiserver 交互。后者通过 Daemonset 部署,负责和 kubelet 交互。
-
CSIControllerService:执行 provision/delete 和 attach/detach
-
CSINodeService:执行 mount/unmount
1. Side Car
我们知道 CSI 是围绕 RPC 接口定义的,但通常情况 kubernetes 体系中与 apiserver 交互是围绕 watch object 机制进行的,为了屏蔽 CSI 插件对 Apiserver 的感知, Kubernetes 做了更进一步的封装。
官方为开发者提供了一系列边车容器,这些容器和 CSIControllerService 在同一个 Pod 中,负责监听特定 Object 的变化,并在适当时候调用 CSIControllerService 的接口。
官方提供的边车容器括以下几种:
上述边车容器中,最重要的是 external-provisioner 和 external-attacher。
external-provisioner 容器负责监听 PVC 创建/删除,并调用 CreateVolume()/DeleteVolume() 接口,当接口返回时创建/删除 PV。该容器还遵循 Volume Topology Specification,实现创建 Volume 和 Node 之间的亲和性关系。
external-attacher 容器监听 VolumeAttachment Object ,调用 ControllerPublishVolume/ControllerUnPublishVolume 接口进行 Attach/Detach 操作。
2. Kubelet
Kubelet 和 CSI 的交互同样通过 gRPC 进行,CSI 需要在物理机地址 /var/lib/kubelet/plugins/[SanitizedCSIDriverName]/csi.sock 中创建 Socket ,kubelet 通过 Device Plugin 机制注册 CSI。
CSI 通过官方提供的 node-driver-registrar 容器在 Kubelet 上完成注册,整个过程如下:
- Kubelet 监听 CSIDriver Object,获取 CSIControllerService 相关配置;
- 调用 CSINodeInfo 接口,获取 CSINodeService 相关配置;
- Create/Update CSINode Object;
- 更新 Node Object ,添加 csi.volume.kubernetes.io/nodeid 注解;
- 更新 Node Object 的相关 Labels;
卷拓扑感知
1. Kubernetes 的设计
Kubernetes 为了让 CSI 或者树内插件能够更便利的实现卷 Topologies, 引入了以下三个属性:
- PV 的 VolumeNodeAffinity 属性
- StorageClass 的 VolumeBindingMode 属性
- StorageClass 的 AllowedTopologies 属性
参考:https://github.com/kubernetes/design-proposals-archive/blob/main/storage/volume-topology-scheduling.md
1. VolumeNodeAffinity
PV 的 VolumeNodeAffinity 属性是 Kubernetes 引入的用来约束 node 和 PV 位置的属性,该属性在以下场景中起作用:
- 当 PVC 和 PV 已经被绑定,并且 PV 带有 VolumeNodeAffinity 属性,那么使用 PVC 的 Pod 只能被调度到 VolumeNodeAffinity 限制的节点;
- 当 Kubelet 执行卷 mounting 时会对 VolumeNodeAffinity 进行验证,即 PVC/PV 绑定时限定了特定节点的 Kubelet 调用 NodePublishVolume/NodeUnPublishVolume 接口
|
|
2. WaitForFirstConsumer
在 Kubernetes 最初的设计中,provisioning 和 binding 卷的操作均是在 Pod 被调度到指定节点前完成的。 由于卷的 PV 是具有位置属性的 PVC 和 PV 完成 binding 后,使用该 PVC 的 Pod 只能运行在 PV 所在节点,kube-scheduler 无法在根据节点的 CPU 、内存等因素对 Pod 做出调度。
为了解决上述情况,Kubernetes 为 StorageClass 提供了 VolumeBindingMode 配置。该配置可以指定为 Immediate 和 WaitForFirstConsumer 配置,前者保持 Kubernetes 的原有绑定和调度逻辑,后者将 PVC 和 PV 的 Binding 操作滞后。
StorageClass 的 VolumeBindingMode 属性为 WaitForFirstConsumer 时,csi-provisioner 不会立即为 PVC 创建对应的 PV 并进行绑定。
CSI Plugin 在 PVC 初次被 Pod 使用时不会有任何操作。当 Pod 首次需要绑定 PVC 时,kube-scheduler 优先对 Pod 进行调度,并将调度结果通过 volume.kubernetes.io/selected-node 注解记录在 PVC 上。 当 external-provisioner 容器 watch PVC 存在 volume.kubernetes.io/selected-node 注解时,开始执行 Provision 逻辑。
PS:CSI Plugin 是否开始处理 PVC 的逻辑在 sig-storage-lib-external-provisioner 库的 ProvisionController.shouldProvision() 函数
3. Restricting Topology
用户可以通过 StorageClass 的 AllowedTopologies 参数进行卷位置拓扑的限制,官方设计文档提到该能力主要在以下场景中使用到:
- StorageClass 设置为 Immediate 时,这种情况下用户可以通过 Restricting Topology 限制 Volume 绑定的位置,即使 PVC 没有被任何 Pod 使用;
- 存储后端存在不同的多个 Zone 时,通过 AllowedTopologies 进行划分;
Restricting Topology 描述并不是太清晰,建议读者自己阅读设计文档!
2. CSI Topologies
从上面的描述中,可以做出以下总结:** Kubernetes 中卷拓扑最终是通过 PV 的 VolumeNodeAffinity 限制的,而 StorageClass 中的属性作为存储插件生成 VolumeNodeAffinity 的依据**。
CSI 中 external-provisioner 负责创建 PV,同时负责生成对应的 VolumeNodeAffinity 属性,因此 CSI Volume 的 Topologies 实现主要在该服务中完成。
external-provisioner 创建 CreateVolumeRequest 对象,CSI-ControllerServer 需要根据请求中的 AccessibilityRequirements 的内容构造 CreateVolumeRespone 返回中的 AccessibleTopology 信息。 external-provisioner 根据返回的信息创建 PV 并设置对应的 NodeAffinity。
external-provisioner 在生成 AccessibilityRequirements 参数时主要参考以下依据:
- PVC 是否有 volume.kubernetes.io/selected-node 注解
- CSINode 对象中的 topologyKeys 参数
- StorageClass 是否配置了 AllowedTopologies,并且 external-provisioner 需要开启 strict-topology 参数时 AllowedTopologies 配置才会生效
从上述设计中可以知道,除了 Kubernetes 原有设计,CSI 规范本身同样设计了 Topologies 机制,体现在 NodeServer 的 NodeGetInfoRespone 接口上。
external-provisioner 中如何生成 AccessibilityRequirements ,可以参考对应源码 pkg/controller/topology.go 中的 GenerateAccessibilityRequirements 函数
4. CSI 开发
CSI 本质上是向接口中填写对应的 Storage 逻辑,开发者参考以下 CSI-Plugin 工程模板:
开发过程中使用的依赖库:
-
kubernetes-sigs/sig-storage-lib-external-provisioner:底层依赖库,rancher/local-path-provisioner 和 external-provisioner 的底层依赖
-
kubernetes-csi/csi-lib-utils: 封装了 leaderelection,rpc,metrics,protosanitizer 等常用接口
参考
官方文档
相关博客
其他资料
文章作者 yoaz
上次更新 2022-03-22
许可协议 MIT