如何接入 K8s 持久化存储?K8s CSI 实现机制浅析

作者

王成,腾讯云研发工程师,Kubernetes contributor,从事数据库产品容器化、资源管控等工作,关注 Kubernetes、Go、云原生领域。

概述

进入 K8s 的世界,会发现有很多方便扩展的 Interface,包括 CSI, CNI, CRI 等,将这些接口抽象出来,是为了更好的提供开放、扩展、规范等能力。

K8s 持久化存储经历了从 in-tree Volume 到 CSI Plugin(out-of-tree) 的迁移,一方面是为了将 K8s 核心主干代码与 Volume 相关代码解耦,便于更好的维护;另一方面则是为了方便各大云厂商实现统一的接口,提供个性化的云存储能力,以期达到云存储生态圈的开放共赢。

本文将从持久卷 PV 的 创建(Create)、附着(Attach)、分离(Detach)、挂载(Mount)、卸载(Unmount)、删除(Delete) 等核心生命周期,对 CSI 实现机制进行了解析。

相关术语

TermDefinition
CSIContainer Storage Interface.
CNIContainer Network Interface.
CRIContainer Runtime Interface.
PVPersistent Volume.
PVCPersistent Volume Claim.
StorageClassDefined by provisioner(i.e. Storage Provider), to assemble Volume parameters as a resource object.
VolumeA unit of storage that will be made available inside of a CO-managed container, via the CSI.
Block VolumeA volume that will appear as a block device inside the container.
Mounted VolumeA volume that will be mounted using the specified file system and appear as a directory inside the container.
COContainer Orchestration system, communicates with Plugins using CSI service RPCs.
SPStorage Provider, the vendor of a CSI plugin implementation.
RPCRemote Procedure Call.
NodeA host where the user workload will be running, uniquely identifiable from the perspective of a Plugin by a node ID.
PluginAka “plugin implementation”, a gRPC endpoint that implements the CSI Services.
Plugin SupervisorProcess that governs the lifecycle of a Plugin, MAY be the CO.
WorkloadThe atomic unit of "work" scheduled by a CO. This MAY be a container or a collection of containers.

本文及后续相关文章都基于 K8s v1.22

流程概览

PV 创建核心流程:

  • apiserver 创建 Pod,根据 PodSpec.Volumes 创建 Volume;
  • PVController 监听到 PV informer,添加相关 Annotation(如 pv.kubernetes.io/provisioned-by),调谐实现 PVC/PV 的绑定(Bound);
  • 判断 StorageClass.volumeBindingModeWaitForFirstConsumer 则等待 Pod 调度到 Node 成功后再进行 PV 创建,Immediate 则立即调用 PV 创建逻辑,无需等待 Pod 调度;
  • external-provisioner 监听到 PV informer, 调用 RPC-CreateVolume 创建 Volume;
  • AttachDetachController 将已经绑定(Bound) 成功的 PVC/PV,经过 InTreeToCSITranslator 转换器,由 CSIPlugin 内部逻辑实现 VolumeAttachment 资源类型的创建;
  • external-attacher 监听到 VolumeAttachment informer,调用 RPC-ControllerPublishVolume 实现 AttachVolume;
  • kubelet reconcile 持续调谐:通过判断 controllerAttachDetachEnabled || PluginIsAttachable 及当前 Volume 状态进行 AttachVolume/MountVolume,最终实现将 Volume 挂载到 Pod 指定目录中,供 Container 使用;

从 CSI 说起

