作者

王成,腾讯云研发工程师,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 联合拟定的一个行业标准接口标准(github.com/container-s…),旨在将恣意存储系统暴露给容器化应用程序。

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 组件完结,相关工程代码在:【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 组件完结,相关工程代码在:【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 功能优化实践、最佳实践等系列。