CSI 工作原理與JuiceFS CSI Driver 的架構(gòu)設(shè)計(jì)詳解

容器存儲(chǔ)接口(Container Storage Interface)簡(jiǎn)稱 CSI,CSI 建立了行業(yè)標(biāo)準(zhǔn)接口的規(guī)范,借助 CSI 容器編排系統(tǒng)(CO)可以將任意存儲(chǔ)系統(tǒng)暴露給自己的容器工作負(fù)載。JuiceFS CSI Driver 通過(guò)實(shí)現(xiàn) CSI 接口使得 Kubernetes 上的應(yīng)用可以通過(guò) PVC(PersistentVolumeClaim)使用 JuiceFS。本文將詳細(xì)介紹 CSI 的工作原理以及 JuiceFS CSI Driver 的架構(gòu)設(shè)計(jì)。

CSI 的基本組件

CSI 的 cloud providers 有兩種類型,一種為 in-tree 類型,一種為 out-of-tree 類型。前者是指運(yùn)行在 K8s 核心組件內(nèi)部的存儲(chǔ)插件;后者是指獨(dú)立在 K8s 組件之外運(yùn)行的存儲(chǔ)插件。本文主要介紹 out-of-tree 類型的插件。

out-of-tree 類型的插件主要是通過(guò) gRPC 接口跟 K8s 組件交互,并且 K8s 提供了大量的 SideCar 組件來(lái)配合 CSI 插件實(shí)現(xiàn)豐富的功能。對(duì)于 out-of-tree 類型的插件來(lái)說(shuō),所用到的組件分為 SideCar 組件和第三方需要實(shí)現(xiàn)的插件。

SideCar 組件

external-attacher

監(jiān)聽(tīng) VolumeAttachment 對(duì)象,并調(diào)用 CSI driver Controller 服務(wù)的 ControllerPublishVolumeControllerUnpublishVolume 接口,用來(lái)將 volume 附著到 node 上,或從 node 上刪除。

如果存儲(chǔ)系統(tǒng)需要 attach/detach 這一步,就需要使用到這個(gè)組件,因?yàn)?K8s 內(nèi)部的 Attach/Detach Controller 不會(huì)直接調(diào)用 CSI driver 的接口。

external-provisioner

監(jiān)聽(tīng) PVC 對(duì)象,并調(diào)用 CSI driver Controller 服務(wù)的 CreateVolumeDeleteVolume 接口,用來(lái)提供一個(gè)新的 volume。前提是 PVC 中指定的 StorageClass 的 provisioner 字段和 CSI driver Identity 服務(wù)的 GetPluginInfo 接口的返回值一樣。一旦新的 volume 提供出來(lái),K8s 就會(huì)創(chuàng)建對(duì)應(yīng)的 PV。

而如果 PVC 綁定的 PV 的回收策略是 delete,那么 external-provisioner 組件監(jiān)聽(tīng)到 PVC 的刪除后,會(huì)調(diào)用 CSI driver Controller 服務(wù)的 DeleteVolume 接口。一旦 volume 刪除成功,該組件也會(huì)刪除相應(yīng)的 PV。

該組件還支持從快照創(chuàng)建數(shù)據(jù)源。如果在 PVC 中指定了 Snapshot CRD 的數(shù)據(jù)源,那么該組件會(huì)通過(guò) SnapshotContent 對(duì)象獲取有關(guān)快照的信息,并將此內(nèi)容在調(diào)用 CreateVolume 接口的時(shí)候傳給 CSI driver,CSI driver 需要根據(jù)數(shù)據(jù)源快照來(lái)創(chuàng)建 volume。

external-resizer

監(jiān)聽(tīng) PVC 對(duì)象,如果用戶請(qǐng)求在 PVC 對(duì)象上請(qǐng)求更多存儲(chǔ),該組件會(huì)調(diào)用 CSI driver Controller 服務(wù)的 NodeExpandVolume 接口,用來(lái)對(duì) volume 進(jìn)行擴(kuò)容。

external-snapshotter

