如何接入 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-csi/external-provisioner】

首先,通過標准的 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-csi/external-attacher】

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 性能優化實踐、最佳實踐等系列。

【騰訊雲原生】雲說新品、雲研新術、雲游新活、雲賞資訊,掃碼關注同名公眾號,及時獲取更多干貨!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM