CSI 工作原理與JuiceFS CSI Driver 的架構設計詳解

語言: CN / TW / HK

容器儲存介面(Container Storage Interface)簡稱 CSI,CSI 建立了行業標準介面的規範,藉助 CSI 容器編排系統(CO)可以將任意儲存系統暴露給自己的容器工作負載。JuiceFS CSI Driver 通過實現 CSI 介面使得 Kubernetes 上的應用可以通過 PVC(PersistentVolumeClaim)使用 JuiceFS。本文將詳細介紹 CSI 的工作原理以及 JuiceFS CSI Driver 的架構設計。

CSI 的基本元件

CSI 的 cloud providers 有兩種型別,一種為 in-tree 型別,一種為 out-of-tree 型別。前者是指執行在 K8s 核心元件內部的儲存外掛;後者是指獨立在 K8s 元件之外執行的儲存外掛。本文主要介紹 out-of-tree 型別的外掛。

out-of-tree 型別的外掛主要是通過 gRPC 介面跟 K8s 元件互動,並且 K8s 提供了大量的 SideCar 元件來配合 CSI 外掛實現豐富的功能。對於 out-of-tree 型別的外掛來說,所用到的元件分為 SideCar 元件和第三方需要實現的外掛。

SideCar 元件

external-attacher

監聽 VolumeAttachment 物件,並呼叫 CSI driver Controller 服務的 ControllerPublishVolumeControllerUnpublishVolume 介面,用來將 volume 附著到 node 上,或從 node 上刪除。

如果儲存系統需要 attach/detach 這一步,就需要使用到這個元件,因為 K8s 內部的 Attach/Detach Controller 不會直接呼叫 CSI driver 的介面。

external-provisioner

監聽 PVC 物件,並呼叫 CSI driver Controller 服務的 CreateVolumeDeleteVolume 介面,用來提供一個新的 volume。前提是 PVC 中指定的 StorageClass 的 provisioner 欄位和 CSI driver Identity 服務的 GetPluginInfo 介面的返回值一樣。一旦新的 volume 提供出來,K8s 就會建立對應的 PV。

而如果 PVC 繫結的 PV 的回收策略是 delete,那麼 external-provisioner 元件監聽到 PVC 的刪除後,會呼叫 CSI driver Controller 服務的 DeleteVolume 介面。一旦 volume 刪除成功,該元件也會刪除相應的 PV。

該元件還支援從快照建立資料來源。如果在 PVC 中指定了 Snapshot CRD 的資料來源,那麼該元件會通過 SnapshotContent 物件獲取有關快照的資訊,並將此內容在呼叫 CreateVolume 介面的時候傳給 CSI driver,CSI driver 需要根據資料來源快照來建立 volume。

external-resizer

監聽 PVC 物件,如果使用者請求在 PVC 物件上請求更多儲存,該元件會呼叫 CSI driver Controller 服務的 NodeExpandVolume 介面,用來對 volume 進行擴容。

external-snapshotter

該元件需要與 Snapshot Controller 配合使用。Snapshot Controller 會根據叢集中建立的 Snapshot 物件建立對應的 VolumeSnapshotContent,而 external-snapshotter 負責監聽 VolumeSnapshotContent 物件。當監聽到 VolumeSnapshotContent 時,將其對應引數通過 CreateSnapshotRequest 傳給 CSI driver Controller 服務,呼叫其 CreateSnapshot 介面。該元件還負責呼叫 DeleteSnapshotListSnapshots 介面。

livenessprobe

負責監測 CSI driver 的健康情況,並通過 Liveness Probe 機制彙報給 K8s,當監測到 CSI driver 有異常時負責重啟 pod。

node-driver-registrar

通過直接呼叫 CSI driver Node 服務的 NodeGetInfo 介面,將 CSI driver 的資訊通過 kubelet 的外掛註冊機制在對應節點的 kubelet 上進行註冊。

external-health-monitor-controller

通過呼叫 CSI driver Controller 服務的 ListVolumes 或者 ControllerGetVolume 介面,來檢查 CSI volume 的健康情況,並上報在 PVC 的 event 中。

external-health-monitor-agent

通過呼叫 CSI driver Node 服務的 NodeGetVolumeStats 介面,來檢查 CSI volume 的健康情況,並上報在 pod 的 event 中。

第三方外掛