該組件需要與 Snapshot Controller 配合使用。Snapshot Controller 會(huì)根據(jù)集群中創(chuàng)建的 Snapshot 對(duì)象創(chuàng)建對(duì)應(yīng)的 VolumeSnapshotContent,而 external-snapshotter 負(fù)責(zé)監(jiān)聽(tīng) VolumeSnapshotContent 對(duì)象。當(dāng)監(jiān)聽(tīng)到 VolumeSnapshotContent 時(shí),將其對(duì)應(yīng)參數(shù)通過(guò) CreateSnapshotRequest 傳給 CSI driver Controller 服務(wù),調(diào)用其 CreateSnapshot 接口。該組件還負(fù)責(zé)調(diào)用 DeleteSnapshotListSnapshots 接口。

livenessprobe

負(fù)責(zé)監(jiān)測(cè) CSI driver 的健康情況,并通過(guò) Liveness Probe 機(jī)制匯報(bào)給 K8s,當(dāng)監(jiān)測(cè)到 CSI driver 有異常時(shí)負(fù)責(zé)重啟 pod。

node-driver-registrar

通過(guò)直接調(diào)用 CSI driver Node 服務(wù)的 NodeGetInfo 接口,將 CSI driver 的信息通過(guò) kubelet 的插件注冊(cè)機(jī)制在對(duì)應(yīng)節(jié)點(diǎn)的 kubelet 上進(jìn)行注冊(cè)。

external-health-monitor-controller

通過(guò)調(diào)用 CSI driver Controller 服務(wù)的 ListVolumes 或者 ControllerGetVolume 接口,來(lái)檢查 CSI volume 的健康情況,并上報(bào)在 PVC 的 event 中。

external-health-monitor-agent

通過(guò)調(diào)用 CSI driver Node 服務(wù)的 NodeGetVolumeStats 接口,來(lái)檢查 CSI volume 的健康情況,并上報(bào)在 pod 的 event 中。

第三方插件

第三方存儲(chǔ)提供方(即 SP,Storage Provider)需要實(shí)現(xiàn) Controller 和 Node 兩個(gè)插件,其中 Controller 負(fù)責(zé) Volume 的管理,以 StatefulSet 形式部署;Node 負(fù)責(zé)將 Volume mount 到 pod 中,以 DaemonSet 形式部署在每個(gè) node 中。

CSI 插件與 kubelet 以及 K8s 外部組件是通過(guò) Unix Domani Socket gRPC 來(lái)進(jìn)行交互調(diào)用的。CSI 定義了三套 RPC 接口,SP 需要實(shí)現(xiàn)這三組接口,以便與 K8s 外部組件進(jìn)行通信。三組接口分別是:CSI Identity、CSI Controller 和 CSI Node,下面詳細(xì)看看這些接口定義。

CSI Identity

用于提供 CSI driver 的身份信息,Controller 和 Node 都需要實(shí)現(xiàn)。接口如下:

service Identity {
  rpc GetPluginInfo(GetPluginInfoRequest)
    returns (GetPluginInfoResponse) {}

  rpc GetPluginCapabilities(GetPluginCapabilitiesRequest)
    returns (GetPluginCapabilitiesResponse) {}

  rpc Probe (ProbeRequest)
    returns (ProbeResponse) {}
}

GetPluginInfo 是必須要實(shí)現(xiàn)的,node-driver-registrar 組件會(huì)調(diào)用這個(gè)接口將 CSI driver 注冊(cè)到 kubelet;GetPluginCapabilities 是用來(lái)表明該 CSI driver 主要提供了哪些功能。

CSI Controller

用于實(shí)現(xiàn)創(chuàng)建/刪除 volume、attach/detach volume、volume 快照、volume 擴(kuò)縮容等功能,Controller 插件需要實(shí)現(xiàn)這組接口。接口如下:

service Controller {
  rpc CreateVolume (CreateVolumeRequest)
    returns (CreateVolumeResponse) {}

  rpc DeleteVolume (DeleteVolumeRequest)
    returns (DeleteVolumeResponse) {}

  rpc ControllerPublishVolume (ControllerPublishVolumeRequest)
    returns (ControllerPublishVolumeResponse) {}

  rpc ControllerUnpublishVolume (ControllerUnpublishVolumeRequest)
    returns (ControllerUnpublishVolumeResponse) {}

  rpc ValidateVolumeCapabilities (ValidateVolumeCapabilitiesRequest)
    returns (ValidateVolumeCapabilitiesResponse) {}

  rpc ListVolumes (ListVolumesRequest)
    returns (ListVolumesResponse) {}

  rpc GetCapacity (GetCapacityRequest)
    returns (GetCapacityResponse) {}

  rpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest)
    returns (ControllerGetCapabilitiesResponse) {}

  rpc CreateSnapshot (CreateSnapshotRequest)
    returns (CreateSnapshotResponse) {}

  rpc DeleteSnapshot (DeleteSnapshotRequest)
    returns (DeleteSnapshotResponse) {}

  rpc ListSnapshots (ListSnapshotsRequest)
    returns (ListSnapshotsResponse) {}

  rpc ControllerExpandVolume (ControllerExpandVolumeRequest)
    returns (ControllerExpandVolumeResponse) {}

  rpc ControllerGetVolume (ControllerGetVolumeRequest)
    returns (ControllerGetVolumeResponse) {
        option (alpha_method) = true;
    }
}

