k8s之CSI 卷挂载问题:同一Pod中挂载多个相同远程存储的隐含限制
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
方法等待所有卷挂载完成,校验逻辑如下:
-
遍历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.SELinuxOptionsif 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 }
-
验证每个声明的卷是否已在
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驱动名称和卷句柄完全相同的存储卷。若存在,及时提示用户,避免因重复挂载相同卷导致的启动失败。