乐趣区

关于腾讯云:如何接入-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 实现机制进行了解析。

相干术语

Term Definition
CSI Container Storage Interface.
CNI Container Network Interface.
CRI Container Runtime Interface.
PV Persistent Volume.
PVC Persistent Volume Claim.
StorageClass Defined by provisioner(i.e. Storage Provider), to assemble Volume parameters as a resource object.
Volume A unit of storage that will be made available inside of a CO-managed container, via the CSI.
Block Volume A volume that will appear as a block device inside the container.
Mounted Volume A volume that will be mounted using the specified file system and appear as a directory inside the container.
CO Container Orchestration system, communicates with Plugins using CSI service RPCs.
SP Storage Provider, the vendor of a CSI plugin implementation.
RPC Remote Procedure Call.
Node A host where the user workload will be running, uniquely identifiable from the perspective of a Plugin by a node ID.
Plugin Aka“plugin implementation”, a gRPC endpoint that implements the CSI Services.
Plugin Supervisor Process that governs the lifecycle of a Plugin, MAY be the CO.
Workload The 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 的生命周期:

   CreateVolume +------------+ DeleteVolume
 +------------->|  CREATED   +--------------+
 |              +---+----^---+              |
 |       Controller |    | Controller       v
+++         Publish |    | Unpublish       +++
|X|          Volume |    | Volume          | |
+-+             +---v----+---+             +-+
                | NODE_READY |
                +---+----^---+
               Node |    | Node
              Stage |    | Unstage
             Volume |    | Volume
                +---v----+---+
                |  VOL_READY |
                +---+----^---+
               Node |    | Node
            Publish |    | Unpublish
             Volume |    | Volume
                +---v----+---+
                | PUBLISHED  |
                +------------+

The lifecycle of a dynamically provisioned volume, from
creation to destruction, when the Node Plugin advertises the
STAGE_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…】

首先,通过规范的 cmd 形式获取命令行参数,执行 newController -> Run() 逻辑,相干代码如下:

// external-provisioner/cmd/csi-provisioner/csi-provisioner.go
main() {
...
    // 初始化控制器,实现 Volume 创立 / 删除接口
    csiProvisioner := ctrl.NewCSIProvisioner(
        clientset,
        *operationTimeout,
        identity,
        *volumeNamePrefix,
        *volumeNameUUIDLength,
        grpcClient,
        snapClient,
        provisionerName,
        pluginCapabilities,
        controllerCapabilities,
        ...
    )
    ...
    // 真正的 ProvisionController,包装了下面的 CSIProvisioner
    provisionController = controller.NewProvisionController(
        clientset,
        provisionerName,
        csiProvisioner,
        provisionerOptions...,
    )
    ...
    run := func(ctx context.Context) {
        ...
        // Run 运行起来
        provisionController.Run(ctx)
    }
}

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

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

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

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

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() 逻辑:

// kubernetes/pkg/controller/volume/persistentvolume/pv_controller_base.go
func NewController(p ControllerParameters) (*PersistentVolumeController, error) {
    ...
    // 初始化 PVController
    controller := &PersistentVolumeController{volumes:                       newPersistentVolumeOrderedIndex(),
        claims:                        cache.NewStore(cache.DeletionHandlingMetaNamespaceKeyFunc),
        kubeClient:                    p.KubeClient,
        eventRecorder:                 eventRecorder,
        runningOperations:             goroutinemap.NewGoRoutineMap(true /* exponentialBackOffOnError */),
        cloud:                         p.Cloud,
        enableDynamicProvisioning:     p.EnableDynamicProvisioning,
        clusterName:                   p.ClusterName,
        createProvisionedPVRetryCount: createProvisionedPVRetryCount,
        createProvisionedPVInterval:   createProvisionedPVInterval,
        claimQueue:                    workqueue.NewNamed("claims"),
        volumeQueue:                   workqueue.NewNamed("volumes"),
        resyncPeriod:                  p.SyncPeriod,
        operationTimestamps:           metrics.NewOperationStartTimeCache(),}
    ...
    // PV 增删改事件监听
    p.VolumeInformer.Informer().AddEventHandler(
        cache.ResourceEventHandlerFuncs{AddFunc:    func(obj interface{}) {controller.enqueueWork(controller.volumeQueue, obj) },
            UpdateFunc: func(oldObj, newObj interface{}) {controller.enqueueWork(controller.volumeQueue, newObj) },
            DeleteFunc: func(obj interface{}) {controller.enqueueWork(controller.volumeQueue, obj) },
        },
    )
    ...
    // PVC 增删改事件监听
    p.ClaimInformer.Informer().AddEventHandler(
        cache.ResourceEventHandlerFuncs{AddFunc:    func(obj interface{}) {controller.enqueueWork(controller.claimQueue, obj) },
            UpdateFunc: func(oldObj, newObj interface{}) {controller.enqueueWork(controller.claimQueue, newObj) },
            DeleteFunc: func(obj interface{}) {controller.enqueueWork(controller.claimQueue, obj) },
        },
    )
    ...
    return controller, nil
}