在上面介紹 K8s 外部組件的時(shí)候已經(jīng)提到,不同的接口分別提供給不同的組件調(diào)用,用于配合實(shí)現(xiàn)不同的功能。比如 CreateVolume/DeleteVolume 配合 external-provisioner 實(shí)現(xiàn)創(chuàng)建/刪除 volume 的功能;ControllerPublishVolume/ControllerUnpublishVolume 配合 external-attacher 實(shí)現(xiàn) volume 的 attach/detach 功能等。

CSI Node

用于實(shí)現(xiàn) mount/umount volume、檢查 volume 狀態(tài)等功能,Node 插件需要實(shí)現(xiàn)這組接口。接口如下:

service Node {
  rpc NodeStageVolume (NodeStageVolumeRequest)
    returns (NodeStageVolumeResponse) {}

  rpc NodeUnstageVolume (NodeUnstageVolumeRequest)
    returns (NodeUnstageVolumeResponse) {}

  rpc NodePublishVolume (NodePublishVolumeRequest)
    returns (NodePublishVolumeResponse) {}

  rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest)
    returns (NodeUnpublishVolumeResponse) {}

  rpc NodeGetVolumeStats (NodeGetVolumeStatsRequest)
    returns (NodeGetVolumeStatsResponse) {}

  rpc NodeExpandVolume(NodeExpandVolumeRequest)
    returns (NodeExpandVolumeResponse) {}

  rpc NodeGetCapabilities (NodeGetCapabilitiesRequest)
    returns (NodeGetCapabilitiesResponse) {}

  rpc NodeGetInfo (NodeGetInfoRequest)
    returns (NodeGetInfoResponse) {}
}

NodeStageVolume 用來(lái)實(shí)現(xiàn)多個(gè) pod 共享一個(gè) volume 的功能,支持先將 volume 掛載到一個(gè)臨時(shí)目錄,然后通過(guò) NodePublishVolume 將其掛載到 pod 中;NodeUnstageVolume 為其反操作。

工作流程

下面來(lái)看看 pod 掛載 volume 的整個(gè)工作流程。整個(gè)流程流程分別三個(gè)階段:Provision/Delete、Attach/Detach、Mount/Unmount,不過(guò)不是每個(gè)存儲(chǔ)方案都會(huì)經(jīng)歷這三個(gè)階段,比如 NFS 就沒(méi)有 Attach/Detach 階段。

整個(gè)過(guò)程不僅僅涉及到上面介紹的組件的工作,還涉及 ControllerManager 的 AttachDetachController 組件和 PVController 組件以及 kubelet。下面分別詳細(xì)分析一下 Provision、Attach、Mount 三個(gè)階段。

Provision

image

先來(lái)看 Provision 階段,整個(gè)過(guò)程如上圖所示。其中 extenal-provisioner 和 PVController 均 watch PVC 資源。

  1. 當(dāng) PVController watch 到集群中有 PVC 創(chuàng)建時(shí),會(huì)判斷當(dāng)前是否有 in-tree plugin 與之相符,如果沒(méi)有則判斷其存儲(chǔ)類型為 out-of-tree 類型,于是給 PVC 打上注解 volume.beta.kubernetes.io/storage-provisioner={csi driver name}
  2. 當(dāng) extenal-provisioner watch 到 PVC 的注解 csi driver 與自己的 csi driver 一致時(shí),調(diào)用 CSI Controller 的 CreateVolume 接口;
  3. 當(dāng) CSI Controller 的 CreateVolume 接口返回成功時(shí),extenal-provisioner 會(huì)在集群中創(chuàng)建對(duì)應(yīng)的 PV;
  4. PVController watch 到集群中有 PV 創(chuàng)建時(shí),將 PV 與 PVC 進(jìn)行綁定。