第三方儲存提供方(即 SP,Storage Provider)需要實現 Controller 和 Node 兩個外掛,其中 Controller 負責 Volume 的管理,以 StatefulSet 形式部署;Node 負責將 Volume mount 到 pod 中,以 DaemonSet 形式部署在每個 node 中。

CSI 外掛與 kubelet 以及 K8s 外部元件是通過 Unix Domani Socket gRPC 來進行互動呼叫的。CSI 定義了三套 RPC 介面,SP 需要實現這三組介面,以便與 K8s 外部元件進行通訊。三組介面分別是:CSI Identity、CSI Controller 和 CSI Node,下面詳細看看這些介面定義。

CSI Identity

用於提供 CSI driver 的身份資訊,Controller 和 Node 都需要實現。介面如下:

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

  rpc GetPluginCapabilities(GetPluginCapabilitiesRequest)
    returns (GetPluginCapabilitiesResponse) {}

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

GetPluginInfo 是必須要實現的,node-driver-registrar 元件會呼叫這個介面將 CSI driver 註冊到 kubelet;GetPluginCapabilities 是用來表明該 CSI driver 主要提供了哪些功能。

CSI Controller

用於實現建立/刪除 volume、attach/detach volume、volume 快照、volume 擴縮容等功能,Controller 外掛需要實現這組介面。介面如下:

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 外部元件的時候已經提到,不同的介面分別提供給不同的元件呼叫,用於配合實現不同的功能。比如 CreateVolume/DeleteVolume 配合 external-provisioner 實現建立/刪除 volume 的功能;ControllerPublishVolume/ControllerUnpublishVolume 配合 external-attacher 實現 volume 的 attach/detach 功能等。

CSI Node

用於實現 mount/umount volume、檢查 volume 狀態等功能,Node 外掛需要實現這組介面。介面如下:

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 用來實現多個 pod 共享一個 volume 的功能,支援先將 volume 掛載到一個臨時目錄,然後通過 NodePublishVolume 將其掛載到 pod 中;NodeUnstageVolume 為其反操作。

工作流程

下面來看看 pod 掛載 volume 的整個工作流程。整個流程流程分別三個階段:Provision/Delete、Attach/Detach、Mount/Unmount,不過不是每個儲存方案都會經歷這三個階段,比如 NFS 就沒有 Attach/Detach 階段。

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

Provision

先來看 Provision 階段,整個過程如上圖所示。其中 extenal-provisioner 和 PVController 均 watch PVC 資源。

  1. 當 PVController watch 到叢集中有 PVC 建立時,會判斷當前是否有 in-tree plugin 與之相符,如果沒有則判斷其儲存型別為 out-of-tree 型別,於是給 PVC 打上註解 volume.beta.kubernetes.io/storage-provisioner={csi driver name}
  2. 當 extenal-provisioner watch 到 PVC 的註解 csi driver 與自己的 csi driver 一致時,呼叫 CSI Controller 的 CreateVolume 介面;
  3. 當 CSI Controller 的 CreateVolume 介面返回成功時,extenal-provisioner 會在叢集中建立對應的 PV;
  4. PVController watch 到叢集中有 PV 建立時,將 PV 與 PVC 進行繫結。

Attach

Attach 階段是指將 volume 附著到節點上,整個過程如上圖所示。

  1. ADController 監聽到 pod 被排程到某節點,並且使用的是 CSI 型別的 PV,會呼叫內部的 in-tree CSI 外掛的介面,該介面會在叢集中建立一個 VolumeAttachment 資源;
  2. external-attacher 元件 watch 到有 VolumeAttachment 資源創建出來時,會呼叫 CSI Controller 的 ControllerPublishVolume 介面;
  3. 當 CSI Controller 的 ControllerPublishVolume 介面呼叫成功後,external-attacher 將對應的 VolumeAttachment 物件的 Attached 狀態設為 true;
  4. ADController watch 到 VolumeAttachment 物件的 Attached 狀態為 true 時,更新 ADController 內部的狀態 ActualStateOfWorld。

Mount

最後一步將 volume 掛載到 pod 裡的過程涉及到 kubelet。整個流程簡單地說是,對應節點上的 kubelet 在建立 pod 的過程中,會呼叫 CSI Node 外掛,執行 mount 操作。下面再針對 kubelet 內部的元件細分進行分析。

