一個(gè)Pod調(diào)度失敗后重新觸發(fā)調(diào)度的所有情況分析
正文
在 k8s 中一個(gè)Pod由于某些原因調(diào)度失敗后,會被放入調(diào)度失敗隊(duì)列,這個(gè)隊(duì)列里面的Pod后面都怎么樣了呢?
他們怎么樣才能重新獲取到”重新做人的機(jī)會“呢?這篇文章,我們從源碼的角度來看看來龍去脈
在 k8s 中會起兩個(gè)協(xié)程,定期把 backoffQ 和 unscheduledQ 里面的 Pod拿到activeQ里面去
func (p *PriorityQueue) Run() { go wait.Until(p.flushBackoffQCompleted, 1.0*time.Second, p.stop) go wait.Until(p.flushUnschedulablePodsLeftover, 30*time.Second, p.stop) }
flushUnschedulablePodsLeftover
func (p *PriorityQueue) flushUnschedulablePodsLeftover() { p.lock.Lock() defer p.lock.Unlock() var podsToMove []*framework.QueuedPodInfo currentTime := p.clock.Now() for _, pInfo := range p.unschedulablePods.podInfoMap { lastScheduleTime := pInfo.Timestamp if currentTime.Sub(lastScheduleTime) > p.podMaxInUnschedulablePodsDuration { podsToMove = append(podsToMove, pInfo) } } if len(podsToMove) > 0 { p.movePodsToActiveOrBackoffQueue(podsToMove, UnschedulableTimeout) } }
func (p *PriorityQueue) movePodsToActiveOrBackoffQueue(podInfoList []*framework.QueuedPodInfo, event framework.ClusterEvent) { activated := false for _, pInfo := range podInfoList { // If the event doesn't help making the Pod schedulable, continue. // Note: we don't run the check if pInfo.UnschedulablePlugins is nil, which denotes // either there is some abnormal error, or scheduling the pod failed by plugins other than PreFilter, Filter and Permit. // In that case, it's desired to move it anyways. if len(pInfo.UnschedulablePlugins) != 0 && !p.podMatchesEvent(pInfo, event) { continue } pod := pInfo.Pod if p.isPodBackingoff(pInfo) { if err := p.podBackoffQ.Add(pInfo); err != nil { klog.ErrorS(err, "Error adding pod to the backoff queue", "pod", klog.KObj(pod)) } else { metrics.SchedulerQueueIncomingPods.WithLabelValues("backoff", event.Label).Inc() p.unschedulablePods.delete(pod) } } else { if err := p.activeQ.Add(pInfo); err != nil { klog.ErrorS(err, "Error adding pod to the scheduling queue", "pod", klog.KObj(pod)) } else { metrics.SchedulerQueueIncomingPods.WithLabelValues("active", event.Label).Inc() p.unschedulablePods.delete(pod) } } } p.moveRequestCycle = p.schedulingCycle if activated { p.cond.Broadcast() } }
將在unscheduledQ里面停留時(shí)長超過podMaxInUnschedulablePodsDuration(默認(rèn)是5min)的pod放入到 ActiveQ 或 BackoffQueue,具體是放到哪個(gè)隊(duì)列里面,根據(jù)下面規(guī)則判斷:
- 根據(jù)這個(gè)Pod嘗試被調(diào)度的次數(shù),計(jì)算這個(gè)Pod應(yīng)該等待下一次調(diào)度的時(shí)間,計(jì)算規(guī)則為指數(shù)級增長,即按照1s,2s,4s,8s這樣的時(shí)間進(jìn)行等待,但是這個(gè)等待時(shí)間也不會無限增加,會受到 podMaxBackoffDuration(默認(rèn)10s) 的限制,這個(gè)參數(shù)的意思是一個(gè) Pod處于Backoff的最大時(shí)間,如果等待的時(shí)間如果超過了 podMaxBackoffDuration,那么就只等待 podMaxBackoffDuration 就會再次被調(diào)度;
- 當(dāng)前時(shí)間 - 上次調(diào)度的時(shí)間 > 根據(jù)1獲取到的應(yīng)該等待的時(shí)間,那么就把Pod放到activeQ里面,將會被調(diào)度,否則Pod被放入 backoff 隊(duì)列里繼續(xù)等待,如果是在backoff 隊(duì)列等待的話,后面就會被flushBackoffQCompleted取出
所以這里 Pod 如果滿足條件的話 就一定會從unscheduleQ里面移到 backooff里面或者activeQ里面
flushBackoffQCompleted
去取 backoff 隊(duì)列(優(yōu)先隊(duì)列)里面取等待時(shí)間結(jié)束的 Pod,放入 activeQ
func (p *PriorityQueue) flushBackoffQCompleted() { p.lock.Lock() defer p.lock.Unlock() activated := false for { rawPodInfo := p.podBackoffQ.Peek() if rawPodInfo == nil { break } pod := rawPodInfo.(*framework.QueuedPodInfo).Pod boTime := p.getBackoffTime(rawPodInfo.(*framework.QueuedPodInfo)) if boTime.After(p.clock.Now()) { break } _, err := p.podBackoffQ.Pop() if err != nil { klog.ErrorS(err, "Unable to pop pod from backoff queue despite backoff completion", "pod", klog.KObj(pod)) break } p.activeQ.Add(rawPodInfo) metrics.SchedulerQueueIncomingPods.WithLabelValues("active", BackoffComplete).Inc() activated = true } if activated { p.cond.Broadcast() } }
那么除了上述定期主動去判斷一個(gè) UnscheduledQ 或 backoffQ 里面的Pod是不是可以再次被調(diào)度,那么還有沒有其他情況呢?
答案是有的。
還有四種情況會重新判斷這兩個(gè)隊(duì)列里的 Pod 是不是要重新調(diào)度
- 有新節(jié)點(diǎn)加入集群
- 節(jié)點(diǎn)配置或狀態(tài)發(fā)生變化
- 已經(jīng)存在的 Pod 發(fā)生變化
- 集群內(nèi)有Pod被刪除
informerFactory.Core().V1().Nodes().Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: sched.addNodeToCache, UpdateFunc: sched.updateNodeInCache, DeleteFunc: sched.deleteNodeFromCache, }, )
新加入節(jié)點(diǎn)
func (sched *Scheduler) addNodeToCache(obj interface{}) { node, ok := obj.(*v1.Node) if !ok { klog.ErrorS(nil, "Cannot convert to *v1.Node", "obj", obj) return } nodeInfo := sched.Cache.AddNode(node) klog.V(3).InfoS("Add event for node", "node", klog.KObj(node)) sched.SchedulingQueue.MoveAllToActiveOrBackoffQueue(queue.NodeAdd, preCheckForNode(nodeInfo)) }
func preCheckForNode(nodeInfo *framework.NodeInfo) queue.PreEnqueueCheck { // Note: the following checks doesn't take preemption into considerations, in very rare // cases (e.g., node resizing), "pod" may still fail a check but preemption helps. We deliberately // chose to ignore those cases as unschedulable pods will be re-queued eventually. return func(pod *v1.Pod) bool { admissionResults := AdmissionCheck(pod, nodeInfo, false) if len(admissionResults) != 0 { return false } _, isUntolerated := corev1helpers.FindMatchingUntoleratedTaint(nodeInfo.Node().Spec.Taints, pod.Spec.Tolerations, func(t *v1.Taint) bool { return t.Effect == v1.TaintEffectNoSchedule }) return !isUntolerated } }
可以看到,當(dāng)有節(jié)點(diǎn)加入集群的時(shí)候,會把unscheduledQ 里面的Pod 依次拿出來做下面的判斷:
- Pod 對 節(jié)點(diǎn)的親和性
- Pod 中 Nodename不為空 那么判斷新加入節(jié)點(diǎn)的Name判斷pod Nodename是否相等
- 判斷 Pod 中容器對端口的要求是否和新加入節(jié)點(diǎn)已經(jīng)被使用的端口沖突
- Pod 是否容忍了Node的Pod
只有上述4個(gè)條件都滿足,那么新加入節(jié)點(diǎn)這個(gè)事件才會觸發(fā)這個(gè)未被調(diào)度的Pod加入到 backoffQ 或者 activeQ,至于是加入哪個(gè)queue,上面已經(jīng)分析過了
節(jié)點(diǎn)更新
func (sched *Scheduler) updateNodeInCache(oldObj, newObj interface{}) { oldNode, ok := oldObj.(*v1.Node) if !ok { klog.ErrorS(nil, "Cannot convert oldObj to *v1.Node", "oldObj", oldObj) return } newNode, ok := newObj.(*v1.Node) if !ok { klog.ErrorS(nil, "Cannot convert newObj to *v1.Node", "newObj", newObj) return } nodeInfo := sched.Cache.UpdateNode(oldNode, newNode) // Only requeue unschedulable pods if the node became more schedulable. if event := nodeSchedulingPropertiesChange(newNode, oldNode); event != nil { sched.SchedulingQueue.MoveAllToActiveOrBackoffQueue(*event, preCheckForNode(nodeInfo)) } }
func nodeSchedulingPropertiesChange(newNode *v1.Node, oldNode *v1.Node) *framework.ClusterEvent { if nodeSpecUnschedulableChanged(newNode, oldNode) { return &queue.NodeSpecUnschedulableChange } if nodeAllocatableChanged(newNode, oldNode) { return &queue.NodeAllocatableChange } if nodeLabelsChanged(newNode, oldNode) { return &queue.NodeLabelChange } if nodeTaintsChanged(newNode, oldNode) { return &queue.NodeTaintChange } if nodeConditionsChanged(newNode, oldNode) { return &queue.NodeConditionChange } return nil }
首先是判斷節(jié)點(diǎn)是何種配置發(fā)生了變化,有如下情況
- 節(jié)點(diǎn)可調(diào)度情況發(fā)生變化
- 節(jié)點(diǎn)可分配資源發(fā)生變化
- 節(jié)點(diǎn)標(biāo)簽發(fā)生變化
- 節(jié)點(diǎn)污點(diǎn)發(fā)生變化
- 節(jié)點(diǎn)狀態(tài)發(fā)生變化
如果某個(gè) Pod 調(diào)度失敗的原因可以匹配到上面其中一個(gè)原因,那么節(jié)點(diǎn)更新這個(gè)事件才會觸發(fā)這個(gè)未被調(diào)度的Pod加入到 backoffQ 或者 activeQ
informerFactory.Core().V1().Pods().Informer().AddEventHandler( cache.FilteringResourceEventHandler{ FilterFunc: func(obj interface{}) bool { switch t := obj.(type) { case *v1.Pod: return assignedPod(t) case cache.DeletedFinalStateUnknown: if _, ok := t.Obj.(*v1.Pod); ok { // The carried object may be stale, so we don't use it to check if // it's assigned or not. Attempting to cleanup anyways. return true } utilruntime.HandleError(fmt.Errorf("unable to convert object %T to *v1.Pod in %T", obj, sched)) return false default: utilruntime.HandleError(fmt.Errorf("unable to handle object in %T: %T", sched, obj)) return false } }, Handler: cache.ResourceEventHandlerFuncs{ AddFunc: sched.addPodToCache, UpdateFunc: sched.updatePodInCache, DeleteFunc: sched.deletePodFromCache, }, }, )
已經(jīng)存在的 Pod 發(fā)生變化
func (sched *Scheduler) addPodToCache(obj interface{}) { pod, ok := obj.(*v1.Pod) if !ok { klog.ErrorS(nil, "Cannot convert to *v1.Pod", "obj", obj) return } klog.V(3).InfoS("Add event for scheduled pod", "pod", klog.KObj(pod)) if err := sched.Cache.AddPod(pod); err != nil { klog.ErrorS(err, "Scheduler cache AddPod failed", "pod", klog.KObj(pod)) } sched.SchedulingQueue.AssignedPodAdded(pod) }
func (p *PriorityQueue) AssignedPodAdded(pod *v1.Pod) { p.lock.Lock() p.movePodsToActiveOrBackoffQueue(p.getUnschedulablePodsWithMatchingAffinityTerm(pod), AssignedPodAdd) p.lock.Unlock() }
func (p *PriorityQueue) getUnschedulablePodsWithMatchingAffinityTerm(pod *v1.Pod) []*framework.QueuedPodInfo { var nsLabels labels.Set nsLabels = interpodaffinity.GetNamespaceLabelsSnapshot(pod.Namespace, p.nsLister) var podsToMove []*framework.QueuedPodInfo for _, pInfo := range p.unschedulablePods.podInfoMap { for _, term := range pInfo.RequiredAffinityTerms { if term.Matches(pod, nsLabels) { podsToMove = append(podsToMove, pInfo) break } } } return podsToMove }
可以看到,已經(jīng)存在的Pod發(fā)生變化后,會把這個(gè)Pod親和性配置依次和unscheduledQ里面的Pod匹配,如果能夠匹配上,那么節(jié)點(diǎn)更新這個(gè)事件才會觸發(fā)這個(gè)未被調(diào)度的Pod加入到 backoffQ 或者 activeQ。
集群內(nèi)有Pod刪除
func (sched *Scheduler) deletePodFromCache(obj interface{}) { var pod *v1.Pod switch t := obj.(type) { case *v1.Pod: pod = t case cache.DeletedFinalStateUnknown: var ok bool pod, ok = t.Obj.(*v1.Pod) if !ok { klog.ErrorS(nil, "Cannot convert to *v1.Pod", "obj", t.Obj) return } default: klog.ErrorS(nil, "Cannot convert to *v1.Pod", "obj", t) return } klog.V(3).InfoS("Delete event for scheduled pod", "pod", klog.KObj(pod)) if err := sched.Cache.RemovePod(pod); err != nil { klog.ErrorS(err, "Scheduler cache RemovePod failed", "pod", klog.KObj(pod)) } sched.SchedulingQueue.MoveAllToActiveOrBackoffQueue(queue.AssignedPodDelete, nil) }
可以看到,Pod刪除時(shí)間不像其他時(shí)間需要做額外的判斷,這個(gè)preCheck函數(shù)是空的,所以所有 unscheduledQ 里面的Pod都會被放到 activeQ或者backoffQ里面。
從上面的情況,我們可以看到,集群內(nèi)有事件發(fā)生變化,是可以加速調(diào)度失敗的Pod被重新調(diào)度的進(jìn)程的。常規(guī)的是,調(diào)度失敗的 Pod 需要等5min 然后才會被重新加入 backoff 或 activeQ。backoffQ里面的Pod也需要等一段時(shí)間才會重新調(diào)度。這也就是為什么,當(dāng)你修改節(jié)點(diǎn)配置的時(shí)候,能看到Pod馬上重新被調(diào)度的原因
上面就是一個(gè)Pod調(diào)度失敗后,重新觸發(fā)調(diào)度的所有情況了。
更多關(guān)于Pod調(diào)度失敗重新觸發(fā)的資料請關(guān)注腳本之家其它相關(guān)文章!
- Go語言kube-scheduler深度剖析與開發(fā)之pod調(diào)度
- pod調(diào)度將 Pod 指派給節(jié)點(diǎn)
- 云原生技術(shù)kubernetes調(diào)度單位pod的使用詳解
- Kubernetes Informer數(shù)據(jù)存儲Index與Pod分配流程解析
- Kubernetes Visitor設(shè)計(jì)模式及發(fā)送pod創(chuàng)建請求解析
- Kubernetes kubectl中Pod創(chuàng)建流程源碼解析
- 靜態(tài)pod 創(chuàng)建使用示例詳解
- 使用k8tz解決pod內(nèi)的時(shí)區(qū)問題(坑的解決)
相關(guān)文章
golang?實(shí)現(xiàn)?pdf?轉(zhuǎn)高清晰度?jpeg的處理方法
這篇文章主要介紹了golang?實(shí)現(xiàn)?pdf?轉(zhuǎn)高清晰度?jpeg,下面主要介紹Golang 代碼使用方法及Golang PDF轉(zhuǎn)JPEG的詳細(xì)代碼,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-10-10go語言中切片Slice與數(shù)組Array對比以及panic:?runtime?error:?index?out?
go語言中數(shù)組與其他語言有在顯著的不同,包括其不能夠進(jìn)行添加,以及值拷貝的特性,下面這篇文章主要給大家介紹了關(guān)于go語言中切片Slice與數(shù)組Array對比以及panic:?runtime?error:?index?out?of?range問題解決的相關(guān)資料,需要的朋友可以參考下2022-07-07Go?語言數(shù)據(jù)結(jié)構(gòu)如何實(shí)現(xiàn)抄一個(gè)list示例詳解
這篇文章主要為大家介紹了Go?語言數(shù)據(jù)結(jié)構(gòu)如何實(shí)現(xiàn)抄一個(gè)list示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04Golang中如何使用lua進(jìn)行擴(kuò)展詳解
這篇文章主要給大家介紹了關(guān)于Golang中如何使用lua進(jìn)行擴(kuò)展的相關(guān)資料,這是最近在工作中遇到的一個(gè)問題,覺著有必要分享出來給大家學(xué)習(xí),文中給出了詳細(xì)的示例,需要的朋友可以參考借鑒,下面來一起看看吧。2017-10-10分布式架構(gòu)在Go語言網(wǎng)站的應(yīng)用
分布式架構(gòu)是目前應(yīng)對高流量、高并發(fā)的重要解決方案,分布式架構(gòu)的核心思想是分而治之,將單臺服務(wù)器的資源劃分為多臺服務(wù)器進(jìn)行協(xié)同完成,分布式架構(gòu)應(yīng)用于Go語言網(wǎng)站中既能提升服務(wù)速度,又能降低了服務(wù)器宕機(jī)的風(fēng)險(xiǎn)2024-01-01