Pod 的状态出现 UnexpectedAdmissionError 是什么鬼?

本文详细记录了一次排查 Kubernetes 集群中 pod 出现 UnexpectedAdmissionError 状态的过程。问题源于集群中同时存在默认调度器和自定义调度器,导致 GPU 资源竞争。在特定情况下,两个调度器可能会同时调度到资源不足的节点上,从而引发该错误。解决方案是确保同一资源的 pod 使用相同的调度器。此外,文章还介绍了 kubelet 如何管理 device-plugin 资源,以及 device 分配的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

37c60a357a5870f02dc273dbf9a5fabf.gif

今天在排查集群一个问题时,发现相关的 pod 的状态为UnexpectedAdmissionError,在这之前从未没遇到过 pod 还有这种状态的,一脸好奇,在解决问题的过程中,发现越挖越深, 里面涉及到的信息也是相当的多,特此记录一下。

集群信息

k8s版本为K8s v1.15.5, 3 master + N node的形式,由于业务特殊,集群中同时存在的nodeamd64arm64两种异构节点,但这些都不重要,重要的一点是,集群中同时存在 2 种scheduler:

  1. default scheduler: 这个不用多说,k8s默认的调度器,本质上来说,是个串行的调度器

  2. x-scheduler: 自定义调度器, 用于批量去调度资源,如果有任一请求的资源不满足,其它的资源也不会调度,pod 处于一直 Pending 状态,直到资源都满足

  3. 在不同的业务中会使用不现的调度器,以实现资源的合理分配,记住,集群中有两个调度器, 这个是本次问题的关键

同时还要说明一点的是,这次引起问题的资源是nvidia.com/gpu,涉及到kubelet 对于 device 是如何管理的,这部分是本次的重点内容

下述出现的资源,device,其实是一个意思,资源可能在日常中使用的较多,而官方都把资源比较是一种device

慢慢道来…

问题现象

问题现象就是工作流(这里不展开细说,简单理解就是运行完就销毁的pod吧)的pod出现了如下图的状态

417311f4941dfd50d83c6c12214090db.png

在作者多年丰富的工作(自吹)经验中,pod的状态从未出现UnexpectedAdmissionError这种状态,甚至作者都不知道除了常见的那几种状态外还有其它的状态,看到这作者还是有点高兴的,因为作者知道,嗯,应该会挖出个盲区

另外,工作流控制器返回的信息为:

Pod Update plugin resources failed due to  requested number of  devices unavailable for nvidia.com/gpu. Requested: 1, Available: 0,  which is unexpected

该报错在kubelet的日志中也有出现

另外,经过作者的多次尝试,发现上述问题并不总是存在,也有成功的时候

排查过程

看到上述的报错message,翻译一下:

pod 更新 Plugin 资源失败, 失败的原因是由于请求的 nvidia.com/gpu 资源需要 1 个,但现在只有 0 个

报错意思很明显,但作者第一时间想到的是以下 3 个问题:

  1. 正常来讲不会出现因为资源不够而调度的情况,如果资源不够,是不应该被调度的,但从上面的报错来看,已经过了调度那个环节,因为已经配置到了node

  2. 如果忽略问题 1,就算是资源不够,也应该是Pending状态,而不应该是UnexpectedAdmissionError这种状态

  3. pod去更新 plugin 的什么资源,为什么需要更新?

问题 1 跟问题 2 应该是同一个问题,就放一起排查吧

作者的猜测: 由于集群中同时存在 2 种调度器,对于同一device(比如nvidia.com/gpu)就可能会发生竞争关系,比如以下场景:

某一nodenvidia.com/gpu资源只有 1 个

  • Pod1,使用了default-scheduler,消耗了nvidia.com/gpu=1,此时,nvidia.com/gpu为 0

  • Pod2,使用了default-scheduler,请求了nvidia.com/gpu=1,状态`Pending

  • Pod3,使用了x-scheduler,请求了nvidia.com/gpu=1,状态`Pending

在特定的场景下,存在pod2,pod3都调度到这个node上的可能,那么在pod1执行完成之后,就有可能出现default-schedulerx-scheduler同时拿到nvidia.com/gpu=1,对Pod2Pod3Pending状态唤醒,但是在接下来的某一时候,只会有一个Pod创建成功,另一个Pod就有可能会出现UnexpectedAdmissionError

如果这样的 Pod 越多的话,发生的可能性就越大

因为是race,所以也不一定总是会发生,这也解释了【问题现】中提到的问题并不总是存在的问题

