K8s 持久化存儲

進入 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.
in-tree 代碼邏輯在 K8s 官方倉庫中
out-of-tree 代碼邏輯在 K8s 官方倉庫之外,實現與 K8s 代碼的解耦

組件介紹

  • PV Controller:負責 PV/PVC 綁定及周期管理,根據需求進行數據卷的 Provision/Delete 操作;
  • AD Controller:負責數據卷的 Attach/Detach 操作,將設備掛接到目標節點;
  • Kubelet:Kubelet 是在每個 Node 節點上運行的主要 “節點代理”,功能是 Pod 生命周期管理、容器健康檢查、容器監控等;
  • Volume Manager:Kubelet 中的組件,負責管理數據卷的 Mount/Umount 操作(也負責數據卷的 Attach/Detach 操作,需配置 kubelet 相關參數開啟該特性)、卷設備的格式化等等;
  • Volume Plugins:存儲插件,由存儲供應商開發,目的在于擴展各種存儲類型的卷管理能力,實現第三方存儲的各種操作能力。Volume Plugins 有 in-tree 和 out-of-tree 兩種;
  • External Provisioner:External Provioner 是一種 sidecar 容器,作用是調用 Volume Plugins 中的 CreateVolume 和 DeleteVolume 函數來執行 Provision/Delete 操作。因為 K8s 的 PV 控制器無法直接調用 -
  • Volume Plugins 的相關函數,故由 External Provioner 通過 gRPC 來調用;
  • External Attacher:External Attacher 是一種 sidecar 容器,作用是調用 Volume Plugins 中的 ControllerPublishVolume 和 ControllerUnpublishVolume 函數來執行 Attach/Detach 操作。因為 K8s 的 AD 控制器無法直接調用 Volume Plugins 的相關函數,故由 External Attacher 通過 gRPC 來調用。

K8s 持久化存儲流程

image.png

流程如下:

  • 用戶創建了一個包含 PVC 的 Pod,該 PVC 要求使用動態存儲卷;
  • apiserver 創建 Pod,根據 PodSpec.Volumes 創建 Volume
  • Scheduler 根據 Pod 配置、節點狀態、PV 配置等信息,把 Pod 調度到一個合適的 Worker 節點上;
  • PV 控制器 watch 到該 Pod 使用的 PVC 處于 Pending 狀態,于是調用 Volume Plugin(in-tree)創建存儲卷,并創建 PV 對象(out-of-tree 由 External Provisioner 來處理);
  • AD 控制器發現 Pod 和 PVC 處于待掛接狀態,于是調用 Volume Plugin 掛接存儲設備到目標 Worker 節點上
  • 在 Worker 節點上,Kubelet 中的 Volume Manager 等待存儲設備掛接完成,并通過 Volume Plugin 將設備掛載到全局目錄:/var/lib/kubelet/pods/[pod uid]/volumes/kubernetes.io~iscsi/[PV name](以 iscsi 為例);
  • Kubelet 通過 Docker 啟動 Pod 的 Containers,用 bind mount 方式將已掛載到本地全局目錄的卷映射到容器中。
    更詳細的流程如下:


    image.png

    image.png

從 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 實現中,相關協同的組件有:

image

組件介紹

  • 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) 通信:

image

RPC 調用

從 CSI 標準規范可以看到,云存儲廠商想要無縫接入 K8s 容器編排系統,需要按規范實現相關接口,相關接口主要為:

image
  • 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 接口功能如下:

image

創建/刪除 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

image

由 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

image

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

image

小結

本文通過分析 K8s 中持久卷 PV 的 創建(Create)、附著(Attach)、分離(Detach)、掛載(Mount)、卸載(Unmount)、刪除(Delete) 等核心生命周期流程,對 CSI 實現機制進行了解析,通過源碼、圖文方式說明了相關流程邏輯,以期更好的理解 K8s CSI 運行流程。

可以看到,K8s 以 CSI Plugin(out-of-tree) 插件方式開放存儲能力,一方面是為了將 K8s 核心主干代碼與 Volume 相關代碼解耦,便于更好的維護;另一方面在遵從 CSI 規范接口下,便于各大云廠商根據業務需求實現相關的接口,提供個性化的云存儲能力,以期達到云存儲生態圈的開放共贏。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容