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 操作:
- 监听 Pod 调度:当 Pod 被调度到特定节点时,AD Controller 获取该节点上所有 Pod 的 Volume 列表。
- 计算待 Attach 卷:对比当前节点的
status.volumesAttached
与 Pod 需要的卷,找出未 Attach 的 PV。 - 多节点挂载检查:对
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. 详细执行流程
2. Mount 流程
2.1 核心组件与数据结构
Kubernetes 的 Mount 流程由 kubelet
的 volumeManager
组件管理,主要包含以下核心元素:
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):// 卷未挂载,等待 Attachrc.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.UniqueVolumeNamepodsToMount map[types.UniquePodName]podToMountpluginIsAttachable boolpluginIsDeviceMountable boolvolumeGIDValue stringdesiredSizeLimit *resource.QuantityeffectiveSELinuxMountFileLabel string// ...其他属性 }
Mount 流程总结
- 状态初始化:
desiredStateOfWorldPopulator
从 Pod 中收集卷信息,更新期望状态 - 状态对比:
reconciler
周期性比较期望状态和实际状态 - 卸载操作:对不再需要的卷执行卸载
- 挂载操作:对新增卷或需要更新的卷执行挂载
- 状态更新:挂载/卸载成功后更新实际状态
- 错误处理:处理挂载/卸载过程中的各种异常情况
通过这种双缓存、周期性同步的机制,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 设备为文件系统 |