从后续的排查情况来看,也证实了上述猜想,开发侧对调度器引用不当,对于nvidia.com/gpu的资源,应该使用x-scheduler,但有些被调整成了default-scheduler也就是说,如果上面的场景,所有请求同一资源的pod,使用相同的scheduler那么就不会出现这种问题,因为相同的scheduler内部的加锁机制是相同的.

解决也很简单,对同一资源的请求,schedulerName设成一致即可

问题虽然解决,但依然没有解决很多疑惑,比如:

  1. UnexpectedAdmissionError状态怎么来的

  2. pod 去更新 plugin 的什么资源,什么时候更新等?

没办法,只能去查源码了

源码分析

首先,从kubelet中看到了相关的报错信息,那么就从kubelet开始吧, 由于环境中的kubelet的日志级别不高,先调整成--v 4,代表debug日志,发现以下日志:

6e4564b80d77af7e5349de152241f927.png

结合kubelet侧关于cm(containermanager的缩写)代码,大体的调用过程: scheduler(predicate.go) --> kubelet(predicate.go) --> manager.go

predictate.go[1]中也确认存在UnexpectedAdmissionError

func (w *predicateAdmitHandler) Admit(attrs *PodAdmitAttributes) PodAdmitResult {
 node, err := w.getNodeAnyWayFunc()
 if err != nil {
  klog.Errorf("Cannot get Node info: %v", err)
  return PodAdmitResult{
   Admit:   false,
   Reason:  "InvalidNodeInfo",
   Message: "Kubelet cannot get node info.",
  }
 }
 admitPod := attrs.Pod
 pods := attrs.OtherPods
 nodeInfo := schedulernodeinfo.NewNodeInfo(pods...)
 nodeInfo.SetNode(node)
 // ensure the node has enough plugin resources for that required in pods
 if err = w.pluginResourceUpdateFunc(nodeInfo, attrs); err != nil {
  message := fmt.Sprintf("Update plugin resources failed due to %v, which is unexpected.", err)
  klog.Warningf("Failed to admit pod %v - %s", format.Pod(admitPod), message)
  return PodAdmitResult{
   Admit:   false,
   Reason:  "UnexpectedAdmissionError",
   Message: message,
  }
 }
 // 省略代码 ...

跟上面kubelet中打印出来的日志是吻合的,经过摸排发现,调用路径主要集中在manager.go[2]如下:

Allocate --> allocatePodResources --> allocateContainerResources --> devicesToAllocate

最终在devicesToAllocate中报出requested number of devices unavailable for的错误一直按上述路径返向传回给Allocate,也就是上图中红色的部分

同时又可以知道,predictate一般属于调度相关,因此,应该是从 scheduler 传过来的, 从 Allocate 函数定义就可以看出

func (m *ManagerImpl) Allocate(node *schedulernodeinfo.NodeInfo, attrs *lifecycle.PodAdmitAttributes)

再根据node *schedulernodeinfo.NodeInfo就可一层层追到scheduler的代码中,由于篇幅原因,就不在这里贴了

Allocate()方法作用是根据scheduler传来的条件为某 pod 分配device,而device则是根据resource.limit做为条件进行计算

func (m *ManagerImpl) allocateContainerResources(pod *v1.Pod, container *v1.Container, devicesToReuse map[string]sets.String) error {
 podUID := string(pod.UID)
 contName := container.Name
 allocatedDevicesUpdated := false
 for k, v := range container.Resources.Limits { //根据limit进行计算
  resource := string(k)
  needed := int(v.Value())
  klog.V(3).Infof("needs %d %s", needed, resource) //这行在kubelet代码中也出现过
  if !m.isDevicePluginResource(resource) {
   continue
  }
    // 省略代码 ...
  }

这里面有个有意思的对象:deviceToReuse,可重用的设备, Allocate调了的allocatePodResources

func (m *ManagerImpl) allocatePodResources(pod *v1.Pod) error {
 devicesToReuse := make(map[string]sets.String)
 for _, container := range pod.Spec.InitContainers {
  if err := m.allocateContainerResources(pod, &container, devicesToReuse); err != nil {
   return err
  }
    // 对于initContainer,将所分配的device不断地加入到可重用设置列表中,以便提供给container使用
  m.podDevices.addContainerAllocatedResources(string(pod.UID), container.Name, devicesToReuse)
 }
 for _, container := range pod.Spec.Containers {
  if err := m.allocateContainerResources(pod, &container, devicesToReuse); err != nil {
   return err
  }
    // 而对于container,则不断地从可重用设置列表中将分配出去的设备删除
  m.podDevices.removeContainerAllocatedResources(string(pod.UID), container.Name, devicesToReuse)
 }
 return nil
}

原因是k8s中有initContainerinitContainer可以有多个,先于container执行,每个initContainer按顺序依次执行完毕后container才会开始创建,而在为containerinitContainer分配设备的时候会优先利用deviceToReuse的设备,这样可避免资源浪费

还有一些比较重要的功能,比如:

updateAllocatedDevices函数的功能是从podDevices中删除所有处于终结状态的pod,并回收其占用的资源,所以有时会在kubelet的日志中看到pods to be removed:xxxx字样

devicesToAllocate用来生成需要向plugin请求的设备列表,如果可重用设备已经够用或者没有设备需求时则不向plugin请求分配新的设备,否则调用grpcplugin申请分配新的设备。设备分配的逻辑是首先看container中是否已经分配了设备,如果设备够用则返回nil,否则查看reusableDevices,取出里面的设备分配,否则根据最终缺少的设备量返回healthdevice - inusedevice(m.allocatedDevices[resource]),中的前needed个,这便是其分配设备的策略

func (m *ManagerImpl) devicesToAllocate(podUID, contName, resource string, required int, reusableDevices sets.String) (sets.String, error) {
  // 省略代码...
 // Gets Devices in use.
 devicesInUse := m.allocatedDevices[resource]
 // Gets a list of available devices.
 available := m.healthyDevices[resource].Difference(devicesInUse)
 if available.Len() < needed {
  return nil, fmt.Errorf("requested number of devices unavailable for %s. Requested: %d, Available: %d", resource, needed, available.Len())
 }
  // 省略代码...

因此,报错的最终原因也是在这里,因为 pod 此时已经分配到了 node 上,但 node 上的可用 device 小于 pod 申请的 device,导致在启动containerpredicate.go报错返回

devicepredicate过程会执行二次,第一次是对schedulernode进行筛选的时候,第二次kubeletcontainer启动之前会再次进行device的确认,而上述报错则是出现在kubelet

最后除一张牛人的manager.go中代码调用图吧,非常清晰,原图地址[3]在这里

ce5b924f43e85f96afb3b3ef60a068a1.png

到这里,其实第 2 个问题还是没有讲的很清楚,即kubelet 怎么去管理 devie-plugin 资源,device-plugin 注册、跟 api-server 同步等

这个主要涉及到kubelet是如何管理device的,即device-plugin是实现原理,做为下次作业吧

参考文章

  • http://www.dockone.io/article/8653

  • https://www.kubernetes.org.cn/4391.html

  • https://www.cnblogs.com/oolo/p/11672720.html#dm-%E8%B0%83%E7%94%A8-dp-listandwatch-%E7%9A%84%E6%97%B6%E6%9C%BA

  • https://github.com/kubernetes/kubernetes/issues/60176

  • https://blog.youkuaiyun.com/s812289480/article/details/84314239

  • https://zwforrest.github.io/post/devicemanager%E5%8E%9F%E7%90%86%E5%8F%8A%E5%88%86%E6%9E%90/#allocate%E5%88%86%E9%85%8D%E8%B5%84%E6%BA%90

  • https://github.com/kubernetes-sigs/kube-batch/issues/931

  • https://sourcegraph.com/github.com/kubernetes/kubernetes/-/blob/pkg/kubelet/lifecycle/predicate.go

  • https://github.com/kubernetes/kubernetes/blob/v1.15.9/pkg/kubelet/cm/devicemanager/manager.go

引用链接

[1]

predictate.go: https://github.com/kubernetes/kubernetes/blob/v1.15.5/pkg/kubelet/lifecycle/predicate.go

[2]

manager.go: https://github.com/kubernetes/kubernetes/blob/v1.15.5/pkg/kubelet/cm/devicemanager/manager.go

[3]

原图地址: https://blog.youkuaiyun.com/s812289480/article/details/84314239

原文链接:https://izsk.me/2022/01/27/Kubernetes-pod-status-is-UnexpectedAdmissionError/

928cc01522d58b587f11c3eaf218d067.gif

d167eb7c72925e2fb7cb1c9f1af667d4.png

你可能还喜欢

点击下方图片即可阅读

8dbe68812a434180cecd756e08910742.png

记一次 Kubernetes 中严重的安全问题

018ab1546ef081826ab19873956ee849.gif

云原生是一种信仰 🤘

关注公众号

后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!

68f9a530bf56be9e800e9246c2a3ed8d.gif

483908fd8badb5f758dbe2290a06f27e.gif

点击 "阅读原文" 获取更好的阅读体验!

发现朋友圈变“安静”了吗?

f6e9815350600221d9410a3c98c05f18.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值