k8s之CSI 卷挂载问题:同一Pod中挂载多个相同远程存储的隐含限制

发布于:2025-08-13 ⋅ 阅读:(14) ⋅ 点赞:(0)

CSI 卷挂载问题:同一Pod中挂载多个相同远程存储的隐含限制

一、现象和报错

当一个任务(Pod)挂载两个或多个完全一致的远程存储时,容器会始终处于ContainerCreating状态,且有一个卷无法挂载成功。

kubelet报错日志

Jul 10 10:09:16 k8s-master01 kubelet[1646910]: E0710 10:09:16.843808 1646910 pod_workers.go:965] "Error syncing pod, skipping" err="Unable to attach or mount volumes: unmounted volumes=[volume-39b0ab9e49ed658010cd8b623db37f10], unattached volumes=[public-keys volume-66937ec53176efa32440bcd7bfb42413 volume-39b0ab9e49ed658010cd8b623db37f10 kube-api-access-dwbrw test-1-script-file]: timed out waiting for the condition" pod="notebook/test-1-5958f5ff7b-zw4sn"

二、k8s代码矛盾之处分析

问题现象

当一个Pod包含两个CSI卷(volume1和volume2),且这两个卷的CSI驱动名称(pluginName)卷句柄(volumeHandle) 完全相同时,会出现:

  • 实际仅成功挂载一个卷;
  • 但系统校验时仍要求两个卷都挂载;
  • 最终导致Pod一直卡在ContainerCreating状态。

根本原因:Kubernetes卷识别机制和校验冲突

Kubernetes卷识别机制
  • 卷名称生成规则:对于CSI卷,Kubernetes通过GetUniqueVolumeNameFromSpec方法生成唯一卷名,格式为:
    "kubernetes.io/csi/<驱动名称>^<卷句柄>"
    示例:"kubernetes.io/csi/nfs.csi.k8s.io^11.127.229.164:2049#/volume1#data/wangtao714/train/tensorflow/code##"

  • 关键代码(CSI插件生成卷名的方法)

    func (p *csiPlugin) GetVolumeName(spec *volume.Spec) (string, error) {
        csi, err := getPVSourceFromSpec(spec)
        if err != nil {
            return "", err
        }
        // 关键点:仅使用 Driver 和 VolumeHandle 组合作为唯一标识
        return fmt.Sprintf("%s%s%s", csi.Driver, volNameSep, csi.VolumeHandle), nil
    }
    
    // 最终生成的唯一卷名格式
    func GetUniqueVolumeName(pluginName, volumeName string) v1.UniqueVolumeName {
        return v1.UniqueVolumeName(fmt.Sprintf("%s/%s", pluginName, volumeName))
    }
    // 示例:"kubernetes.io/csi/nfs.csi.k8s.io^nfs.csi.k8s.io^11.127.229.164:2049#/volume1#data/wangtao714/train/tensorflow/code##"
    
冲突产生条件

当同一个Pod中的两个CSI卷满足以下条件时,会产生冲突:

  • 相同的CSI驱动名称(pluginName);
  • 相同的卷句柄(volumeHandle)。

此时,两个卷会生成完全相同的唯一卷名

系统处理逻辑

Kubernetes的desiredStateOfWorld数据结构会认为这两个卷是同一个卷(因唯一卷名相同),最终volumesToMount中只会记录一个卷条目,仅挂载一个volume。

关键代码(卷条目处理逻辑):

func (dsw *desiredStateOfWorld) AddPodToVolume(
    podName types.UniquePodName,
    pod *v1.Pod,
    volumeSpec *volume.Spec,
    outerVolumeSpecName string,
    volumeGIDValue string,
    seLinuxContainerContexts []*v1.SELinuxOptions) (v1.UniqueVolumeName, error) {
    
    // 关键判断逻辑:相同 volumeName 的卷只会保留一个条目
    if _, volumeExists := dsw.volumesToMount[volumeName]; !volumeExists {
        vmt := volumeToMount{
            volumeName: volumeName,
            podsToMount: make(map[types.UniquePodName]podToMount),
            // ...其他字段
        }
        dsw.volumesToMount[volumeName] = vmt
    }
}
校验流程

