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 設備為文件系統 |