本文发布于Cylon的收藏册,转载请著名原文链接~


Overview

阅读完本文,您当了解

  • Kubernetes 卷
  • CephFS 在 kubernetes 中的挂载
  • Kubelet VolumeManager

本文只是个人理解,如果有大佬觉得不是这样的可以留言一起讨论,参考源码版本为 1.18.20,与高版本相差不大

VolumeManager

VolumeManager VM 是在 kubelet 启动时被初始化的一个异步进程,主要是维护 “Pod" 卷的两个状态,”desiredStateOfWorld“ 和 ”actualStateOfWorld“; 这两个状态用于将节点上的卷 “协调” 到所需的状态。

VM 实际上包含三个 “异步进程” (goroutine),其中有一个 reconciler 就是用于协调与挂载的,下面就来阐述 VM 的挂载过程。

VM中的重要组件

  • actualStateOfWorld
  • mountedPod
  • desiredStateOfWorld
  • VolumeToMount
  • podToMount

VM的组成

VM 的代码位于,由图可以看出,主要包含三个重要部分:

  • reconciler:协调器
  • populator:填充器
  • cache:包含 ”desiredStateOfWorld“ 和 ”actualStateOfWorld“

image-20230820221712742

图:VM的目录组成

在代码结构上,volumeManager 如下所示

// volumeManager implements the VolumeManager interface
type volumeManager struct {
    // DesiredStateOfWorldPopulator 用来与 API 服务器通信以获取 PV 和 PVC 对象的 API 客户端
	kubeClient clientset.Interface
    
    // VolumePluginMgr 是用于访问 VolumePlugin 插件的 VolumePlugin 管理器。它必须预初始化。
	volumePluginMgr *volume.VolumePluginMgr

    // desiredStateOfWorld 是一个数据结构,包含根据 VM 所需的状态:即应附加哪些卷以及 "哪些pod” 正在引用这些卷。
    // 使用 kubelet pod manager 根据 world populator 的所需状态填充数据结构。
	desiredStateOfWorld cache.DesiredStateOfWorld

	// 与 desiredStateOfWorld 相似,是实际状态:即哪些卷被 attacted 到该 Node 以及 volume 被 mounted 到哪些 pod。
    // 成功完成 reconciler attach,detach, mount, 和 unmount 操作后,将填充数据结构。
	actualStateOfWorld cache.ActualStateOfWorld

	// operationExecutor 用于启动异步 attach,detach, mount, 和 unmount 操作。
	operationExecutor operationexecutor.OperationExecutor

	// reconciler reconciler 运行异步周期性循环,通过使用操作执行器触发 attach,detach, mount, 和 unmount操作
    // 来协调 desiredStateOfWorld 与 actualStateOfWorld。
	reconciler reconciler.Reconciler

    // desiredStateOfWorldPopulator 运行异步周期性循环以使用 kubelet Pod Manager 填充desiredStateOfWorld。
	desiredStateOfWorldPopulator populator.DesiredStateOfWorldPopulator

	// csiMigratedPluginManager keeps track of CSI migration status of plugins
	csiMigratedPluginManager csimigration.PluginManager

	// intreeToCSITranslator translates in-tree volume specs to CSI
	intreeToCSITranslator csimigration.InTreeToCSITranslator
}

VM的初始化

  • 入口:“volumeManager”(vm) 的 初始化 操作发生在 kubelet Run 时被作为一个异步进程启动。
  • VM 初始化:
    • 如代码1所示,VM在初始化阶段创建了两个 cache 对象 “desiredStateOfWorld”(dsw)和“actualStateOfWorld”(asw)以及一个 “operationExecutor”,用于启动异步的线程操作 attach,detach, mount, 和 unmount
    • 如代码2所示:VM在初始化阶段还创建了 “desiredStateOfWorldPopulator” (dswp) 与 “reconciler”
      • “reconciler” 通过使用上面的 “operationExecutor” 触发 attach,detach, mount 和 unmount来协调 dsw 与 asw

代码1

vm := &volumeManager{
    kubeClient:          kubeClient,
    volumePluginMgr:     volumePluginMgr,
    desiredStateOfWorld: cache.NewDesiredStateOfWorld(volumePluginMgr),
    actualStateOfWorld:  cache.NewActualStateOfWorld(nodeName, volumePluginMgr),
    operationExecutor: operationexecutor.NewOperationExecutor(operationexecutor.NewOperationGenerator(
        kubeClient,
        volumePluginMgr,
        recorder,
        checkNodeCapabilitiesBeforeMount,
        blockVolumePathHandler)),
}

代码2:

