Explication détaillée du principe de fonctionnement CSI et de la conception de l'architecture du pilote JuiceFS CSI

Container Storage Interface (CSI) est abrégé en CSI. CSI établit une spécification d'interface standard de l'industrie. Avec l'aide du CSI Container Orchestration System (CO), n'importe quel système de stockage peut être exposé à ses propres charges de travail de conteneur. Le pilote JuiceFS CSI permet aux applications sur Kubernetes d'utiliser JuiceFS via PVC (PersistentVolumeClaim) en implémentant l'interface CSI. Cet article présentera en détail le principe de fonctionnement de CSI et la conception de l'architecture du pilote JuiceFS CSI.

Composants de base de CSI

Il existe deux types de fournisseurs de cloud dans CSI, l'un est de type in-tree et l'autre est de type out-of-tree. Le premier fait référence aux plug-ins de stockage qui s'exécutent à l'intérieur des composants principaux du K8 ; le second fait référence aux plug-ins de stockage qui s'exécutent indépendamment des composants du K8. Cet article présente principalement les plugins de type out-of-tree.

Les plug-ins de type hors arbre interagissent principalement avec les composants K8 via l'interface gRPC, et K8 fournit un grand nombre de composants SideCar pour coopérer avec les plug-ins CSI afin d'obtenir des fonctions riches. Pour les plug-ins de type out-of-tree, les composants utilisés sont divisés en composants SideCar et plug-ins qui doivent être implémentés par des tiers.

Composants SideCar

external-attacher

Surveillez l'objet VolumeAttachment et appelez ControllerPublishVolumeles ControllerUnpublishVolumeinterfaces et du service de contrôleur du pilote CSI pour attacher le volume au nœud ou le supprimer du nœud.

Si le système de stockage a besoin de l'étape d'attachement/détachement, ce composant doit être utilisé, car le contrôleur d'attachement/détachement à l'intérieur de K8s n'appellera pas directement l'interface du pilote CSI.

commissions externes

Surveillez l'objet PVC et appelez CreateVolumeles DeleteVolumeinterfaces et du service Contrôleur du pilote CSI pour fournir un nouveau volume. Le principe est que le champ fournisseur de la StorageClass spécifiée dans la PVC est identique à la valeur de retour de l' GetPluginInfointerface . Une fois le nouveau volume fourni, K8s créera le PV correspondant.

Si la politique de recyclage du PV lié au PVC est supprimée, le composant d'approvisionnement externe appellera l' DeleteVolumeinterface . Une fois le volume supprimé avec succès, le composant supprime également la PV correspondante.

Le composant prend également en charge la création de sources de données à partir d'instantanés. Si la source de données de l'instantané CRD est spécifiée dans le PVC, le composant obtiendra des informations sur l'instantané via l' SnapshotContentobjet et transmettra ce contenu CreateVolumeau pilote CSI lors de l'appel de l'interface. Le pilote CSI doit créer un volume basé sur le instantané de la source de données.

redimensionneur externe

Surveille l'objet PVC. Si l'utilisateur demande plus de stockage sur l'objet PVC, ce composant appellera l' NodeExpandVolumeinterface pour étendre le volume.

instantané-externe

Ce composant est requis pour fonctionner avec le Snapshot Controller. Le Snapshot Controller créera le VolumeSnapshotContent correspondant en fonction des objets Snapshot créés dans le cluster, et le snapshot externe est responsable de la surveillance des objets VolumeSnapshotContent. Lorsque VolumeSnapshotContent est surveillé, ses paramètres correspondants sont CreateSnapshotRequesttransmis au pilote CSI et son CreateSnapshotinterface est appelée. Le composant est également responsable de l'appel de DeleteSnapshot, ListSnapshotsinterface.

sonde de vivacité

Il est chargé de surveiller l'état du pilote CSI et de le signaler à K8 via le mécanisme Liveness Probe Il est chargé de redémarrer le pod lorsqu'une anomalie dans le pilote CSI est détectée.