首先 kubelet 建立 pod 的主函式 syncPod 中,kubelet 會呼叫其子元件 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 中包含兩個元件:desiredStateOfWorldPopulator 和 reconciler。這兩個元件相互配合就完成了 volume 在 pod 中的 mount 和 umount 過程。整個過程如下:

desiredStateOfWorldPopulator 和 reconciler 的協同模式是生產者和消費者的模式。volumeManager 中維護了兩個佇列(嚴格來講是 interface,但這裡充當了佇列的作用),即 DesiredStateOfWorld 和 ActualStateOfWorld,前者維護的是當前節點中 volume 的期望狀態;後者維護的是當前節點中 volume 的實際狀態。

而 desiredStateOfWorldPopulator 在自己的迴圈中只做了兩個事情,一個是從 kubelet 的 podManager 中獲取當前節點新建的 Pod,將其需要掛載的 volume 資訊記錄到 DesiredStateOfWorld 中;另一件事是從 podManager 中獲取當前節點中被刪除的 pod,檢查其 volume 是否在 ActualStateOfWorld 的記錄中,如果沒有,將其在 DesiredStateOfWorld 中也刪除,從而保證 DesiredStateOfWorld 記錄的是節點中所有 volume 的期望狀態。相關程式碼如下(為了精簡邏輯,刪除了部分程式碼):

// 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 就是消費者,它主要做了三件事:

  1. unmountVolumes():在 ActualStateOfWorld 中遍歷 volume,判斷其是否在 DesiredStateOfWorld 中,如果不在,則呼叫 CSI Node 的介面執行 unmount,並在 ActualStateOfWorld 中記錄;
  2. mountAttachVolumes():從 DesiredStateOfWorld 中獲取需要被 mount 的 volume,呼叫 CSI Node 的介面執行 mount 或擴容,並在 ActualStateOfWorld 中做記錄;
  3. unmountDetachDevices(): 在 ActualStateOfWorld 中遍歷 volume,若其已經 attach,但沒有使用的 pod,並在 DesiredStateOfWorld 也沒有記錄,則將其 unmount/detach 掉。

我們以 mountAttachVolumes() 為例,看看其如何呼叫 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)
			...
		}
	}
}

執行 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)
}

該函式先構造執行函式,再執行,那麼再看建構函式:

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"),
	}
}

這裡先去註冊到 kubelet 的 CSI 的 plugin 列表中找到對應的外掛,然後再執行 volumeMounter.SetUp,最後更新 ActualStateOfWorld 的記錄。這裡負責執行 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 中呼叫 CSI Node NodePublishVolume/NodeUnPublishVolume 介面的是 volumeManager 的 csiMountMgr。至此,整個 Pod 的 volume 流程就已經梳理清楚了。

JuiceFS CSI Driver 工作原理

接下來再來看看 JuiceFS CSI Driver 的工作原理。架構圖如下:

JuiceFS 在 CSI Node 介面 NodePublishVolume 中建立 pod,用來執行 juicefs mount xxx,從而保證 juicefs 客戶端執行在 pod 裡。如果有多個的業務 pod 共用一份儲存,mount pod 會在 annotation 進行引用計數,確保不會重複建立。具體的程式碼如下(為了方便閱讀,省去了日誌等無關程式碼):

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)
}

每當有業務 pod 退出時,CSI Node 會在介面 NodeUnpublishVolume 刪除 mount pod annotation 中對應的計數,當最後一個記錄被刪除時,mount pod 才會被刪除。具體程式碼如下(為了方便閱讀,省去了日誌等無關程式碼):

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 客戶端解耦,做升級不會影響到業務容器;將客戶端獨立在 pod 中執行也就使其在 K8s 的管控內,可觀測性更強;同時 pod 的好處我們也能享受到,比如隔離性更強,可以單獨設定客戶端的資源配額等。

總結

本文從 CSI 的元件、CSI 介面、volume 如何掛載到 pod 上,三個方面入手,分析了 CSI 整個體系工作的過程,並介紹了 JuiceFS CSI Driver 的工作原理。CSI 是整個容器生態的標準儲存介面,CO 通過 gRPC 方式和 CSI 外掛通訊,而為了做到普適,K8s 設計了很多外部元件來配合 CSI 外掛來實現不同的功能,從而保證了 K8s 內部邏輯的純粹以及 CSI 外掛的簡單易用。

如有幫助的話歡迎關注我們專案 Juicedata/JuiceFS 喲! (0ᴗ0✿)