vm.intreeToCSITranslator = intreeToCSITranslator
vm.csiMigratedPluginManager = csiMigratedPluginManager
vm.desiredStateOfWorldPopulator = populator.NewDesiredStateOfWorldPopulator(
    kubeClient,
    desiredStateOfWorldPopulatorLoopSleepPeriod,
    desiredStateOfWorldPopulatorGetPodStatusRetryDuration,
    podManager,
    podStatusProvider,
    vm.desiredStateOfWorld,
    vm.actualStateOfWorld,
    kubeContainerRuntime,
    keepTerminatedPodVolumes,
    csiMigratedPluginManager,
    intreeToCSITranslator)
vm.reconciler = reconciler.NewReconciler(
    kubeClient,
    controllerAttachDetachEnabled,
    reconcilerLoopSleepPeriod,
    waitForAttachTimeout,
    nodeName,
    vm.desiredStateOfWorld,
    vm.actualStateOfWorld,
    vm.desiredStateOfWorldPopulator.HasAddedPods,
    vm.operationExecutor,
    mounter,
    hostutil,
    volumePluginMgr,
    kubeletPodsDir)

VM 的 Run

VM 是在 Kubelet 启动时作为异步线程启动,如代码1所示

如下面代码2所示,VM 在运行时会启动 三个 异步线程

  • 第一个调用是 第二个是调用 “dswp” 填充其的“ Run ”,这里主要做的操作是从 API 拿到 Pod 列表,根据对应条件来决定 attach,detach, mount, 和 unmount
  • 第二个调用的是,reconciler 来协调应该安装的卷是否已安装以及应该卸载的卷是否已卸载。
  • 第三个调用的是,volumePluginMgr,启用 CSI informer

代码1

// Start volume manager
go kl.volumeManager.Run(kl.sourcesReady, wait.NeverStop)

代码2

func (vm *volumeManager) Run(sourcesReady config.SourcesReady, stopCh <-chan struct{}) {
	defer runtime.HandleCrash()

	go vm.desiredStateOfWorldPopulator.Run(sourcesReady, stopCh)
	klog.V(2).Infof("The desired_state_of_world populator starts")

	klog.Infof("Starting Kubelet Volume Manager")
	go vm.reconciler.Run(stopCh)

	metrics.Register(vm.actualStateOfWorld, vm.desiredStateOfWorld, vm.volumePluginMgr)

	if vm.kubeClient != nil {
		// start informer for CSIDriver
		vm.volumePluginMgr.Run(stopCh)
	}

	<-stopCh
	klog.Infof("Shutting down Kubelet Volume Manager")
}

VM 的调用流程

desiredStateOfWorldPopulator

DesiredStateOfWorldPopulator 是一个周期 Loop,会定期循环遍历 Active Pod 列表,并确保每个 Pod 都处于所需状态(如果有卷,World state)。它还会验证 World cache 中处于所需状态的 pod 是否仍然存在,如果不存在,则会将其删除。

desiredStateOfWorldPopulator 结构包含两个方法,ReprocessPod 和 HasAddedPods;ReprocessPod 负责将 processedPods 中指定 pod 的值设置为false,强制重新处理它。这是在 Pod 更新时启用重新挂载卷所必需的。而 HasAddedPods 返回 填充器 是否已循环遍历 Active Pod 列表并将它们添加到 world cache 的所需状态。

在期待填充器 desiredStateOfWorldPopulator 启动时,会运行一个 populatorLoop,这里主要负责运行两个函数,

  • findAndAddNewPods 负责迭代所有 pod,如果它们不存在添加到 desired state of world (desiredStateOfWorld)
  • findAndRemoveDeletedPods 负责迭代 desiredStateOfWorld 下的所有 Pod,如果它们不再存在则将其删除

reconciler

reconciler Run 的过程是通过一个 Loop 函数 reconciliationLoopFunc 完成的,正如下列 代码 所示

func (rc *reconciler) Run(stopCh <-chan struct{}) {
	wait.Until(rc.reconciliationLoopFunc(), rc.loopSleepDuration, stopCh)
}

func (rc *reconciler) reconciliationLoopFunc() func() {
	return func() {
		rc.reconcile()

		// Sync the state with the reality once after all existing pods are added to the desired state from all sources.
		// Otherwise, the reconstruct process may clean up pods' volumes that are still in use because
		// desired state of world does not contain a complete list of pods.
		if rc.populatorHasAddedPods() && !rc.StatesHasBeenSynced() {
			klog.Infof("Reconciler: start to sync state")
			rc.sync()
		}
	}
}