nœud-driver-registrar

En appelant directement l' NodeGetInfointerface , les informations du pilote CSI sont enregistrées sur le kubelet du nœud correspondant via le mécanisme d'enregistrement du plug-in du kubelet.

contrôleur-moniteur-de-santé-externe

Vérifiez la santé du volume CSI en appelant ListVolumesl' ControllerGetVolumeinterface ou du service de contrôleur du pilote CSI, et signalez-le en cas de PVC.

agent-de-moniteur-de-santé-externe

Vérifiez l'état de santé du volume CSI en appelant l' NodeGetVolumeStatsinterface et signalez-le en cas de pod.

plugins tiers

Le fournisseur de stockage tiers (c'est-à-dire SP, fournisseur de stockage) doit implémenter deux plug-ins, le contrôleur et le nœud. Le contrôleur est responsable de la gestion du volume et est déployé sous la forme de StatefulSet ; le nœud est responsable du montage du volume dans le pod et en le déployant dans chaque nœud sous la forme de DaemonSet middle.

Le plug-in CSI, le kubelet et les composants externes K8s sont appelés de manière interactive via Unix Domani Socket gRPC. CSI définit trois ensembles d'interfaces RPC, que SP doit implémenter pour communiquer avec les composants externes du K8. Les trois groupes d'interfaces sont : CSI Identity, CSI Controller et CSI Node. Examinons ces définitions d'interface en détail.

Identité CSI

Pour fournir les informations d'identité du pilote CSI, le contrôleur et le nœud doivent implémenter. L'interface est la suivante :

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

  rpc GetPluginCapabilities(GetPluginCapabilitiesRequest)
    returns (GetPluginCapabilitiesResponse) {}

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

GetPluginInfoIl doit être implémenté.Le composant node-driver-registrar appellera cette interface pour enregistrer le pilote CSI auprès du kubelet ; GetPluginCapabilitiesil est utilisé pour indiquer quelles fonctions le pilote CSI fournit principalement.

Contrôleur CSI

Il est utilisé pour implémenter des fonctions telles que la création/suppression de volumes, l'attachement/détachement de volumes, les instantanés de volume et la mise à l'échelle du volume. Le plug-in Controller doit implémenter cet ensemble d'interfaces. L'interface est la suivante :

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

Comme mentionné ci-dessus lors de l'introduction des composants externes des K8, différentes interfaces sont fournies pour que différents appels de composants coopèrent pour réaliser différentes fonctions. Par exemple CreateVolume/ DeleteVolumecoopérez avec le fournisseur externe pour réaliser la fonction de création/suppression de volume ; ControllerPublishVolume/ ControllerUnpublishVolumecoopérez avec l'attacheur externe pour réaliser la fonction d'attachement/détachement du volume, etc.

Nœud CSI

Il est utilisé pour implémenter des fonctions telles que monter/démonter le volume et vérifier l'état du volume. Les plugins de nœud doivent implémenter cet ensemble d'interfaces. L'interface est la suivante :

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

NodeStageVolumeIl est utilisé pour réaliser la fonction selon laquelle plusieurs pods partagent un volume. Il prend en charge le montage d'abord du volume dans un répertoire temporaire, NodePublishVolumepuis le monte sur le pod via ; NodeUnstageVolumec'est l'opération inverse.

processus de travail

Jetons un coup d'œil à l'ensemble du flux de travail du volume de montage de pod. L'ensemble du flux de processus est divisé en trois étapes : Provisionner/Supprimer, Attacher/Détacher, Monter/Démonter, mais toutes les solutions de stockage ne passeront pas par ces trois étapes, par exemple, NFS n'a pas l'étape Attacher/Détacher.

L'ensemble du processus implique non seulement le travail des composants présentés ci-dessus, mais également les composants AttachDetachController et PVController du ControllerManager et du kubelet. Les trois étapes de Provision, Attach et Mount seront analysées en détail ci-dessous.

Disposition

Regardons d'abord l'étape Provision, l'ensemble du processus est illustré dans la figure ci-dessus. Parmi eux, extenal-provisioner et PVController sont des ressources PVC de surveillance.

  1. Lorsque PVController regarde pour voir qu'il y a un PVC créé dans le cluster, il jugera s'il y a un plugin dans l'arborescence qui lui correspond. Sinon, il jugera que son type de stockage est de type hors arborescence, donc il annotera le PVC volume.beta.kubernetes.io/storage-provisioner={csi driver name};
  2. Lorsque la surveillance du fournisseur externe du pilote csi d'annotation du PVC est cohérente avec son propre pilote csi, appelez l' CreateVolumeinterface ;
  3. Lorsque l' CreateVolumeinterface revient avec succès, le fournisseur externe créera le PV correspondant dans le cluster ;
  4. Lorsque PVController veille à créer des PV dans le cluster, il lie les PV aux PVC.

Attacher

L'étape d'attachement fait référence à l'attachement du volume au nœud. L'ensemble du processus est illustré dans la figure ci-dessus.

  1. ADController surveille qu'un pod est planifié sur un nœud et utilise un PV de type CSI, et appellera l'interface du plug-in CSI interne dans l'arborescence, qui créera une ressource VolumeAttachment dans le cluster ;
  2. La surveillance du composant external-attacher appellera l' ControllerPublishVolumeinterface ;
  3. Lorsque l' ControllerPublishVolumeinterface appelée avec succès, l'attacheur externe définit l'état Attaché de l'objet VolumeAttachment correspondant sur vrai ;
  4. Lorsque ADController observe que l'état Attaché de l'objet VolumeAttachment est vrai, mettez à jour l'état ActualStateOfWorld dans ADController.

Monter

La dernière étape de montage du volume dans le pod implique le kubelet. L'ensemble du processus est simplement que lorsque le kubelet sur le nœud correspondant crée un pod, il appellera le plug-in CSI Node pour effectuer l'opération de montage. Ensuite, nous analyserons la segmentation des composants à l'intérieur du kubelet.

Tout d'abord, dans la fonction principale syncPodde , kubelet appellera la WaitForAttachAndMountméthode de son sous-composant volumeManager et attendra que le montage du volume se termine :

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

Le volumeManager contient deux composants : wantedStateOfWorldPopulator et le réconciliateur. Ces deux composants coopèrent l'un avec l'autre pour terminer le processus de montage et de démontage du volume dans le pod. L'ensemble du processus est le suivant:

Le modèle collaboratif de l'état de population souhaité du monde et du réconciliateur est le modèle du producteur et du consommateur. Deux files d'attente sont maintenues dans le volumeManager (à proprement parler, c'est une interface, mais il agit ici comme une file d'attente), à ​​savoir DesiredStateOfWorld et ActualStateOfWorld. La première maintient l'état attendu du volume dans le nœud courant ; la seconde maintient le volume dans l'état actuel du nœud actuel.

Le desireStateOfWorldPopulator ne fait que deux choses dans sa propre boucle. L'une est d'obtenir le Pod nouvellement créé du nœud actuel à partir du podManager du kubelet, et d'enregistrer les informations de volume à monter dans DesiredStateOfWorld ; l'autre est de l'obtenir à partir du podManager . Pour le pod supprimé dans le nœud actuel, vérifiez si son volume figure dans l'enregistrement de ActualStateOfWorld. Si ce n'est pas le cas, supprimez-le dans DesiredStateOfWorld pour vous assurer que DesiredStateOfWorld enregistre l'état attendu de tous les volumes du nœud. Le code pertinent est le suivant (afin de simplifier la logique, certains codes ont été supprimés) :

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

Le réconciliateur est le consommateur, et il fait principalement trois choses :

  1. unmountVolumes(): Parcourez le volume dans ActualStateOfWorld pour déterminer s'il se trouve dans DesiredStateOfWorld, sinon, appelez l'interface de CSI Node pour effectuer le démontage et enregistrez-le dans ActualStateOfWorld ;
  2. mountAttachVolumes(): Obtenez le volume à monter à partir de DesiredStateOfWorld, appelez l'interface de CSI Node pour monter ou étendre, et enregistrez-le dans ActualStateOfWorld ;
  3. unmountDetachDevices() : parcourez le volume dans ActualStateOfWorld. S'il a été attaché mais qu'aucun pod n'est utilisé et qu'il n'y a pas d'enregistrement dans DesiredStateOfWorld, démontez-le/détachez-le.

Prenons mountAttachVolumes()comme exemple pour voir comment il appelle l'interface de 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)
			...
		}
	}
}