接着,调用 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() 中实现如下:

// kubernetes/pkg/controller/volume/attachdetach/reconciler/reconciler.go
func (rc *reconciler) reconcile() {// 先进行 DetachVolume,确保因 Pod 从新调度到其余节点的 Volume 提前拆散(Detach)
    for _, attachedVolume := range rc.actualStateOfWorld.GetAttachedVolumes() {
        // 如果不在冀望状态的 Volume,则调用 DetachVolume 删除 VolumeAttachment 资源对象
        if !rc.desiredStateOfWorld.VolumeExists(attachedVolume.VolumeName, attachedVolume.NodeName) {
            ...
            err = rc.attacherDetacher.DetachVolume(attachedVolume.AttachedVolume, verifySafeToDetach, rc.actualStateOfWorld)
            ...
        }
    }
    // 调用 AttachVolume 创立 VolumeAttachment 资源对象
    rc.attachDesiredVolumes()
    ...
}

附着 / 拆散 Volume

K8s 中长久卷 PV 的附着 (Attach) 与拆散(Detach),由 external-attacher 组件实现,相干工程代码在:【https://github.com/kubernetes…】

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

首先,通过规范的 cmd 形式获取命令行参数,执行 newController -> Run() 逻辑,相干代码如下:

// external-attacher/cmd/csi-attacher/main.go
func main() {
    ...
    ctrl := controller.NewCSIAttachController(
        clientset,
        csiAttacher,
        handler,
        factory.Storage().V1().VolumeAttachments(),
        factory.Core().V1().PersistentVolumes(),
        workqueue.NewItemExponentialFailureRateLimiter(*retryIntervalStart, *retryIntervalMax),
        workqueue.NewItemExponentialFailureRateLimiter(*retryIntervalStart, *retryIntervalMax),
        supportsListVolumesPublishedNodes,
        *reconcileSync,
    )

    run := func(ctx context.Context) {stopCh := ctx.Done()
        factory.Start(stopCh)
        ctrl.Run(int(*workerThreads), stopCh)
    }
    ...
}

接着,调用 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 相干逻辑解决。

// kubernetes/pkg/kubelet/volumemanager/reconciler/reconciler.go
func (rc *reconciler) reconcile() {// 先进行 UnmountVolume,确保因 Pod 删除被从新 Attach 到其余 Pod 的 Volume 提前卸载(Unmount)
    rc.unmountVolumes()

    // 接着通过判断 controllerAttachDetachEnabled || PluginIsAttachable 及以后 Volume 状态
    // 进行 AttachVolume / MountVolume / ExpandInUseVolume
    rc.mountAttachVolumes()

    // 卸载 (Unmount) 或拆散(Detach) 不再须要(Pod 删除) 的 Volume
    rc.unmountDetachDevices()}

相干调用逻辑如下:

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 介绍

对于咱们

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

福利:

   ①公众号后盾回复【手册】,可取得《腾讯云原生路线图手册》&《腾讯云原生最佳实际》~

   ②公众号后盾回复【系列】,可取得《15 个系列 100+ 篇超实用云原生原创干货合集》,蕴含 Kubernetes 降本增效、K8s 性能优化实际、最佳实际等系列。

【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!

退出移动版