Attach

image

Attach 階段是指將 volume 附著到節(jié)點(diǎn)上,整個(gè)過(guò)程如上圖所示。

  1. ADController 監(jiān)聽(tīng)到 pod 被調(diào)度到某節(jié)點(diǎn),并且使用的是 CSI 類型的 PV,會(huì)調(diào)用內(nèi)部的 in-tree CSI 插件的接口,該接口會(huì)在集群中創(chuàng)建一個(gè) VolumeAttachment 資源;
  2. external-attacher 組件 watch 到有 VolumeAttachment 資源創(chuàng)建出來(lái)時(shí),會(huì)調(diào)用 CSI Controller 的 ControllerPublishVolume 接口;
  3. 當(dāng) CSI Controller 的 ControllerPublishVolume 接口調(diào)用成功后,external-attacher 將對(duì)應(yīng)的 VolumeAttachment 對(duì)象的 Attached 狀態(tài)設(shè)為 true;
  4. ADController watch 到 VolumeAttachment 對(duì)象的 Attached 狀態(tài)為 true 時(shí),更新 ADController 內(nèi)部的狀態(tài) ActualStateOfWorld。

Mount

image

最后一步將 volume 掛載到 pod 里的過(guò)程涉及到 kubelet。整個(gè)流程簡(jiǎn)單地說(shuō)是,對(duì)應(yīng)節(jié)點(diǎn)上的 kubelet 在創(chuàng)建 pod 的過(guò)程中,會(huì)調(diào)用 CSI Node 插件,執(zhí)行 mount 操作。下面再針對(duì) kubelet 內(nèi)部的組件細(xì)分進(jìn)行分析。

首先 kubelet 創(chuàng)建 pod 的主函數(shù) syncPod 中,kubelet 會(huì)調(diào)用其子組件 volumeManager 的 WaitForAttachAndMount 方法,等待 volume mount 完成:

func (kl *Kubelet) syncPod(o syncPodOptions) error {
...
    // Volume manager will not mount volumes for terminated pods
    if !kl.podIsTerminated(pod) {
        // Wait for volumes to attach/mount
        if err := kl.volumeManager.WaitForAttachAndMount(pod); err != nil {
            kl.recorder.Eventf(pod, v1.EventTypeWarning, events.FailedMountVolume, "Unable to attach or mount volumes: %v", err)
            klog.Errorf("Unable to attach or mount volumes for pod %q: %v; skipping pod", format.Pod(pod), err)
            return err
        }
    }
...
}

volumeManager 中包含兩個(gè)組件:desiredStateOfWorldPopulator 和 reconciler。這兩個(gè)組件相互配合就完成了 volume 在 pod 中的 mount 和 umount 過(guò)程。整個(gè)過(guò)程如下:

image

desiredStateOfWorldPopulator 和 reconciler 的協(xié)同模式是生產(chǎn)者和消費(fèi)者的模式。volumeManager 中維護(hù)了兩個(gè)隊(duì)列(嚴(yán)格來(lái)講是 interface,但這里充當(dāng)了隊(duì)列的作用),即 DesiredStateOfWorld 和 ActualStateOfWorld,前者維護(hù)的是當(dāng)前節(jié)點(diǎn)中 volume 的期望狀態(tài);后者維護(hù)的是當(dāng)前節(jié)點(diǎn)中 volume 的實(shí)際狀態(tài)。

而 desiredStateOfWorldPopulator 在自己的循環(huán)中只做了兩個(gè)事情,一個(gè)是從 kubelet 的 podManager 中獲取當(dāng)前節(jié)點(diǎn)新建的 Pod,將其需要掛載的 volume 信息記錄到 DesiredStateOfWorld 中;另一件事是從 podManager 中獲取當(dāng)前節(jié)點(diǎn)中被刪除的 pod,檢查其 volume 是否在 ActualStateOfWorld 的記錄中,如果沒(méi)有,將其在 DesiredStateOfWorld 中也刪除,從而保證 DesiredStateOfWorld 記錄的是節(jié)點(diǎn)中所有 volume 的期望狀態(tài)。相關(guān)代碼如下(為了精簡(jiǎn)邏輯,刪除了部分代碼):