Les opérations d'exécution de mount sont toutes effectuées rc.operationExecutordans , puis regardez le code de 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)
}

La fonction construit d'abord la fonction d'exécution, puis l'exécute, puis regarde le constructeur :

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

Ici, allez d'abord dans la liste des plugins CSI enregistrés sur le kubelet pour trouver le plugin correspondant, puis exécutez volumeMounter.SetUp-le, et enfin mettez à jour l'enregistrement ActualStateOfWorld. Voici csiMountMgr responsable de l'exécution du plug-in CSI externe, le code est le suivant :

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
}

Comme vous pouvez le voir, le csiMountMgr du volumeManager appelle le nœud NodePublishVolume/ NodeUnPublishVolumel'interface CSI dans le kubelet. Jusqu'à présent, le processus de volume de l'ensemble du pod a été réglé.

Fonctionnement du pilote JuiceFS CSI

Voyons maintenant comment fonctionne le pilote JuiceFS CSI. Le schéma d'architecture est le suivant :

JuiceFS crée un pod dans l' NodePublishVolumeinterface pour l'exécution juicefs mount xxx, garantissant ainsi que le client juicefs s'exécute dans le pod. Si plusieurs modules métier partagent un stockage, le module de montage sera compté en référence dans l'annotation pour garantir qu'il ne sera pas créé à plusieurs reprises. Le code spécifique est le suivant (pour la commodité de la lecture, le journal et les autres codes non pertinents sont omis) :

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

