今天在排查集群一个问题时,发现相关的 pod 的状态为UnexpectedAdmissionError
,在这之前从未没遇到过 pod 还有这种状态的,一脸好奇,在解决问题的过程中,发现越挖越深, 里面涉及到的信息也是相当的多,特此记录一下。
集群信息
k8s
版本为K8s v1.15.5, 3 master + N node
的形式,由于业务特殊,集群中同时存在的node
有amd64
及arm64
两种异构节点,但这些都不重要,重要的一点是,集群中同时存在 2 种scheduler
:
default scheduler
: 这个不用多说,k8s
默认的调度器,本质上来说,是个串行的调度器x-scheduler
: 自定义调度器, 用于批量去调度资源,如果有任一请求的资源不满足,其它的资源也不会调度,pod 处于一直 Pending 状态,直到资源都满足在不同的业务中会使用不现的调度器,以实现资源的合理分配,记住,集群中有两个调度器, 这个是本次问题的关键
同时还要说明一点的是,这次引起问题的资源是nvidia.com/gpu
,涉及到kubelet 对于 device 是如何管理的,这部分是本次的重点内容
下述出现的资源,device,其实是一个意思,资源可能在日常中使用的较多,而官方都把资源比较是一种device
慢慢道来…
问题现象
问题现象就是工作流(这里不展开细说,简单理解就是运行完就销毁的pod
吧)的pod
出现了如下图的状态

在作者多年丰富的工作(自吹)经验中,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 个问题:
正常来讲不会出现因为资源不够而调度的情况,如果资源不够,是不应该被调度的,但从上面的报错来看,已经过了调度那个环节,因为已经配置到了
node
上如果忽略问题 1,就算是资源不够,也应该是
Pending
状态,而不应该是UnexpectedAdmissionError
这种状态pod
去更新 plugin 的什么资源,为什么需要更新?
问题 1 跟问题 2 应该是同一个问题,就放一起排查吧
作者的猜测: 由于集群中同时存在 2 种调度器,对于同一device
(比如nvidia.com/gpu
)就可能会发生竞争关系,比如以下场景:
某一node
上nvidia.com/gpu
资源只有 1 个
Pod1
,使用了default-scheduler
,消耗了nvidia.com/gpu=1
,此时,nvidia.com/gpu
为 0Pod2
,使用了default-scheduler
,请求了nvidia.com/gpu=1
,状态`PendingPod3
,使用了x-scheduler
,请求了nvidia.com/gpu=1
,状态`Pending
在特定的场景下,存在pod2
,pod3
都调度到这个node
上的可能,那么在pod1
执行完成之后,就有可能出现default-scheduler
与x-scheduler
同时拿到nvidia.com/gpu=1
,对Pod2
及Pod3
从Pending
状态唤醒,但是在接下来的某一时候,只会有一个Pod
创建成功,另一个Pod
就有可能会出现UnexpectedAdmissionError
如果这样的 Pod 越多的话,发生的可能性就越大
因为是race
,所以也不一定总是会发生,这也解释了【问题现】中提到的问题并不总是存在的问题
从后续的排查情况来看,也证实了上述猜想,开发侧对调度器引用不当,对于nvidia.com/gpu
的资源,应该使用x-scheduler
,但有些被调整成了default-scheduler
也就是说,如果上面的场景,所有请求同一资源的pod
,使用相同的scheduler
那么就不会出现这种问题,因为相同的scheduler
内部的加锁机制是相同的.
解决也很简单,对同一资源的请求,schedulerName
设成一致即可
问题虽然解决,但依然没有解决很多疑惑,比如:
UnexpectedAdmissionError
状态怎么来的pod 去更新 plugin 的什么资源,什么时候更新等?
没办法,只能去查源码了
源码分析
首先,从kubelet
中看到了相关的报错信息,那么就从kubelet
开始吧, 由于环境中的kubelet
的日志级别不高,先调整成--v 4
,代表debug
日志,发现以下日志:

结合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
中有initContainer
,initContainer
可以有多个,先于container
执行,每个initContainer
按顺序依次执行完毕后container
才会开始创建,而在为container
或initContainer
分配设备的时候会优先利用deviceToReuse
的设备,这样可避免资源浪费
还有一些比较重要的功能,比如:
updateAllocatedDevices
函数的功能是从podDevices
中删除所有处于终结状态的pod
,并回收其占用的资源,所以有时会在kubelet
的日志中看到pods to be removed:xxxx
字样
devicesToAllocate
用来生成需要向plugin
请求的设备列表,如果可重用设备已经够用或者没有设备需求时则不向plugin
请求分配新的设备,否则调用grpc
向plugin
申请分配新的设备。设备分配的逻辑是首先看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,导致在启动container
时predicate.go
报错返回
device
的predicate
过程会执行二次,第一次是对scheduler
对node
进行筛选的时候,第二次kubelet
在container
启动之前会再次进行device
的确认,而上述报错则是出现在kubelet
。
最后除一张牛人的manager.go
中代码调用图吧,非常清晰,原图地址[3]在这里

到这里,其实第 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/
你可能还喜欢
点击下方图片即可阅读
云原生是一种信仰 🤘
关注公众号
后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!
点击 "阅读原文" 获取更好的阅读体验!
发现朋友圈变“安静”了吗?