系统通过WaitForAttachAndMount方法等待所有卷挂载完成,校验逻辑如下:

  1. 遍历Pod中所有容器和初始化容器的volumeMounts(通过getExpectedVolumes获取期望挂载的卷列表);

    func getExpectedVolumes(pod *v1.Pod) []string {
    	mounts, devices, _ := util.GetPodVolumeNames(pod, false /* collectSELinuxOptions */)
    	return mounts.Union(devices).UnsortedList()
    }
    
    func GetPodVolumeNames(pod *v1.Pod, collectSELinuxOptions bool) (mounts sets.Set[string], devices sets.Set[string], seLinuxContainerContexts map[string][]*v1.SELinuxOptions) {
    	mounts = sets.New[string]()
    	devices = sets.New[string]()
    	seLinuxContainerContexts = make(map[string][]*v1.SELinuxOptions)
    
    	podutil.VisitContainers(&pod.Spec, podutil.AllFeatureEnabledContainers(), func(container *v1.Container, containerType podutil.ContainerType) bool {
    		var seLinuxOptions *v1.SELinuxOptions
    		if collectSELinuxOptions {
    			effectiveContainerSecurity := securitycontext.DetermineEffectiveSecurityContext(pod, container)
    			if effectiveContainerSecurity != nil {
    				seLinuxOptions = effectiveContainerSecurity.SELinuxOptions
    			}
    		}
    
    		if container.VolumeMounts != nil {
    			for _, mount := range container.VolumeMounts {
    				mounts.Insert(mount.Name)
    				if seLinuxOptions != nil && collectSELinuxOptions {
    					seLinuxContainerContexts[mount.Name] = append(seLinuxContainerContexts[mount.Name], seLinuxOptions.DeepCopy())
    				}
    			}
    		}
    		if container.VolumeDevices != nil {
    			for _, device := range container.VolumeDevices {
    				devices.Insert(device.Name)
    			}
    		}
    		return true
    	})
    	return
    }
    
  2. 验证每个声明的卷是否已在actualStateOfWorld中挂载;

    func (vm *volumeManager) verifyVolumesMountedFunc(podName types.UniquePodName, expectedVolumes []string) wait.ConditionWithContextFunc {
    	return func(_ context.Context) (done bool, err error) {
    		if errs := vm.desiredStateOfWorld.PopPodErrors(podName); len(errs) > 0 {
    			return true, errors.New(strings.Join(errs, "; "))
    		}
    		for _, expectedVolume := range expectedVolumes {
    			_, found := vm.actualStateOfWorld.GetMountedVolumeForPodByOuterVolumeSpecName(podName, expectedVolume)
    			if !found {
    				return false, nil
    			}
    		}
    		return true, nil
    	}
    }
    
    func (asw *actualStateOfWorld) GetMountedVolumeForPodByOuterVolumeSpecName(
    	podName volumetypes.UniquePodName, outerVolumeSpecName string) (MountedVolume, bool) {
    	asw.RLock()
    	defer asw.RUnlock()
    	for _, volumeObj := range asw.attachedVolumes {
    		if podObj, hasPod := volumeObj.mountedPods[podName]; hasPod {
    			if podObj.volumeMountStateForPod == operationexecutor.VolumeMounted && podObj.outerVolumeSpecName == outerVolumeSpecName {
    				return getMountedVolume(&podObj, &volumeObj), true
    			}
    		}
    	}
    
    	return MountedVolume{}, false
    }
    
冲突结果

系统认为只需要挂载一个卷(因卷名相同),但Pod声明需要挂载两个卷,导致校验不通过,Pod一直卡在ContainerCreating状态。

三、结论和建议

结论

同一Pod中挂载两个或多个CSI驱动名称和卷句柄完全相同的CSI卷时,会因Kubernetes卷识别机制与校验逻辑的冲突,导致卷挂载失败,Pod无法正常启动。

建议

在创建任务(Pod)时,增加校验逻辑:检查是否存在CSI驱动名称和卷句柄完全相同的存储卷。若存在,及时提示用户,避免因重复挂载相同卷导致的启动失败。


网站公告

今日签到

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