Chaque fois qu'un module de service se ferme, le nœud CSI NodeUnpublishVolumesupprime , et le module de montage est supprimé uniquement lorsque le dernier enregistrement est supprimé. Le code spécifique est le suivant (pour la commodité de la lecture, le journal et les autres codes non pertinents sont omis) :

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

Le pilote CSI est découplé du client juicefs et la mise à niveau n'affectera pas le conteneur métier ; exécuter le client indépendamment dans le pod le rend plus observable sous le contrôle des K8 ; en même temps, nous pouvons également profiter des avantages du pod Par exemple, l'isolation est plus forte et le quota de ressources du client peut être défini séparément.

Résumer

Cet article part de trois aspects : les composants CSI, les interfaces CSI et la manière dont les volumes sont montés sur les pods, analyse le processus de fonctionnement de l'ensemble du système CSI et présente le principe de fonctionnement du pilote JuiceFS CSI. CSI est l'interface de stockage standard pour l'ensemble de l'écosystème de conteneurs. CO communique avec les plug-ins CSI via gRPC. Afin d'être universel, K8s a conçu de nombreux composants externes pour coopérer avec les plug-ins CSI afin d'obtenir différentes fonctions, garantissant ainsi la pureté de la logique interne du K8 et de la facilité d'utilisation du plug-in CSI.

Si cela vous est utile, veuillez suivre notre projet Juicedata/JuiceFS ! (0ᴗ0✿)

{{o.name}}
{{m.name}}

Je suppose que tu aimes

Origine my.oschina.net/u/5389802/blog/5495854
conseillé
Classement