当前位置: 首页 > news >正文

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方法等待所有卷挂载完成,校验逻辑如下:

  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.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
    }
    
  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驱动名称和卷句柄完全相同的存储卷。若存在,及时提示用户,避免因重复挂载相同卷导致的启动失败。

http://www.dtcms.com/a/325640.html

相关文章:

  • 2025面试题——(12)
  • Vue3从入门到精通:3.1 性能优化策略深度解析
  • 思科交换机的不同级别IOS软件有什么区别?
  • android 换肤框架详解1-换肤逻辑基本
  • R语言机器学习算法实战系列(二十七)LASSO 与 Adaptive LASSO 在特征选择中的比较与应用
  • 为什么TEXT不区分大小写,而BLOB严格区分?
  • 剑桥大学最新研究:基于大语言模型(LLM)的分子动力学模拟框架,是MD的GPT时刻还是概念包装?
  • Selenium竞品价格监控爬虫(代理防封版)
  • C语言模拟 MCU 上电后程序的执行顺序 + 回调函数机制 + 程序计数器(PC)和堆栈的作用
  • PID 控制算法 | stm32 直流电机控制
  • 从零开始的云计算生活——项目实战容器化
  • 当生产环境卡成 PPT:Spring Boot 线程 Dump 捉妖指南 - 第544篇
  • AI入门学习--如何写好prompt?
  • STM32学习笔记7-TIM输入捕获模式
  • 1000w小时语音数据!语音模型Higgs Audio V2情感能力跃迁;MathCaptcha10k提升验证码识别技术
  • 主DNS部署+辅助DNS服务器部署
  • 嵌入式学习(Day24)fread/fwrite
  • 【华为机试】208. 实现 Trie (前缀树)
  • 钓鱼鱼饵制作的方式
  • 【项目测试】:问卷考试系统项目测试报告
  • FlinkSql(详细讲解一)
  • C#中如何运用JWT用户认证
  • AT24C02C-SSHM-T用法
  • 什么情况下会导致日本服务器变慢?解决办法
  • 系统编程——消息队列
  • 前端实现 MD5 + AES 加密的安全登录请求
  • Nacos-1--什么是Nacos?
  • 疫情可视化:基孔肯雅热风险地图实战解析
  • Dubbo从入门到实战:分布式服务开发指南
  • WPF之绑定!