做者 | 惠志
来源 | 阿里巴巴云原生公众号html
导读:在《一文读懂 K8s 持久化存储流程》一文咱们重点介绍了 K8s 内部的存储流程,以及 PV、PVC、StorageClass、Kubelet 等之间的调用关系。接下来本文将将重点放在 CSI(Container Storage Interface)容器存储接口上,探究什么是 CSI 及其内部工做原理。node
K8s 原生支持一些存储类型的 PV,如 iSCSI、NFS、CephFS 等等(详见连接),这些 in-tree 类型的存储代码放在 Kubernetes 代码仓库中。这里带来的问题是 K8s 代码与三方存储厂商的代码强耦合:nginx
CSI 容器存储接口标准的出现解决了上述问题,将三方存储代码与 K8s 代码解耦,使得三方存储厂商研发人员只需实现 CSI 接口(无需关注容器平台是 K8s 仍是 Swarm 等)。算法
在详细介绍 CSI 组件及其接口以前,咱们先对 K8s 中 CSI 存储流程进行一个介绍。《一文读懂 K8s 持久化存储流程》一文介绍了 K8s 中的 Pod 在挂载存储卷时需经历三个的阶段:Provision/Delete(创盘/删盘)、Attach/Detach(挂接/摘除)和 Mount/Unmount(挂载/卸载),下面以图文的方式讲解 K8s 在这三个阶段使用 CSI 的流程。api
1.集群管理员建立 StorageClass 资源,该 StorageClass 中包含 CSI 插件名称(provisioner:pangu.csi.alibabacloud.com)以及存储类必须的参数(parameters: type=cloud_ssd)。sc.yaml 文件以下:安全
2.用户建立 PersistentVolumeClaim 资源,PVC 指定存储大小及 StorageClass(如上)。pvc.yaml 文件以下:app
3.卷控制器(PersistentVolumeController)观察到集群中新建立的 PVC 没有与之匹配的 PV,且其使用的存储类型为 out-of-tree,因而为 PVC 打 annotation:volume.beta.kubernetes.io/storage-provisioner=[out-of-tree CSI 插件名称](本例中即为 provisioner:pangu.csi.alibabacloud.com)。dom
4.External Provisioner 组件观察到 PVC 的 annotation 中包含 "volume.beta.kubernetes.io/storage-provisioner" 且其 value 是本身,因而开始创盘流程。socket
5.外部 CSI 插件返回成功后表示盘建立完成,此时External Provisioner 组件会在集群建立一个 PersistentVolume 资源。ide
6.卷控制器会将 PV 与 PVC 进行绑定。
1.AD 控制器(AttachDetachController)观察到使用 CSI 类型 PV 的 Pod 被调度到某一节点,此时AD 控制器会调用内部 in-tree CSI 插件(csiAttacher)的 Attach 函数。
2.内部 in-tree CSI 插件(csiAttacher)会建立一个 VolumeAttachment 对象到集群中。
3.External Attacher 观察到该 VolumeAttachment 对象,并调用外部 CSI插件的ControllerPublish 函数以将卷挂接到对应节点上。外部 CSI 插件挂载成功后,External Attacher会更新相关 VolumeAttachment 对象的 .Status.Attached 为 true。
4.AD 控制器内部 in-tree CSI 插件(csiAttacher)观察到 VolumeAttachment 对象的 .Status.Attached 设置为 true,因而更新AD 控制器内部状态(ActualStateOfWorld),该状态会显示在 Node 资源的 .Status.VolumesAttached 上。
1.Volume Manager(Kubelet 组件)观察到有新的使用 CSI 类型 PV 的 Pod 调度到本节点上,因而调用内部 in-tree CSI 插件(csiAttacher)的 WaitForAttach 函数。
2.内部 in-tree CSI 插件(csiAttacher)等待集群中 VolumeAttachment 对象状态 .Status.Attached 变为 true。
3.in-tree CSI 插件(csiAttacher)调用 MountDevice 函数,该函数内部经过 unix domain socket 调用外部 CSI 插件的NodeStageVolume 函数;以后插件(csiAttacher)调用内部 in-tree CSI 插件(csiMountMgr)的 SetUp 函数,该函数内部会经过 unix domain socket 调用外部 CSI 插件的NodePublishVolume 函数。
1.用户删除相关 Pod。
2.Volume Manager(Kubelet 组件)观察到包含 CSI 存储卷的 Pod 被删除,因而调用内部 in-tree CSI 插件(csiMountMgr)的 TearDown 函数,该函数内部会经过 unix domain socket 调用外部 CSI 插件的 NodeUnpublishVolume 函数。
3.Volume Manager(Kubelet 组件)调用内部 in-tree CSI 插件(csiAttacher)的 UnmountDevice 函数,该函数内部会经过 unix domain socket 调用外部 CSI 插件的 NodeUnpublishVolume 函数。
1.AD 控制器观察到包含 CSI 存储卷的 Pod 被删除,此时该控制器会调用内部 in-tree CSI 插件(csiAttacher)的 Detach 函数。
2.csiAttacher会删除集群中相关 VolumeAttachment 对象(但因为存在 finalizer,va 对象不会当即删除)。
3.External Attacher观察到集群中 VolumeAttachment 对象的 DeletionTimestamp 非空,因而调用外部 CSI 插件的ControllerUnpublish 函数以将卷从对应节点上摘除。外部 CSI 插件摘除成功后,External Attacher会移除相关 VolumeAttachment 对象的 finalizer 字段,此时 VolumeAttachment 对象被完全删除。
4.AD 控制器中内部 in-tree CSI 插件(csiAttacher)观察到 VolumeAttachment 对象已删除,因而更新AD 控制器中的内部状态;同时AD 控制器更新 Node 资源,此时 Node 资源的 .Status.VolumesAttached 上已没有相关挂接信息。
1.用户删除相关 PVC。
2.External Provisioner 组件观察到 PVC 删除事件,根据 PVC 的回收策略(Reclaim)执行不一样操做:
为使 K8s 适配 CSI 标准,社区将与 K8s 相关的存储流程逻辑放在了 CSI Sidecar 组件中。
Node-Driver-Registrar 组件会将外部 CSI 插件注册到Kubelet,从而使Kubelet经过特定的 Unix Domain Socket 来调用外部 CSI 插件函数(Kubelet 会调用外部 CSI 插件的 NodeGetInfo、NodeStageVolume、NodePublishVolume、NodeGetVolumeStats 等函数)。
Node-Driver-Registrar 组件经过Kubelet 外部插件注册机制实现注册,注册成功后:
Kubelet为本节点 Node 资源打 annotation:Kubelet调用外部 CSI 插件的NodeGetInfo 函数,其返回值 [nodeID]、[driverName] 将做为值用于 "csi.volume.kubernetes.io/nodeid" 键。
Kubelet更新 Node Label:将NodeGetInfo 函数返回的 [AccessibleTopology] 值用于节点的 Label。
建立/删除实际的存储卷,以及表明存储卷的 PV 资源。
External-Provisioner在启动时需指定参数 -- provisioner,该参数指定 Provisioner 名称,与 StorageClass 中的 provisioner 字段对应。
External-Provisioner启动后会 watch 集群中的 PVC 和 PV 资源。
对于集群中的 PVC 资源:
判断 PVC 是否须要动态建立存储卷,标准以下:
经过特定的 Unix Domain Socket 调用外部 CSI 插件的 CreateVolume 函数。
对于集群中的 PV 资源:
判断 PV 是否须要删除,标准以下:
经过特定的 Unix Domain Socket 调用外部 CSI 插件的 DeleteVolume 接口。
挂接/摘除存储卷。
External-Attacher 内部会时刻 watch 集群中的 VolumeAttachment 资源和 PersistentVolume 资源。
对于 VolumeAttachment 资源:
从 VolumeAttachment 资源中得到 PV 的全部信息,如 volume ID、node ID、挂载 Secret 等。
对于 PersistentVolume 资源:
在挂接时为相关 PV 打上 Finalizer:external-attacher/[driver 名称]。
扩容存储卷。
External-Resizer内部会 watch 集群中的 PersistentVolumeClaim 资源。
对于 PersistentVolumeClaim 资源:
判断 PersistentVolumeClaim 资源是否须要扩容:PVC 状态须要是 Bound 且 .Status.Capacity 与 .Spec.Resources.Requests 不等。
更新 PVC 的 .Status.Conditions,代表此时处于 Resizing 状态。
经过特定的 Unix Domain Socket 调用外部 CSI 插件的 ControllerExpandVolume 接口。
更新 PV 的 .Spec.Capacity。
Volume Manager(Kubelet 组件)观察到存储卷需在线扩容,因而经过特定的 Unix Domain Socket 调用外部 CSI 插件的NodeExpandVolume 接口实现文件系统扩容。
检查 CSI 插件是否正常。
经过对外暴露一个 / healthz HTTP 端口以服务 kubelet 的探针探测器,内部是经过特定的 Unix Domain Socket 调用外部 CSI 插件的 Probe 接口。
三方存储厂商需实现 CSI 插件的三大接口:IdentityServer、ControllerServer、NodeServer。
IdentityServer 主要用于认证 CSI 插件的身份信息。
// IdentityServer is the server API for Identity service. type IdentityServer interface { // 获取CSI插件的信息,好比名称、版本号 GetPluginInfo(context.Context, *GetPluginInfoRequest) (*GetPluginInfoResponse, error) // 获取CSI插件提供的能力,好比是否提供ControllerService能力 GetPluginCapabilities(context.Context, *GetPluginCapabilitiesRequest) (*GetPluginCapabilitiesResponse, error) // 获取CSI插件健康情况 Probe(context.Context, *ProbeRequest) (*ProbeResponse, error) }
ControllerServer 主要负责存储卷及快照的建立/删除以及挂接/摘除操做。
// ControllerServer is the server API for Controller service. type ControllerServer interface { // 建立存储卷 CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error) // 删除存储卷 DeleteVolume(context.Context, *DeleteVolumeRequest) (*DeleteVolumeResponse, error) // 挂接存储卷到特定节点 ControllerPublishVolume(context.Context, *ControllerPublishVolumeRequest) (*ControllerPublishVolumeResponse, error) // 从特定节点摘除存储卷 ControllerUnpublishVolume(context.Context, *ControllerUnpublishVolumeRequest) (*ControllerUnpublishVolumeResponse, error) // 验证存储卷能力是否知足要求,好比是否支持跨节点多读多写 ValidateVolumeCapabilities(context.Context, *ValidateVolumeCapabilitiesRequest) (*ValidateVolumeCapabilitiesResponse, error) // 列举所有存储卷信息 ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error) // 获取存储资源池可用空间大小 GetCapacity(context.Context, *GetCapacityRequest) (*GetCapacityResponse, error) // 获取ControllerServer支持功能点,好比是否支持快照能力 ControllerGetCapabilities(context.Context, *ControllerGetCapabilitiesRequest) (*ControllerGetCapabilitiesResponse, error) // 建立快照 CreateSnapshot(context.Context, *CreateSnapshotRequest) (*CreateSnapshotResponse, error) // 删除快照 DeleteSnapshot(context.Context, *DeleteSnapshotRequest) (*DeleteSnapshotResponse, error) // 获取全部快照信息 ListSnapshots(context.Context, *ListSnapshotsRequest) (*ListSnapshotsResponse, error) // 扩容存储卷 ControllerExpandVolume(context.Context, *ControllerExpandVolumeRequest) (*ControllerExpandVolumeResponse, error) }
NodeServer 主要负责存储卷挂载/卸载操做。
// NodeServer is the server API for Node service. type NodeServer interface { // 将存储卷格式化并挂载至临时全局目录 NodeStageVolume(context.Context, *NodeStageVolumeRequest) (*NodeStageVolumeResponse, error) // 将存储卷从临时全局目录卸载 NodeUnstageVolume(context.Context, *NodeUnstageVolumeRequest) (*NodeUnstageVolumeResponse, error) // 将存储卷从临时目录bind-mount到目标目录 NodePublishVolume(context.Context, *NodePublishVolumeRequest) (*NodePublishVolumeResponse, error) // 将存储卷从目标目录卸载 NodeUnpublishVolume(context.Context, *NodeUnpublishVolumeRequest) (*NodeUnpublishVolumeResponse, error) // 获取存储卷的容量信息 NodeGetVolumeStats(context.Context, *NodeGetVolumeStatsRequest) (*NodeGetVolumeStatsResponse, error) // 存储卷扩容 NodeExpandVolume(context.Context, *NodeExpandVolumeRequest) (*NodeExpandVolumeResponse, error) // 获取NodeServer支持功能点,好比是否支持获取存储卷容量信息 NodeGetCapabilities(context.Context, *NodeGetCapabilitiesRequest) (*NodeGetCapabilitiesResponse, error) // 获取CSI节点信息,好比最大支持卷个数 NodeGetInfo(context.Context, *NodeGetInfoRequest) (*NodeGetInfoResponse, error) }
K8s 为支持 CSI 标准,包含以下 API 对象:
apiVersion: storage.k8s.io/v1beta1 kind: CSINode metadata: name: node-10.212.101.210 spec: drivers: - name: yodaplugin.csi.alibabacloud.com nodeID: node-10.212.101.210 topologyKeys: - kubernetes.io/hostname - name: pangu.csi.alibabacloud.com nodeID: a5441fd9013042ee8104a674e4a9666a topologyKeys: - topology.pangu.csi.alibabacloud.com/zone
做用:
判断外部 CSI 插件是否注册成功。在 Node Driver Registrar 组件向 Kubelet 注册完毕后,Kubelet 会建立该资源,故不须要显式建立 CSINode 资源。
将 Kubernetes 中 Node 资源名称与三方存储系统中节点名称(nodeID)一一对应。此处Kubelet会调用外部 CSI 插件NodeServer 的 GetNodeInfo 函数获取 nodeID。
apiVersion: storage.k8s.io/v1beta1 kind: CSIDriver metadata: name: pangu.csi.alibabacloud.com spec: # 插件是否支持卷挂接(VolumeAttach) attachRequired: true # Mount阶段是否CSI插件须要Pod信息 podInfoOnMount: true # 指定CSI支持的卷模式 volumeLifecycleModes: - Persistent
做用:
简化外部 CSI 插件的发现。由集群管理员建立,经过 kubectl get csidriver 便可得知环境上有哪些 CSI 插件。
apiVersion: storage.k8s.io/v1 kind: VolumeAttachment metadata: annotations: csi.alpha.kubernetes.io/node-id: 21481ae252a2457f9abcb86a3d02ba05 finalizers: - external-attacher/pangu-csi-alibabacloud-com name: csi-0996e5e9459e1ccc1b3a7aba07df4ef7301c8e283d99eabc1b69626b119ce750 spec: attacher: pangu.csi.alibabacloud.com nodeName: node-10.212.101.241 source: persistentVolumeName: pangu-39aa24e7-8877-11eb-b02f-021234350de1 status: attached: true
做用:VolumeAttachment 记录了存储卷的挂接/摘除信息以及节点信息。
在 StorageClass 中有 AllowedTopologies 字段:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: csi-pangu provisioner: pangu.csi.alibabacloud.com parameters: type: cloud_ssd volumeBindingMode: Immediate allowedTopologies: - matchLabelExpressions: - key: topology.pangu.csi.alibabacloud.com/zone values: - zone-1 - zone-2
外部 CSI 插件部署后会为每一个节点打标,打标内容NodeGetInfo 函数返回的 [AccessibleTopology] 值(详见 Node Driver Registrar 部分)。
External Provisioner在调用 CSI 插件的 CreateVolume 接口以前,会在请求参数设置 AccessibilityRequirements:
对于 WaitForFirstConsumer
基于社区 1.18 版本调度器
调度器的调度过程主要有以下三步:
调度器预选阶段:处理 Pod 的 PVC/PV 绑定关系以及动态供应 PV(Dynamic Provisioning),同时使调度器调度时考虑 Pod 所使用 PV 的节点亲和性。详细调度过程以下:
Pod 不包含 PVC 直接跳过。
FindPodVolumes
获取 Pod 的 boundClaims、claimsToBind 以及 unboundClaimsImmediate。
若 len(unboundClaimsImmediate) 不为空,表示这种 PVC 须要当即绑定 PV(即存 PVC 建立后,马上动态建立 PV 并将其绑定到 PVC,该过程不走调度),若 PVC 处于 unbound 阶段则报错。
若 len(boundClaims) 不为空,则检查 PVC 对应 PV 的节点亲和性与当前节点的 Label 是否冲突,若冲突则报错(可检查 Immediate 类型的 PV 拓扑)。
调度器优选阶段不讨论。
调度器 Assume 阶段
调度器会先 Assume PV/PVC,再 Assume Pod。
将当前待调度的 Pod 进行深拷贝。
AssumePodVolumes(针对 WaitForFirstConsumer 类型的 PVC)
Assume Pod 完毕
调度器 Bind 阶段
BindPodVolumes:
调用 Kubernetes 的 API 更新集群中 PV/PVC 资源,使其与调度器 Cache 中的 PV/PVC 一致。
检查 PV/PVC 状态:
存储卷扩容部分在 External Resizer 部分已提到,故再也不赘述。用户只须要编辑 PVC 的 .Spec.Resources.Requests.Storage 字段便可,注意只可扩容不可缩容。
若 PV 扩容失败,此时 PVC 没法从新编辑 spec 字段的 storage 为原来的值(只可扩容不可缩容)。参考 K8s 官网提供的 PVC 还原方法:
https://kubernetes.io/docs/concepts/storage/persistent-volumes/#recovering-from-failure-when-expanding-volumes
卷数量限制在 Node Driver Registrar 部分已提到,故再也不赘述。
存储商需实现 CSI 插件的 NodeGetVolumeStats 接口,Kubelet 会调用该函数,并反映在其 metrics上:
CSI 存储卷支持传入 Secret 来处理不一样流程中所须要的私密数据,目前 StorageClass 支持以下 Parameter:
Secret 会包含在对应 CSI 接口的参数中,如对于 CreateVolume 接口而言则包含在 CreateVolumeRequest.Secrets 中。
apiVersion: apps/v1 kind: StatefulSet metadata: name: nginx-example spec: selector: matchLabels: app: nginx serviceName: "nginx" volumeClaimTemplates: - metadata: name: html spec: accessModes: - ReadWriteOnce volumeMode: Block storageClassName: csi-pangu resources: requests: storage: 40Gi template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx volumeDevices: - devicePath: "/dev/vdb" name: html
三方存储厂商需实现 NodePublishVolume 接口。Kubernetes 提供了针对块设备的工具包("k8s.io/kubernetes/pkg/util/mount"),在 NodePublishVolume 阶段可调用该工具的 EnsureBlock 和 MountBlock 函数。
鉴于本文篇幅,此处不作过多原理性介绍。读者感兴趣见官方介绍:卷快照、卷克隆。
本文首先对 CSI 核心流程进行了大致介绍,并结合 CSI Sidecar 组件、CSI 接口、API 对象对 CSI 标准进行了深度解析。在 K8s 上,使用任何一种 CSI 存储卷都离不开上面的流程,环境上的容器存储问题也必定是其中某个环节出现了问题。本文对其流程进行梳理,以便于广大程序猿(媛)排查环境问题。
容器存储的坑比较多,专有云环境下尤为如此。不过挑战越多,机遇也越多!