// Iterate through all pods and add to desired state of world if they don't
// exist but should
func (dswp *desiredStateOfWorldPopulator) findAndAddNewPods() {
    // Map unique pod name to outer volume name to MountedVolume.
    mountedVolumesForPod := make(map[volumetypes.UniquePodName]map[string]cache.MountedVolume)
    ...
    processedVolumesForFSResize := sets.NewString()
    for _, pod := range dswp.podManager.GetPods() {
        dswp.processPodVolumes(pod, mountedVolumesForPod, processedVolumesForFSResize)
    }
}

// processPodVolumes processes the volumes in the given pod and adds them to the
// desired state of the world.
func (dswp *desiredStateOfWorldPopulator) processPodVolumes(
    pod *v1.Pod,
    mountedVolumesForPod map[volumetypes.UniquePodName]map[string]cache.MountedVolume,
    processedVolumesForFSResize sets.String) {
    uniquePodName := util.GetUniquePodName(pod)
    ...
    for _, podVolume := range pod.Spec.Volumes {   
        pvc, volumeSpec, volumeGidValue, err :=
            dswp.createVolumeSpec(podVolume, pod, mounts, devices)

        // Add volume to desired state of world
        _, err = dswp.desiredStateOfWorld.AddPodToVolume(
            uniquePodName, pod, volumeSpec, podVolume.Name, volumeGidValue)
        dswp.actualStateOfWorld.MarkRemountRequired(uniquePodName)
    }
}

而 reconciler 就是消費(fèi)者,它主要做了三件事:

  1. unmountVolumes():在 ActualStateOfWorld 中遍歷 volume,判斷其是否在 DesiredStateOfWorld 中,如果不在,則調(diào)用 CSI Node 的接口執(zhí)行 unmount,并在 ActualStateOfWorld 中記錄;
  2. mountAttachVolumes():從 DesiredStateOfWorld 中獲取需要被 mount 的 volume,調(diào)用 CSI Node 的接口執(zhí)行 mount 或擴(kuò)容,并在 ActualStateOfWorld 中做記錄;
  3. unmountDetachDevices(): 在 ActualStateOfWorld 中遍歷 volume,若其已經(jīng) attach,但沒(méi)有使用的 pod,并在 DesiredStateOfWorld 也沒(méi)有記錄,則將其 unmount/detach 掉。

我們以 mountAttachVolumes() 為例,看看其如何調(diào)用 CSI Node 的接口。

func (rc *reconciler) mountAttachVolumes() {
    // Ensure volumes that should be attached/mounted are attached/mounted.
    for _, volumeToMount := range rc.desiredStateOfWorld.GetVolumesToMount() {
        volMounted, devicePath, err := rc.actualStateOfWorld.PodExistsInVolume(volumeToMount.PodName, volumeToMount.VolumeName)
        volumeToMount.DevicePath = devicePath
        if cache.IsVolumeNotAttachedError(err) {
            ...
        } else if !volMounted || cache.IsRemountRequiredError(err) {
            // Volume is not mounted, or is already mounted, but requires remounting
            err := rc.operationExecutor.MountVolume(
                rc.waitForAttachTimeout,
                volumeToMount.VolumeToMount,
                rc.actualStateOfWorld,
                isRemount)
            ...
        } else if cache.IsFSResizeRequiredError(err) {
            err := rc.operationExecutor.ExpandInUseVolume(
                volumeToMount.VolumeToMount,
                rc.actualStateOfWorld)
            ...
        }
    }
}

執(zhí)行 mount 的操作全在 rc.operationExecutor 中完成,再看 operationExecutor 的代碼:

func (oe *operationExecutor) MountVolume(
    waitForAttachTimeout time.Duration,
    volumeToMount VolumeToMount,
    actualStateOfWorld ActualStateOfWorldMounterUpdater,
    isRemount bool) error {
    ...
    var generatedOperations volumetypes.GeneratedOperations
        generatedOperations = oe.operationGenerator.GenerateMountVolumeFunc(
            waitForAttachTimeout, volumeToMount, actualStateOfWorld, isRemount)

    // Avoid executing mount/map from multiple pods referencing the
    // same volume in parallel
    podName := nestedpendingoperations.EmptyUniquePodName

    return oe.pendingOperations.Run(
        volumeToMount.VolumeName, podName, "" /* nodeName */, generatedOperations)
}

該函數(shù)先構(gòu)造執(zhí)行函數(shù),再執(zhí)行,那么再看構(gòu)造函數(shù):