func (rc *reconciler) reconcile() {
   // Unmounts are triggered before mounts so that a volume that was
   // referenced by a pod that was deleted and is now referenced by another
   // pod is unmounted from the first pod before being mounted to the new
   // pod.
   // 卸载会在挂载之前触发,以便已删除的 Pod 引用的卷现在被另一个 Pod 引用,
   // 然后再挂载到新 Pod 之前从第一个 Pod 中卸载。
   rc.unmountVolumes()

   // Next we mount required volumes. This function could also trigger
   // attach if kubelet is responsible for attaching volumes.
   // If underlying PVC was resized while in-use then this function also handles volume
   // resizing.
   // 接下来我们安装所需的卷。如果 kubelet 负责附加卷,
   // 则此函数还可以触发附加。如果底层 PVC 在使用时调整了大小,则此函数还可以处理卷大小调整。
   rc.mountAttachVolumes()

   // Ensure devices that should be detached/unmounted are detached/unmounted.
   // 确保应 detached/unmounted 的设备已完成 detached/unmounted。
   rc.unmountDetachDevices()
}

Reconciler 是挂载部分最重要的角色,用于协调应该安装的卷是否已安装以及应该卸载的卷是否已卸载;对应的,实际上执行的为三个函数:“unmountVolumes”、“mountAttachVolumes” 和 “unmountDetachDevices”。

mountAttachVolumes
  1. 首先,“mountAttachVolumes” 会调用 “dsw” (desiredStateOfWorld) 的函数 “GetVolumesToMount” 来检索所有 “volumesToMount” 并迭代它们,这里主要是为了确保 “volumes” 应完成了 “attached/mounted”

  2. 接下来这个循环做的工作是,对于每个 Volume 和 Pod,都会检查该 Volume 或 Pod 是否存在于 “asw” 的 “attachedVolumes” 中。如果 Volume 不存在,则“asw”返回 “newVolumeNotAttachedError ”,否则它检查指定的 pod 是否存在并根据状态返回结果。这里存在 三个状态,返回也是根据这个状态返回。这里主要为了得到挂载路径和是否挂载

    • VolumeMounted:表示 Volume 已挂载到 pod 的本地路径中

    • VolumeMountUncertain:表示 Volume 可能会也可能不会安装在 Pod 的本地路径中

    • VolumeNotMounted:表示 Volume 还未挂载到 pod 的本地路径中

  3. 当“ asw” 返回 “ newVolumeNotAttachedError ” 时,“reconciler” 会检查 “controllerAttachDetachEnabled” 是否启用,或 “volumeToMount” 没有实现了对应插件,这里面如果其中任何一个为 true,“reconciler” 将调用 “operationExecutor” 来执行操作“ ,走到这里代表了 Volume 没有被 attach,或者没有实现 attacher,例如 cephfs 没有实现 attacher;或者是 kubelet 禁用了 attach [1] (默认是开启状态),将进入 “ VerifyControllerAttachedVolume

    • 在此期间,“operationExecutor” 生成一个名为 “verifyControllerAttachedVolumeFunc” 的函数来实际实现。在此函数中,如果 “volumeToMount” 的 “PluginIsAttachable” 为 false(没有实现),则假设其已经实现并标记 attached,标记出错时进行重试(这是一个函数用于后面的调用,这里只是定义)

    • 如果还没有将 Node attached 到 Volume 节点列表状态中,则返回错误进行重试(这是一个函数用于后面的调用,这里只是定义)

    • 上面两个步骤是为了组装这个操作,返回的是操作的内容,包含执行的函数,完成的hook等,最后运行这个函数并返回

  4. 这是步骤3的另外一个分支,即 kubelet 启用了 ADController,并且实现了对应的 attcher,那么将执行附加操作

    • 拼接对象
    • 执行函数 ”AttachVolume“
    • AttachVolume 如上面步骤一样,拼接出最后的执行的动作,进行执行操作(将 node 附加到 volume 之上)
  5. 步骤5 表示 3, 4 条件均不满足,也就是 Attached,目前状态为 ”未挂载“ 或者 ”已挂载“,将执行这个步骤,未挂载的进行挂载,已挂载的进行 remount

  6. 在该分支中(也就是 步骤5 执行的)执行的是名为 “GenerateMountVolumeFunc“ 的函数,在此函数中,会获取 Plugin ,并通过 Plugin 创建出一个 volumeMounter,在通过 Plugin 获取一个 deviceMouter(能够挂载块设备的);当然我们这里挂载的是 ”cephfs“ 所以没有 ”deviceMouter“ 这里不被执行。

    • 如果 ”deviceMounter“ 定义了,那么则执行这个 plugin 的 “MountDevice” 函数
    • 如果没有定义,那么执行 volumeMounter 的 SetUp 进行挂载(因为不是块设备)
  7. 执行 SetUp 函数,通常 NFS, CephFS, HostPath,都实现了这个函数,那么就会通过这个函数挂载到 Node 对应的目录

  8. 最后通过 Overlay2 文件系统附加到容器里

Reference

[1] command-line-tools-reference kubelet

[2] What happens when volumeManager in the kubelet starts?

本文发布于Cylon的收藏册,转载请著名原文链接~

链接:https://www.oomkill.com/2023/08/ch29-volumemanager/

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」 许可协议进行许可。