CSI(Container Storage Interface) 是由来自 Kubernetes、Mesos、Docker 等社区 member 联合制定的一个行业标准接口规范(https://github.com/container-storage-interface/spec),旨在将任意存储系统暴露给容器化应用程序。

CSI 规范定义了存储提供商实现 CSI 兼容的 Volume Plugin 的最小操作集和部署建议。CSI 规范的主要焦点是声明 Volume Plugin 必须实现的接口。

先看一下 Volume 的生命周期:

1 CreateVolume +------------+ DeleteVolume 2 +------------->| CREATED +--------------+ 3 | +---+----^---+ | 4 | Controller | | Controller v 5+++ Publish | | Unpublish +++ 6|X| Volume | | Volume | | 7+-+ +---v----+---+ +-+ 8 | NODE_READY | 9 +---+----^---+ 10 Node | | Node 11 Stage | | Unstage 12 Volume | | Volume 13 +---v----+---+ 14 | VOL_READY | 15 +---+----^---+ 16 Node | | Node 17 Publish | | Unpublish 18 Volume | | Volume 19 +---v----+---+ 20 | PUBLISHED | 21 +------------+ 22 23The lifecycle of a dynamically provisioned volume, from 24creation to destruction, when the Node Plugin advertises the 25STAGE_UNSTAGE_VOLUME capability.

从 Volume 生命周期可以看到,一块持久卷要达到 Pod 可使用状态,需要经历以下阶段:

CreateVolume -> ControllerPublishVolume -> NodeStageVolume -> NodePublishVolume

而当删除 Volume 的时候,会经过如下反向阶段:

NodeUnpublishVolume -> NodeUnstageVolume -> ControllerUnpublishVolume -> DeleteVolume

上面流程的每个步骤,其实就对应了 CSI 提供的标准接口,云存储厂商只需要按标准接口实现自己的云存储插件,即可与 K8s 底层编排系统无缝衔接起来,提供多样化的云存储、备份、快照(snapshot)等能力。

多组件协同

为实现具有高扩展性、out-of-tree 的持久卷管理能力,在 K8s CSI 实现中,相关协同的组件有:

组件介绍

  • kube-controller-manager:K8s 资源控制器,主要通过 PVController, AttachDetach 实现持久卷的绑定(Bound)/解绑(Unbound)、附着(Attach)/分离(Detach);
  • CSI-plugin:K8s 独立拆分出来,实现 CSI 标准规范接口的逻辑控制与调用,是整个 CSI 控制逻辑的核心枢纽;
  • node-driver-registrar:是一个由官方 K8s sig 小组维护的辅助容器(sidecar),它使用 kubelet 插件注册机制向 kubelet 注册插件,需要请求 CSI 插件的 Identity 服务来获取插件信息;
  • external-provisioner:是一个由官方 K8s sig 小组维护的辅助容器(sidecar),主要功能是实现持久卷的创建(Create)、删除(Delete);
  • external-attacher:是一个由官方 K8s sig 小组维护的辅助容器(sidecar),主要功能是实现持久卷的附着(Attach)、分离(Detach);
  • external-snapshotter:是一个由官方 K8s sig 小组维护的辅助容器(sidecar),主要功能是实现持久卷的快照(VolumeSnapshot)、备份恢复等能力;
  • external-resizer:是一个由官方 K8s sig 小组维护的辅助容器(sidecar),主要功能是实现持久卷的弹性扩缩容,需要云厂商插件提供相应的能力;
  • kubelet:K8s 中运行在每个 Node 上的控制枢纽,主要功能是调谐节点上 Pod 与 Volume 的附着、挂载、监控探测上报等;
  • cloud-storage-provider:由各大云存储厂商基于 CSI 标准接口实现的插件,包括 Identity 身份服务、Controller 控制器服务、Node 节点服务;

组件通信

由于 CSI plugin 的代码在 K8s 中被认为是不可信的,因此 CSI Controller Server 和 External CSI SideCar、CSI Node Server 和 Kubelet 通过 Unix Socket 来通信,与云存储厂商提供的 Storage Service 通过 gRPC(HTTP/2) 通信:

RPC 调用

从 CSI 标准规范可以看到,云存储厂商想要无缝接入 K8s 容器编排系统,需要按规范实现相关接口,相关接口主要为:

  • Identity 身份服务:Node Plugin 和 Controller Plugin 都必须实现这些 RPC 集,协调 K8s 与 CSI 的版本信息,负责对外暴露这个插件的信息。
  • Controller 控制器服务:Controller Plugin 必须实现这些 RPC 集,创建以及管理 Volume,对应 K8s 中 attach/detach volume 操作。
  • Node 节点服务:Node Plugin 必须实现这些 RPC 集,将 Volume 存储卷挂载到指定目录中,对应 K8s 中的 mount/unmount volume 操作。

相关 RPC 接口功能如下:

创建/删除 PV

K8s 中持久卷 PV 的创建(Create)与删除(Delete),由 external-provisioner 组件实现,相关工程代码在:【https://github.com/kubernetes-csi/external-provisioner】

首先,通过标准的 cmd 方式获取命令行参数,执行 newController -> Run() 逻辑,相关代码如下:

1// external-provisioner/cmd/csi-provisioner/csi-provisioner.go 2main() { 3... 4 // 初始化控制器,实现 Volume 创建/删除接口 5 csiProvisioner := ctrl.NewCSIProvisioner( 6 clientset, 7 *operationTimeout, 8 identity, 9 *volumeNamePrefix, 10 *volumeNameUUIDLength, 11 grpcClient, 12 snapClient, 13 provisionerName, 14 pluginCapabilities, 15 controllerCapabilities, 16 ... 17 ) 18 ... 19 // 真正的 ProvisionController,包装了上面的 CSIProvisioner 20 provisionController = controller.NewProvisionController( 21 clientset, 22 provisionerName, 23 csiProvisioner, 24 provisionerOptions..., 25 ) 26 ... 27 run := func(ctx context.Context) { 28 ... 29 // Run 运行起来 30 provisionController.Run(ctx) 31 } 32}

接着,调用 PV 创建/删除流程:

PV 创建:runClaimWorker -> syncClaimHandler -> syncClaim -> provisionClaimOperation -> Provision -> CreateVolume PV 删除:runVolumeWorker -> syncVolumeHandler -> syncVolume -> deleteVolumeOperation -> Delete -> DeleteVolume

由 sigs.k8s.io/sig-storage-lib-external-provisioner 抽象了相关接口:

1// 通过 vendor 方式引入 sigs.k8s.io/sig-storage-lib-external-provisioner 2// external-provisioner/vendor/sigs.k8s.io/sig-storage-lib-external-provisioner/v7/controller/volume.go 3type Provisioner interface { 4 // 调用 PRC CreateVolume 接口实现 PV 创建 5 Provision(context.Context, ProvisionOptions) (*v1.PersistentVolume, ProvisioningState, error) 6 // 调用 PRC DeleteVolume 接口实现 PV 删除 7 Delete(context.Context, *v1.PersistentVolume) error 8}

Controller 调谐

K8s 中与 PV 相关的控制器有 PVController、AttachDetachController。

PVController

PVController 通过在 PVC 添加相关 Annotation(如 pv.kubernetes.io/provisioned-by),由 external-provisioner 组件负责完成对应 PV 的创建/删除,然后 PVController 监测到 PV 创建成功的状态,完成与 PVC 的绑定(Bound),调谐(reconcile)任务完成。然后交给 AttachDetachController 控制器进行下一步逻辑处理。

值得一提的是,PVController 内部通过使用 local cache,高效实现了 PVC 与 PV 的状态更新与绑定事件处理,相当于在 K8s informer 机制之外,又自己维护了一个 local store 进行 Add/Update/Delete 事件处理。

首先,通过标准的 newController -> Run() 逻辑:

1// kubernetes/pkg/controller/volume/persistentvolume/pv_controller_base.go 2func NewController(p ControllerParameters) (*PersistentVolumeController, error) { 3 ... 4 // 初始化 PVController 5 controller := &PersistentVolumeController{ 6 volumes: newPersistentVolumeOrderedIndex(), 7 claims: cache.NewStore(cache.DeletionHandlingMetaNamespaceKeyFunc), 8 kubeClient: p.KubeClient, 9 eventRecorder: eventRecorder, 10 runningOperations: goroutinemap.NewGoRoutineMap(true /* exponentialBackOffOnError */), 11 cloud: p.Cloud, 12 enableDynamicProvisioning: p.EnableDynamicProvisioning, 13 clusterName: p.ClusterName, 14 createProvisionedPVRetryCount: createProvisionedPVRetryCount, 15 createProvisionedPVInterval: createProvisionedPVInterval, 16 claimQueue: workqueue.NewNamed("claims"), 17 volumeQueue: workqueue.NewNamed("volumes"), 18 resyncPeriod: p.SyncPeriod, 19 operationTimestamps: metrics.NewOperationStartTimeCache(), 20 } 21 ... 22 // PV 增删改事件监听 23 p.VolumeInformer.Informer().AddEventHandler( 24 cache.ResourceEventHandlerFuncs{ 25 AddFunc: func(obj interface{}) { controller.enqueueWork(controller.volumeQueue, obj) }, 26 UpdateFunc: func(oldObj, newObj interface{}) { controller.enqueueWork(controller.volumeQueue, newObj) }, 27 DeleteFunc: func(obj interface{}) { controller.enqueueWork(controller.volumeQueue, obj) }, 28 }, 29 ) 30 ... 31 // PVC 增删改事件监听 32 p.ClaimInformer.Informer().AddEventHandler( 33 cache.ResourceEventHandlerFuncs{ 34 AddFunc: func(obj interface{}) { controller.enqueueWork(controller.claimQueue, obj) }, 35 UpdateFunc: func(oldObj, newObj interface{}) { controller.enqueueWork(controller.claimQueue, newObj) }, 36 DeleteFunc: func(obj interface{}) { controller.enqueueWork(controller.claimQueue, obj) }, 37 }, 38 ) 39 ... 40 return controller, nil 41}

接着,调用 PVC/PV 绑定/解绑逻辑:

PVC/PV 绑定:claimWorker -> updateClaim -> syncClaim -> syncBoundClaim -> bind PVC/PV 解绑:volumeWorker -> updateVolume -> syncVolume -> unbindVolume

AttachDetachController

AttachDetachController 将已经绑定(Bound) 成功的 PVC/PV,内部经过 InTreeToCSITranslator 转换器,实现由 in-tree 方式管理的 Volume 向 out-of-tree 方式管理的 CSI 插件模式转换。

接着,由 CSIPlugin 内部逻辑实现 VolumeAttachment 资源类型的创建/删除,调谐(reconcile) 任务完成。然后交给 external-attacher 组件进行下一步逻辑处理。

相关核心代码在 reconciler.Run() 中实现如下:

1// kubernetes/pkg/controller/volume/attachdetach/reconciler/reconciler.go 2func (rc *reconciler) reconcile() { 3 4 // 先进行 DetachVolume,确保因 Pod 重新调度到其他节点的 Volume 提前分离(Detach) 5 for _, attachedVolume := range rc.actualStateOfWorld.GetAttachedVolumes() { 6 // 如果不在期望状态的 Volume,则调用 DetachVolume 删除 VolumeAttachment 资源对象 7 if !rc.desiredStateOfWorld.VolumeExists( 8 attachedVolume.VolumeName, attachedVolume.NodeName) { 9 ... 10 err = rc.attacherDetacher.DetachVolume(attachedVolume.AttachedVolume, verifySafeToDetach, rc.actualStateOfWorld) 11 ... 12 } 13 } 14 // 调用 AttachVolume 创建 VolumeAttachment 资源对象 15 rc.attachDesiredVolumes() 16 ... 17}

附着/分离 Volume

K8s 中持久卷 PV 的附着(Attach)与分离(Detach),由 external-attacher 组件实现,相关工程代码在:【https://github.com/kubernetes-csi/external-attacher】

external-attacher 组件观察到由上一步 AttachDetachController 创建的 VolumeAttachment 对象,如果其 .spec.Attacher 中的 Driver name 指定的是自己同一 Pod 内的 CSI Plugin,则调用 CSI Plugin 的ControllerPublish 接口进行 Volume Attach。

首先,通过标准的 cmd 方式获取命令行参数,执行 newController -> Run() 逻辑,相关代码如下:

1// external-attacher/cmd/csi-attacher/main.go 2func main() { 3 ... 4 ctrl := controller.NewCSIAttachController( 5 clientset, 6 csiAttacher, 7 handler, 8 factory.Storage().V1().VolumeAttachments(), 9 factory.Core().V1().PersistentVolumes(), 10 workqueue.NewItemExponentialFailureRateLimiter(*retryIntervalStart, *retryIntervalMax), 11 workqueue.NewItemExponentialFailureRateLimiter(*retryIntervalStart, *retryIntervalMax), 12 supportsListVolumesPublishedNodes, 13 *reconcileSync, 14 ) 15 16 run := func(ctx context.Context) { 17 stopCh := ctx.Done() 18 factory.Start(stopCh) 19 ctrl.Run(int(*workerThreads), stopCh) 20 } 21 ... 22}

接着,调用 Volume 附着/分离逻辑:

Volume 附着(Attach):syncVA -> SyncNewOrUpdatedVolumeAttachment -> syncAttach -> csiAttach -> Attach -> ControllerPublishVolume Volume 分离(Detach):syncVA -> SyncNewOrUpdatedVolumeAttachment -> syncDetach -> csiDetach -> Detach -> ControllerUnpublishVolume

kubelet 挂载/卸载 Volume

K8s 中持久卷 PV 的挂载(Mount)与卸载(Unmount),由 kubelet 组件实现。

kubelet 通过 VolumeManager 启动 reconcile loop,当观察到有新的使用 PersistentVolumeSource 为CSI 的 PV 的 Pod 调度到本节点上,于是调用 reconcile 函数进行 Attach/Detach/Mount/Unmount 相关逻辑处理。

1// kubernetes/pkg/kubelet/volumemanager/reconciler/reconciler.go 2func (rc *reconciler) reconcile() { 3 // 先进行 UnmountVolume,确保因 Pod 删除被重新 Attach 到其他 Pod 的 Volume 提前卸载(Unmount) 4 rc.unmountVolumes() 5 6 // 接着通过判断 controllerAttachDetachEnabled || PluginIsAttachable 及当前 Volume 状态 7 // 进行 AttachVolume / MountVolume / ExpandInUseVolume 8 rc.mountAttachVolumes() 9 10 // 卸载(Unmount) 或分离(Detach) 不再需要(Pod 删除)的 Volume 11 rc.unmountDetachDevices() 12}

相关调用逻辑如下:

Volume 挂载(Mount):reconcile -> mountAttachVolumes -> MountVolume -> SetUp -> SetUpAt -> NodePublishVolume Volume 卸载(Unmount):reconcile -> unmountVolumes -> UnmountVolume -> TearDown -> TearDownAt -> NodeUnpublishVolume

小结

本文通过分析 K8s 中持久卷 PV 的 创建(Create)、附着(Attach)、分离(Detach)、挂载(Mount)、卸载(Unmount)、删除(Delete) 等核心生命周期流程,对 CSI 实现机制进行了解析,通过源码、图文方式说明了相关流程逻辑,以期更好的理解 K8s CSI 运行流程。

可以看到,K8s 以 CSI Plugin(out-of-tree) 插件方式开放存储能力,一方面是为了将 K8s 核心主干代码与 Volume 相关代码解耦,便于更好的维护;另一方面在遵从 CSI 规范接口下,便于各大云厂商根据业务需求实现相关的接口,提供个性化的云存储能力,以期达到云存储生态圈的开放共赢。

PS: 更多内容请关注 k8s-club

相关资料

  1. CSI 规范
  2. Kubernetes 源码
  3. kubernetes-csi 源码
  4. kubernetes-sig-storage 源码
  5. K8s CSI 概念
  6. K8s CSI 介绍

关于我们

更多关于云原生的案例和知识,可关注同名【腾讯云原生】公众号~

福利:

1 ①公众号后台回复【手册】,可获得《腾讯云原生路线图手册》&《腾讯云原生最佳实践》~ 2 3 ②公众号后台回复【系列】,可获得《15个系列100+篇超实用云原生原创干货合集》,包含Kubernetes 降本增效、K8s 性能优化实践、最佳实践等系列。
点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

KVM调整cpu和内存

一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid