Go语言kube-scheduler深度剖析与开发之pod调度

目录
  • 正文
  • 感知 Pod
  • 取出 Pod
  • 调度 Pod

正文

为了深入学习 kube-scheduler,本系从源码和实战角度深度学 习kube-scheduler,该系列一共分6篇文章,如下:

  • kube-scheduler 整体架构
  • 初始化一个 scheduler
  • 本文: 一个 Pod 是如何调度的
  • 如何开发一个属于自己的scheduler插件
  • 开发一个 prefilter 扩展点的插件
  • 开发一个 socre 扩展点的插件

上一篇文章我们讲了一个 kube-scheduler 是怎么初始化出来的,有了 调度器之后就得开始让他干活了 这一篇文章我们来讲讲一个 Pod 是怎么被调度到某个 Node 的。

我把调度一个 Pod 分为3个阶段

  • 获取需要被调度的 Pod
  • 运行每个扩展点的所有插件,给 Pod 选择一个最合适的 Node
  • 将 Pod 绑定到选出来的 Node

感知 Pod

要能够获取到 Pod 的前提是:kube-scheduler 能感知到有 Pod 需要被调度,得知有 Pod 需要被调度后还需要有地方存放被调度的 Pod 的信息。为了感知有 Pod 需要被调度,kube-scheduler 启动时通过 Informer watch Pod 的变化,它把待调度的 Pod 分了两种情况,代码如下

// pkg/scheduler/eventhandlers.go
func addAllEventHandlers(...) {
  //已经调度过的 Pod 则加到本地缓存,并判断是加入到调度队列还是加入到backoff队列
  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,
      },
    },
  )
  // 没有调度过的Pod,放到调度队列
  informerFactory.Core().V1().Pods().Informer().AddEventHandler(
    cache.FilteringResourceEventHandler{
      FilterFunc: func(obj interface{}) bool {
        switch t := obj.(type) {
        case *v1.Pod:
          return !assignedPod(t) && responsibleForPod(t, sched.Profiles)
        case cache.DeletedFinalStateUnknown:
          if pod, 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.
            return responsibleForPod(pod, sched.Profiles)
          }
          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.addPodToSchedulingQueue,
        UpdateFunc: sched.updatePodInSchedulingQueue,
        DeleteFunc: sched.deletePodFromSchedulingQueue,
      },
    },
  )
......
}
  • 已经调度过的 Pod 区分是不是调度过的 Pod 是通过:len(pod.Spec.NodeName) != 0 来判断的,因为调度过的 Pod 这个字段总是会被赋予被选中的 Node 名字。但是,既然是调度过的 Pod 下面的代码中为什么还要区分:sched.addPodToCache 和 sched.updatePodInCache 呢?原因在于我们可以在创建 Pod 的时候人为给它分配一个 Node(即给 pod.Spec.NodeName 赋值),这样 kube-scheduler 在监听到该 Pod 后,判断这个 Pod 的该字段不为空就会认为这个 Pod 已经调度过了,但是这个字段不为空并不是 kube-scheduler 调度的结果,而是人为赋值的,那么 kube-scheduler 的 cache(可以参考上一篇 cache 相关的内容)中没有这个 Pod 的信息,所以就需要将 Pod 信息加入到 cache 中。至于在监听到 Pod 后 sched.addPodToCache 和 sched.updatePodInCache 哪个会被调用,这是 Informer 决定的,它会根据监听到变化的 Pod 和 Informer 的本地缓存做对比,要是缓存中没有这个 Pod,那么就调用 add 函数,否则就调用 update 函数。

加入或更新缓存后,还需要做一件事:去 unschedulablePods(调度失败的Pod) 中获取 Pod,这些 Pod 的亲和性和刚刚加入的这个 Pod 匹配,然后根据下面的规则判断是把 Pod 放入 backoffQ 还是放入 activeQ

  • 根据这个 Pod 尝试被调度的次数计算这个 Pod 下次调度应该等待的时间,计算规则为指数级增长,即按照1s,2s,4s,8s这样的时间进行等待,但是这个等待时间也不会无限增加,会受到 podMaxBackoffDuration(默认10s) 的限制,这个参数表示是一个 Pod 处于 backoff 的最大时间,如果等待的时间如果超过了 podMaxBackoffDuration,那么就只等待 podMaxBackoffDuration 就会再次被调度;
  • 当前时间 - 上次调度的时间 > 根据(1)获取到的应该等待的时间,那么就把Pod放到activeQ里面,将会被调度,否则Pod被放入 backoff 队列里等待。

从上面我们可以看到,一个 Pod 的变化会触发此前调度失败的 Pod 重新判断是否可以被调度

  • 没有调度过的 Pod

len(pod.Spec.NodeName) = 0,那么这个 Pod 没有被调度过或者是此前调度过但是调度失败的(用户修改了 Pod 的配置导致 Pod 发生变化,又被 kube-scheduler 感知到了),如果是没有调度过的 Pod 那么直接加入到 activeQ,如果是调度失败的 Pod 则根据上述规则判断是加入 backoffQ 还是 activeQ。加入到 activeQ 会马上被取走,然后开始调度。

那么那些因为调度失败而被放入 unscheduleable 的 Pod 还有其他机会(上面说的有新 Pod 创建是一个机会)重新被调度么?答案是有的,否则他们就“被饿死了”,有两种途径:1. 定期强制将 unscheduleable 的 Pod 放入 backoffQ 或 activeQ,定期将 backoffQ 等待超时的 Pod 放入 ac activeQ;2. 集群内其他相关资源变化时,判断 unscheduleable 中的 Pod 是不是要放入 backoffQ 或 activeQ,其实这跟有 Pod 发生变化的情况是一样的。

第一种情况

在 kube-scheduler启动的时候中会起两个协程,他们会定期把 backoffQ 和 unscheduleable 里面的 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()
       }
    }x

将在 unscheduleable 里面停留时长超过 podMaxInUnschedulablePodsDuration(默认是5min)的pod放入到 ActiveQ 或 BackoffQueue,具体是放到哪个队列里面,还是根据我们上文说的那个实际计算规则来。这么做的原因就是给那些“问题少年”一次重新做人的机会,也不能一犯错误(调度失败)就彻底打入死牢了。

flushBackoffQCompleted

去 backoffQ 获取等待结束的 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()
       }
    }

第二种情况

集群内资源发生变化

  • 有新节点加入集群
  • 节点配置或状态发生变化
  • 已经存在的 Pod 发生变化
  • 集群内有Pod被删除
informerFactory.Core().V1().Nodes().Informer().AddEventHandler(
   cache.ResourceEventHandlerFuncs{
      AddFunc:    sched.addNodeToCache,
      UpdateFunc: sched.updateNodeInCache,
      DeleteFunc: sched.deleteNodeFromCache,
   },
)

新加入节点

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
   }
}

可以看到,当有节点加入集群的时候,会把 unscheduleable 里面的Pod 依次拿出来做下面的判断:

  • Pod 对 节点的亲和性
  • Pod 中 Nodename不为空 那么判断新加入节点的Name判断pod Nodename是否相等
  • 判断 Pod 中容器对端口的要求是否和新加入节点已经被使用的端口冲突
  • Pod 是否容忍了Node的Pod

只有上述4个条件都满足,那么新加入节点这个事件才会触发这个未被调度的Pod加入到 backoffQ 或者 activeQ,至于是加入哪个queue,上面已经分析过了

节点更新

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
}

首先是判断节点是何种配置发生了变化,有如下情况

  • 节点可调度情况发生变化
  • 节点可分配资源发生变化
  • 节点标签发生变化
  • 节点污点发生变化
  • 节点状态发生变化

如果某个 Pod 调度失败的原因可以匹配到上面其中一个原因,那么节点更新这个事件才会触发这个未被调度的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,
      },
   },
)

已经存在的 Pod 发生变化

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
}

可以看到,已经存在的Pod发生变化后,会把这个Pod亲和性配置依次和 unscheduleable 里面的Pod匹配,如果能够匹配上,那么节点更新这个事件才会触发这个未被调度的Pod加入到 backoffQ 或者 activeQ。

集群内有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删除时间不像其他时间需要做额外的判断,这个preCheck函数是空的,所以所有 unscheduleable 里面的Pod都会被放到 activeQ 或 backoffQ 中。

从上面的情况,我们可以看到,集群内有事件发生变化,是可以加速调度失败的Pod被重新调度的进程的。常规的是,调度失败的 Pod 需要等5min 然后才会被重新加入 backoffQ 或 activeQ。backoffQ里面的Pod也需要等一段时间才会重新调度。这也就是为什么,当你修改节点配置的时候,能看到Pod马上重新被调度的原因

上面就是一个Pod调度失败后,重新触发调度的情况了。

取出 Pod

Scheduler 中有个成员 NextPod 会从 activeQ 队列中尝试获取一个待调度的 Pod,该函数在 SchedulePod 中被调用,如下:

// 启动 Scheduler
func (sched *Scheduler) Run(ctx context.Context) {
	sched.SchedulingQueue.Run()
	go wait.UntilWithContext(ctx, sched.scheduleOne, 0)
	<-ctx.Done()
	sched.SchedulingQueue.Close()
}
// 尝试调度一个 Pod,所以 Pod 的调度入口
func (sched *Scheduler) scheduleOne(ctx context.Context) {
	// 会一直阻塞,直到获取到一个Pod
	......
	podInfo := sched.NextPod()
    ......
}

NextPod 它被赋予如下函数:

// pkg/scheduler/internal/queue/scheduling_queue.go
func MakeNextPodFunc(queue SchedulingQueue) func() *framework.QueuedPodInfo {
	return func() *framework.QueuedPodInfo {
		podInfo, err := queue.Pop()
		if err == nil {
			klog.V(4).InfoS("About to try and schedule pod", "pod", klog.KObj(podInfo.Pod))
			for plugin := range podInfo.UnschedulablePlugins {
				metrics.UnschedulableReason(plugin, podInfo.Pod.Spec.SchedulerName).Dec()
			}
			return podInfo
		}
		klog.ErrorS(err, "Error while retrieving next pod from scheduling queue")
		return nil
	}
}

Pop 会一直阻塞,直到 activeQ 长度大于0,然后取出一个 Pod 返回

// pkg/scheduler/internal/queue/scheduling_queue.go
func (p *PriorityQueue) Pop() (*framework.QueuedPodInfo, error) {
	p.lock.Lock()
	defer p.lock.Unlock()
	for p.activeQ.Len() == 0 {
		// When the queue is empty, invocation of Pop() is blocked until new item is enqueued.
		// When Close() is called, the p.closed is set and the condition is broadcast,
		// which causes this loop to continue and return from the Pop().
		if p.closed {
			return nil, fmt.Errorf(queueClosed)
		}
		p.cond.Wait()
	}
	obj, err := p.activeQ.Pop()
	if err != nil {
		return nil, err
	}
	pInfo := obj.(*framework.QueuedPodInfo)
	pInfo.Attempts++
	p.schedulingCycle++
	return pInfo, nil
}

调度 Pod

func (sched *Scheduler) scheduleOne(ctx context.Context) {
    // 取出 Pod
    podInfo := sched.NextPod()
    ...
    // 根据 Pod 的调度名字,获取之前初始化好的调度框架(framework)
    fwk, err := sched.frameworkForPod(pod)
    ...
    // 开始执行插件,包括 filter, socre 两个扩展点内的所有插件,获取一个最合适 Pod 的节点
    scheduleResult, err := sched.SchedulePod(schedulingCycleCtx, fwk, state, pod)
    // 如果获取节点失败,则开始运行 postFilter 开始抢占一个 Pod
    if err != nil {
        result, status := fwk.RunPostFilterPlugins(ctx, state, pod, fitError.Diagnosis.NodeToStatusMap)
    }
    ....
    // 将 Pod 放入 assumedPod 存储,即假设 Pod 已经调度成功
    err = sched.assume(assumedPod, scheduleResult.SuggestedHost)
    // 运行 Reserve 插件
    fwk.RunReservePluginsReserve(schedulingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
    ...
    // 运行 Permit 插件
    fwk.RunPermitPlugins(schedulingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
    ...
    // 启动一个协程,开始绑定,主流程到了这里就结束了,然后开始新的一轮调度;
    go func() {
        // 执行 preBind 插件
        fwk.RunPreBindPlugins(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
        ...
        // 执行绑定插件,会调用 kube-apiserver 写入etcd 调度结果,就是给 Pod 赋予 Nodename
        err := sched.bind(bindingCycleCtx, fwk, assumedPod, scheduleResult.SuggestedHost, state)
        ...
        // 执行 postBind
        fwk.RunPostBindPlugins(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
    }
  • 执行 filter 类型扩展点(包括preFilter,filter,postFilter)插件,选出所有符合 Pod 的 Node,如果无法找到符合的 Node, 则把 Pod 加入 unscheduleable 中,此次调度结束;
  • 执行 score 扩展点插件,给所有 Node 打分;
  • 拿出得分最高的 Node;
  • assume Pod。这一步就是乐观假设 Pod 已经调度成功,更新缓存中 Node 和 PodStats 信息,到了这里scheduling cycle就已经结束了,然后会开启新的一轮调度。至于真正的绑定,则会新起一个协程。
  • 执行 reserve 插件;
  • 启动协程绑定 Pod 到 Node上。实际上就是修改 Pod.spec.nodeName: 选定的node名字,然后调用 kube-apiserver 接口写入 etcd。如果绑定失败了,那么移除缓存中此前加入的信息,然后把 Pod 放入activeQ 中,后续重新调度。执行 postBinding,该步没有实现的插件没所以没有做任何事。

好了,到了这里一个 Pod 如果能够正常的被调度的话,那么流程就结束了。如果调度失败的话,Pod会被放入 unscheduleable 中,后续还会对 unscheduleable 中的 Pod 重新调度。

(0)

相关推荐

  • GoJs连线绘图模板Link使用示例详解

    目录 前言 go.link的简单使用 go.Link的属性配置 routing属性 curve属性 corner.toEndSegmentLength.fromEndSegmentLength.fromShortLength.toShortLength属性 selectable.fromSpot.toSpot属性 总结 前言 可视化图形中除了携带很多信息的节点(node)之外,还需要知道他们之间的关系,而链接他们之间的连线在gojs中是使用go.link进行绘制.在渲染的时候我们根据数据的fro

  • Go语言开发kube-scheduler整体架构深度剖析

    目录 k8s 的调度器 kube-scheduler 官方描述scheduler 各个类型扩展点 kube-scheduler 代码的主要框架 k8s 的调度器 kube-scheduler kube-scheduler 作为 k8s 的调度器,就好比人的大脑,将行动指定传递到手脚等器官,进而执行对应的动作,对于 kube-scheduler 则是将 Pod 分配(调度)到集群内的各个节点,进而创建容器运行进程,对于k8s来说至关重要. 为了深入学习 kube-scheduler,本系从源码和实

  • Go语言学习otns示例分析

    目录 学习过程 proto文件 visualize/grpc/replay目录下的文件 cmd/otns-replay目录下的文件 grpc_Service(包含pb) otns_replay(包含pb) cmd/otns/otns.go文件 simulation目录下的文件 type.go simulationController.go simulation_config.go simulation.go cli目录 ast.go CmdRunner.go 总结 学习过程 由于在用虚拟机体验过

  • Go语言kube-scheduler深度剖析开发之scheduler初始化

    目录 引言 Scheduler之Profiles Scheduler 之 SchedulingQueue Scheduler 之 cache Scheduler 之 NextPod 和 SchedulePod 引言 为了深入学习 kube-scheduler,本系从源码和实战角度深度学 习kube-scheduler,该系列一共分6篇文章,如下: kube-scheduler 整体架构 本文 :初始化一个 scheduler 一个 Pod 是如何调度的 如何开发一个属于自己的scheduler插

  • 深入string理解Golang是怎样实现的

    目录 引言 内容介绍 字符串数据结构 字符串会分配到内存中的哪块区域 编译期即可确定的字符串 如果我们创建两个hello world字符串, 他们会放到同一内存区域吗? 运行时通过+拼接的字符串会放到那块内存中 字面量是否会在编译器合并 当我们用+连接多个字符串时, 会发生什么 rawstring函数 go中字符串是不可变的吗, 我们如何得到一个可变的字符串 []byte和string的更高效转换 结尾 引言 本身打算先写完sync包的, 但前几天在复习以前笔记的时候突然发现与字符串相关的寥寥无

  • Go语言kube-scheduler深度剖析与开发之pod调度

    目录 正文 感知 Pod 取出 Pod 调度 Pod 正文 为了深入学习 kube-scheduler,本系从源码和实战角度深度学 习kube-scheduler,该系列一共分6篇文章,如下: kube-scheduler 整体架构 初始化一个 scheduler 本文: 一个 Pod 是如何调度的 如何开发一个属于自己的scheduler插件 开发一个 prefilter 扩展点的插件 开发一个 socre 扩展点的插件 上一篇文章我们讲了一个 kube-scheduler 是怎么初始化出来的

  • C语言可变参数列表的用法与深度剖析

    目录 前言 一.可变参数列表是什么? 二.怎么用可变参数列表 三.对于宏的深度剖析 隐式类型转换 对两个函数的重新认知 总结 前言 可变参数列表,使用起来像是数组,学习过函数栈帧的话可以发现实际上他也就是在栈区定义的一块空间当中连续访问,不过他不支持直接在中间部分访问. 声明: 以下所有测试都是在x86,vs2013下完成的. 一.可变参数列表是什么? 在我们初始C语言的第一节课的时候我们就已经接触了可变参数列表,在printf的过程当中我们通常可以传递大量要打印的参数,但是我们却不知道他是如何

  • 你真的理解C语言qsort函数吗 带你深度剖析qsort函数

    目录 一.前言 二.简单冒泡排序法 三.qsort函数的使用 1.qsort函数的介绍 2.qsort函数的运用 2.1.qsort函数排序整型数组 2.2.qsort函数排序结构体 四.利用冒泡排序模拟实现qsort函数 五.总结 一.前言 我们初识C语言时,会做过让一个整型数组按照从小到大来排序的问题,我们使用的是冒泡排序法,但是如果我们想要比较其他类型怎么办呢,显然我们当时的代码只适用于简单的整形排序,对于结构体等没办法排序,本篇将引入一个库函数来实现我们希望的顺序. 二.简单冒泡排序法

  • js对象实例详解(JavaScript对象深度剖析,深度理解js对象)

    这算是酝酿很久的一篇文章了. JavaScript作为一个基于对象(没有类的概念)的语言,从入门到精通到放弃一直会被对象这个问题围绕. 平时发的文章基本都是开发中遇到的问题和对最佳解决方案的探讨,终于忍不住要写一篇基础概念类的文章了. 本文探讨以下问题,在座的朋友各取所需,欢迎批评指正: 1.创建对象 2.__proto__与prototype 3.继承与原型链 4.对象的深度克隆 5.一些Object的方法与需要注意的点 6.ES6新增特性 下面反复提到实例对象和原型对象,通过构造函数 new

  • 深度剖析Golang如何实现GC扫描对象

    目录 扫描的目的 扫描的实现 运行期内存分配 运行扫描阶段 总结 之前阐述了 golang 垃圾回收通过保证三色不变式来保证回收的正确性,通过写屏障来实现业务赋值器和 gc 回收器正确的并发的逻辑.其中高概率的提到了“扫描队列”和“扫描对象”.队列这个逻辑非常容易理解,那么”扫描对象“ 这个你理解了吗?有直观的感受吗?这篇文章就是要把这个扫描的过程深入剖析下. 扫描的东西是啥?形象化描述下 怎么去做的扫描?形象化描述下 我们就是要把这两个抽象的概念搞懂,不能停留在语言级别浅层面,要知其然知其所以

  • CSS的margin边界叠加深度剖析图文演示

    边界叠加是一个相当简单的概念.但是,在实践中对网页进行布局时,它会造成许多混淆.简单地说,当两个垂直边界相遇时,它们将形成一个边界.这个边界的高度等于两个发生叠加的边界的高度中的较大者. 当一个元素出现在另一个元素上面时,第一个元素的底边界与第二个元素的顶边界发生叠加,见图: 元素的顶边界与前面元素的底边界发生叠加 当一个元素包含在另一个元素中时(假设没有填充或边框将边界分隔开),它们的顶和/或底边界也发生叠加,见图: 元素的顶边界与父元素的顶边界发生叠加 尽管初看上去有点儿奇怪,但是边界甚至可

  • PHP序列化和反序列化深度剖析实例讲解

    序列化 序列化格式 在PHP中,序列化用于存储或传递 PHP 的值的过程中,同时不丢失其类型和结构. 序列化函数原型如下: string serialize ( mixed $value ) 先看下面的例子: class CC { public $data; private $pass; public function __construct($data, $pass) { $this->data = $data; $this->pass = $pass; } } $number = 34;

  • R语言开发之CSV文件的读写操作实现

    在R中,我们可以从存储在R环境外部的文件读取数据,还可以将数据写入由操作系统存储和访问的文件.这个csv文件应该存在于当前工作目录中,以方便R可以读取它, 当然,也可以设置自己的目录,并从那里读取文件. 我们可以使用getwd()函数来检查R工作区指向哪个目录,并且使用setwd()函数设置新的工作目录,如下: 输出结果如下: csv文件是一个文本文件,其中列中的值用逗号分隔,我们可以将以下数据保存入txt文件中,并且修改后缀名称为csv: id,name,salary,start_date,d

  • MyBatis核心源码深度剖析SQL语句执行过程

    目录 1 SQL语句的执行过程介绍 2 SQL执行的入口分析 2.1 为Mapper接口创建代理对象 2.2 执行代理逻辑 3 查询语句的执行过程分析 3.1 selectOne方法分析 3.2 sql获取 3.3 参数设置 3.4 SQL执行和结果集的封装 4 更新语句的执行过程分析 4.1 sqlsession增删改方法分析 4.2 sql获取 4.3 参数设置 4.4 SQL执行 5 小结 1 SQL语句的执行过程介绍 MyBatis核心执行组件: 2 SQL执行的入口分析 2.1 为Ma

  • 深度剖析Golang中的数组,字符串和切片

    目录 1. 数组 1.1 定义数组 1.2 访问数组 1.3 修改数组 1.4 数组长度 1.5 遍历数组 1.6 多维数组 2. 切片 2.1 定义切片 2.2 访问切片元素 2.3 修改切片元素 2.4 切片长度和容量 2.5 向切片中添加元素 2.6 切片的切片 2.7 切片排序 3. 字符串 3.1 访问字符串中的字符 3.2 字符串切片 3.3 字符串操作 3.4 关于字符串的常见问题 4. 总结 1. 数组 数组是 Golang 中的一种基本数据类型,用于存储固定数量的同类型元素.在

随机推荐