func (og *operationGenerator) GenerateMountVolumeFunc(
    waitForAttachTimeout time.Duration,
    volumeToMount VolumeToMount,
    actualStateOfWorld ActualStateOfWorldMounterUpdater,
    isRemount bool) volumetypes.GeneratedOperations {

    volumePlugin, err :=
        og.volumePluginMgr.FindPluginBySpec(volumeToMount.VolumeSpec)

    mountVolumeFunc := func() volumetypes.OperationContext {
        // Get mounter plugin
        volumePlugin, err := og.volumePluginMgr.FindPluginBySpec(volumeToMount.VolumeSpec)
        volumeMounter, newMounterErr := volumePlugin.NewMounter(
            volumeToMount.VolumeSpec,
            volumeToMount.Pod,
            volume.VolumeOptions{})
        ...
        // Execute mount
        mountErr := volumeMounter.SetUp(volume.MounterArgs{
            FsUser:              util.FsUserFrom(volumeToMount.Pod),
            FsGroup:             fsGroup,
            DesiredSize:         volumeToMount.DesiredSizeLimit,
            FSGroupChangePolicy: fsGroupChangePolicy,
        })
        // Update actual state of world
        markOpts := MarkVolumeOpts{
            PodName:             volumeToMount.PodName,
            PodUID:              volumeToMount.Pod.UID,
            VolumeName:          volumeToMount.VolumeName,
            Mounter:             volumeMounter,
            OuterVolumeSpecName: volumeToMount.OuterVolumeSpecName,
            VolumeGidVolume:     volumeToMount.VolumeGidValue,
            VolumeSpec:          volumeToMount.VolumeSpec,
            VolumeMountState:    VolumeMounted,
        }

        markVolMountedErr := actualStateOfWorld.MarkVolumeAsMounted(markOpts)
        ...
        return volumetypes.NewOperationContext(nil, nil, migrated)
    }

    return volumetypes.GeneratedOperations{
        OperationName:     "volume_mount",
        OperationFunc:     mountVolumeFunc,
        EventRecorderFunc: eventRecorderFunc,
        CompleteFunc:      util.OperationCompleteHook(util.GetFullQualifiedPluginNameForVolume(volumePluginName, volumeToMount.VolumeSpec), "volume_mount"),
    }
}

這里先去注冊(cè)到 kubelet 的 CSI 的 plugin 列表中找到對(duì)應(yīng)的插件,然后再執(zhí)行 volumeMounter.SetUp,最后更新 ActualStateOfWorld 的記錄。這里負(fù)責(zé)執(zhí)行 external CSI 插件的是 csiMountMgr,代碼如下:

func (c *csiMountMgr) SetUp(mounterArgs volume.MounterArgs) error {
    return c.SetUpAt(c.GetPath(), mounterArgs)
}

func (c *csiMountMgr) SetUpAt(dir string, mounterArgs volume.MounterArgs) error {
    csi, err := c.csiClientGetter.Get()
    ...

    err = csi.NodePublishVolume(
        ctx,
        volumeHandle,
        readOnly,
        deviceMountPath,
        dir,
        accessMode,
        publishContext,
        volAttribs,
        nodePublishSecrets,
        fsType,
        mountOptions,
    )
    ...
    return nil
}

可以看到,在 kubelet 中調(diào)用 CSI Node NodePublishVolume/NodeUnPublishVolume 接口的是 volumeManager 的 csiMountMgr。至此,整個(gè) Pod 的 volume 流程就已經(jīng)梳理清楚了。

JuiceFS CSI Driver 工作原理

接下來(lái)再來(lái)看看 JuiceFS CSI Driver 的工作原理。架構(gòu)圖如下:

55.png

JuiceFS 在 CSI Node 接口 NodePublishVolume 中創(chuàng)建 pod,用來(lái)執(zhí)行 juicefs mount xxx,從而保證 juicefs 客戶端運(yùn)行在 pod 里。如果有多個(gè)的業(yè)務(wù) pod 共用一份存儲(chǔ),mount pod 會(huì)在 annotation 進(jìn)行引用計(jì)數(shù),確保不會(huì)重復(fù)創(chuàng)建。具體的代碼如下(為了方便閱讀,省去了日志等無(wú)關(guān)代碼):

func (p *PodMount) JMount(jfsSetting *jfsConfig.JfsSetting) error {
    if err := p.createOrAddRef(jfsSetting); err != nil {
        return err
    }
    return p.waitUtilPodReady(GenerateNameByVolumeId(jfsSetting.VolumeId))
}

