容器存儲(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ù)的 ControllerPublishVolume
和 ControllerUnpublishVolume
接口,用來(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ù)的 CreateVolume
和 DeleteVolume
接口,用來(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)用 DeleteSnapshot
、ListSnapshots
接口。
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
先來(lái)看 Provision 階段,整個(gè)過(guò)程如上圖所示。其中 extenal-provisioner 和 PVController 均 watch PVC 資源。
- 當(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}
; - 當(dāng) extenal-provisioner watch 到 PVC 的注解 csi driver 與自己的 csi driver 一致時(shí),調(diào)用 CSI Controller 的
CreateVolume
接口; - 當(dāng) CSI Controller 的
CreateVolume
接口返回成功時(shí),extenal-provisioner 會(huì)在集群中創(chuàng)建對(duì)應(yīng)的 PV; - PVController watch 到集群中有 PV 創(chuàng)建時(shí),將 PV 與 PVC 進(jìn)行綁定。
Attach
Attach 階段是指將 volume 附著到節(jié)點(diǎn)上,整個(gè)過(guò)程如上圖所示。
- 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 資源;
- external-attacher 組件 watch 到有 VolumeAttachment 資源創(chuàng)建出來(lái)時(shí),會(huì)調(diào)用 CSI Controller 的
ControllerPublishVolume
接口; - 當(dāng) CSI Controller 的
ControllerPublishVolume
接口調(diào)用成功后,external-attacher 將對(duì)應(yīng)的 VolumeAttachment 對(duì)象的 Attached 狀態(tài)設(shè)為 true; - ADController watch 到 VolumeAttachment 對(duì)象的 Attached 狀態(tài)為 true 時(shí),更新 ADController 內(nèi)部的狀態(tài) ActualStateOfWorld。
Mount
最后一步將 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ò)程如下:
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)者,它主要做了三件事:
-
unmountVolumes()
:在 ActualStateOfWorld 中遍歷 volume,判斷其是否在 DesiredStateOfWorld 中,如果不在,則調(diào)用 CSI Node 的接口執(zhí)行 unmount,并在 ActualStateOfWorld 中記錄; -
mountAttachVolumes()
:從 DesiredStateOfWorld 中獲取需要被 mount 的 volume,調(diào)用 CSI Node 的接口執(zhí)行 mount 或擴(kuò)容,并在 ActualStateOfWorld 中做記錄; -
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)圖如下:
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?)