k8s之Attach 和 Mount

发布于:2025-07-17 ⋅ 阅读:(16) ⋅ 点赞:(0)

Attach 和 Mount

一、核心概念对比

操作 Attach(挂载设备) Mount(挂载文件系统)
定义 将存储卷(如 EBS、NFS 等)连接到宿主机 将已 Attach 的存储设备映射为宿主机上的文件系统路径
执行者 云提供商驱动(AWS EBS CSI Driver)或存储系统插件 容器运行时(containerd、Docker)或 kubelet
操作对象 存储卷(Volume) 文件系统(Filesystem)
Kubernetes 资源 VolumeAttachment 对象 Pod.spec.volumes 定义
操作结果 宿主机可识别存储设备(如 /dev/xvdf 容器可访问文件路径(如 /data

二、工作流程与协作关系

1. Attach 流程

请添加图片描述
图源:https://www.lixueduan.com/posts/kubernetes/14-pv-dynamic-provision-process/#1-attach

1. 核心组件与职责

Kubernetes 的存储 Attach 流程由两个核心组件协作完成:

  • AD Controller (AttachDetach Controller)
    位于 kube-controller-manager 中,负责计算节点上需要 Attach/Detach 的卷,并创建 VolumeAttachment 资源。
  • external-attacher
    独立运行的 CSI 插件,监听 VolumeAttachment 资源变化,调用 CSI Driver 的接口执行实际 Attach 操作。
2. Attach 触发条件

AD Controller 通过以下逻辑触发 Attach 操作:

  1. 监听 Pod 调度:当 Pod 被调度到特定节点时,AD Controller 获取该节点上所有 Pod 的 Volume 列表。
  2. 计算待 Attach 卷:对比当前节点的 status.volumesAttached 与 Pod 需要的卷,找出未 Attach 的 PV。
  3. 多节点挂载检查:对 ReadWriteOnce (RWO) 类型的卷,检查是否已被其他节点挂载(若已挂载则报错)。
3. VolumeAttachment 资源

AD Controller 创建的 VolumeAttachment 对象包含三个关键信息:

apiVersion: storage.k8s.io/v1
kind: VolumeAttachment
spec:
  attacher: nfs.csi.k8s.io  # CSI Driver 名称
  nodeName: ee              # 目标节点
  source:
    persistentVolumeName: pvc-047acd58-...  # 待挂载的 PV
status:
  attached: false  # 挂载状态(由 external-attacher 更新)
4. 详细执行流程
ADController VolumeAttachment ExternalAttacher CSIDriver CloudProvider Node 创建 VolumeAttachment 资源 监听资源变化 调用 ControllerPublishVolume(PV, Node) 调用云 API(如 AWS EBS Attach) 返回设备 ID(如 /dev/xvdf) 返回成功 更新 status.attached=true 更新 status.volumesAttached ADController VolumeAttachment ExternalAttacher CSIDriver CloudProvider Node

2. Mount 流程

2.1 核心组件与数据结构

Kubernetes 的 Mount 流程由 kubeletvolumeManager 组件管理,主要包含以下核心元素:

type volumeManager struct {
    desiredStateOfWorld cache.DesiredStateOfWorld // 期望状态缓存
    actualStateOfWorld  cache.ActualStateOfWorld // 实际状态缓存
    reconciler          reconciler.Reconciler     // 状态协调器
    desiredStateOfWorldPopulator populator.DesiredStateOfWorldPopulator // 状态填充器
    // ...其他组件
}
  • desiredStateOfWorld:保存当前节点上所有 Volume 期望的状态
  • actualStateOfWorld:保存当前节点上所有 Volume 实际的状态
  • reconciler:周期性比较两个状态,执行挂载/卸载操作
  • desiredStateOfWorldPopulator:处理节点上的 Pod,更新期望状态
2.2 状态同步机制

reconciler 通过周期性对比状态执行挂载/卸载操作:

func (rc *reconciler) reconcile() {
    if rc.readyToUnmount() {
        rc.unmountVolumes() // 卸载不再需要的卷
    }
    
    rc.mountOrAttachVolumes() // 挂载新卷或处理已挂载卷
    
    if rc.readyToUnmount() {
        rc.unmountDetachDevices() // 卸载设备
        rc.cleanOrphanVolumes()   // 清理孤立卷
    }
    
    // 更新状态同步时间
    if len(rc.volumesNeedUpdateFromNodeStatus) != 0 {
        rc.updateReconstructedFromNodeStatus()
    }
    if len(rc.volumesNeedUpdateFromNodeStatus) == 0 {
        rc.updateLastSyncTime()
    }
}
2.3 卸载流程

遍历 actualStateOfWorld,卸载不再需要的卷:

func (rc *reconciler) unmountVolumes() {
    for _, mountedVolume := range rc.actualStateOfWorld.GetAllMountedVolumes() {
        // 检查是否有未完成的操作
        if rc.operationExecutor.IsOperationPending(mountedVolume.VolumeName, mountedVolume.PodName, nestedpendingoperations.EmptyNodeName) {
            continue
        }
        
        // 如果卷不在期望状态中,执行卸载
        if !rc.desiredStateOfWorld.PodExistsInVolume(mountedVolume.PodName, mountedVolume.VolumeName, mountedVolume.SELinuxMountContext) {
            err := rc.operationExecutor.UnmountVolume(
                mountedVolume.MountedVolume, rc.actualStateOfWorld, rc.kubeletPodsDir)
            if err != nil {
                klog.ErrorS(err, "UnmountVolume failed")
            }
        }
    }
}
2.4 挂载流程

遍历 desiredStateOfWorld,挂载新卷或处理需要更新的卷:

func (rc *reconciler) mountOrAttachVolumes() {
    for _, volumeToMount := range rc.desiredStateOfWorld.GetVolumesToMount() {
        // 检查是否有未完成的操作
        if rc.operationExecutor.IsOperationPending(volumeToMount.VolumeName, nestedpendingoperations.EmptyUniquePodName, nestedpendingoperations.EmptyNodeName) {
            continue
        }
        
        // 检查卷状态
        volMounted, devicePath, err := rc.actualStateOfWorld.PodExistsInVolume(
            volumeToMount.PodName, volumeToMount.VolumeName, 
            volumeToMount.DesiredPersistentVolumeSize, volumeToMount.SELinuxLabel)
        
        volumeToMount.DevicePath = devicePath
        
        // 根据不同错误类型执行不同操作
        switch {
        case cache.IsSELinuxMountMismatchError(err):
            // SELinux 上下文不匹配,标记错误
        case cache.IsVolumeNotAttachedError(err):
            // 卷未挂载,等待 Attach
            rc.waitForVolumeAttach(volumeToMount)
        case !volMounted || cache.IsRemountRequiredError(err):
            // 卷未挂载或需要重新挂载
            rc.mountAttachedVolumes(volumeToMount, err)
        case cache.IsFSResizeRequiredError(err):
            // 文件系统需要扩容
            rc.expandVolume(volumeToMount, err.CurrentSize)
        }
    }
}
2.5 实际挂载操作

通过 operationGenerator 执行实际挂载,并更新状态:

func (og *operationGenerator) GenerateMountVolumeFunc(...) volumetypes.GeneratedOperations {
    mountVolumeFunc := func() {
        // 获取卷插件
        volumePlugin, err := og.volumePluginMgr.FindPluginBySpec(volumeToMount.VolumeSpec)
        if err != nil {
            return volumetypes.NewOperationContext(err, nil, migrated)
        }
        
        // 创建挂载器
        volumeMounter, err := volumePlugin.NewMounter(
            volumeToMount.VolumeSpec, volumeToMount.Pod)
        if err != nil {
            return volumetypes.NewOperationContext(err, nil, migrated)
        }
        
        // 等待设备挂载(如果需要)
        devicePath, err := volumeAttacher.WaitForAttach(
            volumeToMount.VolumeSpec, devicePath, volumeToMount.Pod, waitForAttachTimeout)
        if err != nil {
            return volumetypes.NewOperationContext(err, nil, migrated)
        }
        
        // 执行挂载
        mountErr := volumeMounter.SetUp(volume.MounterArgs{...})
        if mountErr != nil {
            return volumetypes.NewOperationContext(mountErr, nil, migrated)
        }
        
        // 扩容文件系统(如果需要)
        if resizeNeeded {
            err = og.expandVolumeDuringMount(volumeToMount, actualStateOfWorld, resizeOptions)
            if err != nil {
                return volumetypes.NewOperationContext(err, nil, migrated)
            }
        }
        
        // 更新实际状态
        markVolMountedErr := actualStateOfWorld.MarkVolumeAsMounted(markOpts)
        if markVolMountedErr != nil {
            return volumetypes.NewOperationContext(markVolMountedErr, nil, migrated)
        }
        
        return volumetypes.NewOperationContext(nil, nil, migrated)
    }
    
    return volumetypes.GeneratedOperations{
        OperationFunc: mountVolumeFunc,
        // ...其他回调函数
    }
}
2.6 实际卸载操作

通过 operationGenerator 执行卸载,并更新状态:

func (og *operationGenerator) GenerateUnmountVolumeFunc(...) {
    unmountVolumeFunc := func() {
        // 获取卸载器
        volumeUnmounter, err := volumePlugin.NewUnmounter(
            volumeToUnmount.InnerVolumeSpecName, volumeToUnmount.PodUID)
        if err != nil {
            return volumetypes.NewOperationContext(err, nil, migrated)
        }
        
        // 清理子路径挂载点
        if err := subpather.CleanSubPaths(podDir, volumeToUnmount.InnerVolumeSpecName); err != nil {
            return volumetypes.NewOperationContext(err, nil, migrated)
        }
        
        // 执行卸载
        unmountErr := volumeUnmounter.TearDown()
        if unmountErr != nil {
            // 标记卷状态为不确定
            actualStateOfWorld.MarkVolumeMountAsUncertain(opts)
            return volumetypes.NewOperationContext(unmountErr, nil, migrated)
        }
        
        // 更新实际状态
        actualStateOfWorld.MarkVolumeAsUnmounted(
            volumeToUnmount.PodName, volumeToUnmount.VolumeName)
            
        return volumetypes.NewOperationContext(nil, nil, migrated)
    }
    
    return volumetypes.GeneratedOperations{
        OperationFunc: unmountVolumeFunc,
        // ...其他回调函数
    }
}
2.7 期望状态更新机制

desiredStateOfWorldPopulator 周期性处理 Pod,更新期望状态:

func (dswp *desiredStateOfWorldPopulator) Run(ctx context.Context, sourcesReady config.SourcesReady) {
    // 周期性执行状态填充
    wait.UntilWithContext(ctx, dswp.populatorLoop, dswp.loopSleepDuration)
}

func (dswp *desiredStateOfWorldPopulator) processPodVolumes(ctx context.Context, pod *v1.Pod) {
    for _, podVolume := range pod.Spec.Volumes {
        // 将 Pod 的卷添加到期望状态
        uniqueVolumeName, err := dswp.desiredStateOfWorld.AddPodToVolume(
            uniquePodName, pod, volumeSpec, podVolume.Name, volumeGIDValue, seLinuxContainerContexts[podVolume.Name])
        if err != nil {
            klog.ErrorS(err, "Failed to add pod to volume")
        }
    }
}
2.8 关键数据结构
  • volumesToMount:记录需要挂载的卷
    type volumeToMount struct {
        volumeName                     v1.UniqueVolumeName
        podsToMount                    map[types.UniquePodName]podToMount
        pluginIsAttachable             bool
        pluginIsDeviceMountable        bool
        volumeGIDValue                 string
        desiredSizeLimit               *resource.Quantity
        effectiveSELinuxMountFileLabel string
        // ...其他属性
    }
    

Mount 流程总结

  1. 状态初始化desiredStateOfWorldPopulator 从 Pod 中收集卷信息,更新期望状态
  2. 状态对比reconciler 周期性比较期望状态和实际状态
  3. 卸载操作:对不再需要的卷执行卸载
  4. 挂载操作:对新增卷或需要更新的卷执行挂载
  5. 状态更新:挂载/卸载成功后更新实际状态
  6. 错误处理:处理挂载/卸载过程中的各种异常情况

通过这种双缓存、周期性同步的机制,Kubernetes 确保了节点上卷的状态始终与期望状态一致。

3. 协作关系

存储卷生命周期:
创建PV/PVC → Attach(设备挂载到宿主机) → Mount(文件系统挂载到容器) → 
Unmount(从容器卸载) → Detach(从宿主机卸载) → 删除PV/PVC

三、常见存储类型的 Attach/Mount 差异

存储类型 Attach 操作 Mount 操作
EBS(块存储) 将 EBS 卷挂载到 EC2 实例 在实例上格式化并挂载文件系统(如 ext4)
NFS(网络存储) 建立网络连接(无需显式 Attach) 通过 NFS 客户端挂载远程文件系统
HostPath(宿主机路径) 无(已在宿主机上) 直接将宿主机路径挂载到容器
Ceph RBD 将 RBD 设备映射到宿主机 在宿主机上挂载 RBD 设备为文件系统

网站公告

今日签到

点亮在社区的每一天
去签到