func (p *PodMount) createOrAddRef(jfsSetting *jfsConfig.JfsSetting) error {
    ...
    
    for i := 0; i < 120; i++ {
        // wait for old pod deleted
        oldPod, err := p.K8sClient.GetPod(podName, jfsConfig.Namespace)
        if err == nil && oldPod.DeletionTimestamp != nil {
            time.Sleep(time.Millisecond * 500)
            continue
        } else if err != nil {
            if K8serrors.IsNotFound(err) {
                newPod := r.NewMountPod(podName)
                if newPod.Annotations == nil {
                    newPod.Annotations = make(map[string]string)
                }
                newPod.Annotations[key] = jfsSetting.TargetPath
                po, err := p.K8sClient.CreatePod(newPod)
                ...
                return err
            }
            return err
        }
      ...
        return p.AddRefOfMount(jfsSetting.TargetPath, podName)
    }
    return status.Errorf(codes.Internal, "Mount %v failed: mount pod %s has been deleting for 1 min", jfsSetting.VolumeId, podName)
}

func (p *PodMount) waitUtilPodReady(podName string) error {
    // Wait until the mount pod is ready
    for i := 0; i < 60; i++ {
        pod, err := p.K8sClient.GetPod(podName, jfsConfig.Namespace)
        ...
        if util.IsPodReady(pod) {
            return nil
        }
        time.Sleep(time.Millisecond * 500)
    }
    ...
    return status.Errorf(codes.Internal, "waitUtilPodReady: mount pod %s isn't ready in 30 seconds: %v", podName, log)
}

每當(dāng)有業(yè)務(wù) pod 退出時(shí),CSI Node 會(huì)在接口 NodeUnpublishVolume 刪除 mount pod annotation 中對(duì)應(yīng)的計(jì)數(shù),當(dāng)最后一個(gè)記錄被刪除時(shí),mount pod 才會(huì)被刪除。具體代碼如下(為了方便閱讀,省去了日志等無(wú)關(guān)代碼):

func (p *PodMount) JUmount(volumeId, target string) error {
   ...
    err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
        po, err := p.K8sClient.GetPod(pod.Name, pod.Namespace)
        if err != nil {
            return err
        }
        annotation := po.Annotations
        ...
        delete(annotation, key)
        po.Annotations = annotation
        return p.K8sClient.UpdatePod(po)
    })
    ...

    deleteMountPod := func(podName, namespace string) error {
        return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
            po, err := p.K8sClient.GetPod(podName, namespace)
            ...
            shouldDelay, err = util.ShouldDelay(po, p.K8sClient)
            if err != nil {
                return err
            }
            if !shouldDelay {
                // do not set delay delete, delete it now
                if err := p.K8sClient.DeletePod(po); err != nil {
                    return err
                }
            }
            return nil
        })
    }

    newPod, err := p.K8sClient.GetPod(pod.Name, pod.Namespace)
    ...
    if HasRef(newPod) {
        return nil
    }
    return deleteMountPod(pod.Name, pod.Namespace)
}

CSI Driver 與 juicefs 客戶端解耦,做升級(jí)不會(huì)影響到業(yè)務(wù)容器;將客戶端獨(dú)立在 pod 中運(yùn)行也就使其在 K8s 的管控內(nèi),可觀測(cè)性更強(qiáng);同時(shí) pod 的好處我們也能享受到,比如隔離性更強(qiáng),可以單獨(dú)設(shè)置客戶端的資源配額等。

總結(jié)

本文從 CSI 的組件、CSI 接口、volume 如何掛載到 pod 上,三個(gè)方面入手,分析了 CSI 整個(gè)體系工作的過(guò)程,并介紹了 JuiceFS CSI Driver 的工作原理。CSI 是整個(gè)容器生態(tài)的標(biāo)準(zhǔn)存儲(chǔ)接口,CO 通過(guò) gRPC 方式和 CSI 插件通信,而為了做到普適,K8s 設(shè)計(jì)了很多外部組件來(lái)配合 CSI 插件來(lái)實(shí)現(xiàn)不同的功能,從而保證了 K8s 內(nèi)部邏輯的純粹以及 CSI 插件的簡(jiǎn)單易用。

如有幫助的話歡迎關(guān)注我們項(xiàng)目 Juicedata/JuiceFS 喲! (0?0?)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容