k8s之Attach 和 Mount

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. 详细执行流程
ADControllerVolumeAttachmentExternalAttacherCSIDriverCloudProviderNode创建 VolumeAttachment 资源监听资源变化调用 ControllerPublishVolume(PV, Node)调用云 API(如 AWS EBS Attach)返回设备 ID(如 /dev/xvdf)返回成功更新 status.attached=true更新 status.volumesAttachedADControllerVolumeAttachmentExternalAttacherCSIDriverCloudProviderNode

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 设备为文件系统
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值