前言

之前探讨scheduler的调度流程时,提及过preempt抢占机制,它发生在预选调度失败的时候,当时由于篇幅限制就没有展开细说。

回顾一下抢占流程的主要逻辑在DefaultPreemption.preempt方法,步骤包括:

  1. 拿最新版本的pod,刷新lister的缓存
  2. 确保抢占者有资格抢占其他Pod
  3. 寻找抢占候选者
  4. 与注册扩展器进行交互,以便在需要时筛选出某些候选者。
  5. 选出最佳的候选者
  6. 在提名选定的候选人之前,先进行准备工作。

代码位于/pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.go

func (pl *DefaultPreemption) preempt(...) (string, error) {
	cs := pl.fh.ClientSet()
	ph := pl.fh.PreemptHandle()
	nodeLister := pl.fh.SnapshotSharedLister().NodeInfos()
	//1.拿最新版本的pod,刷新lister的缓存
	pod, err := pl.podLister.Pods(pod.Namespace).Get(pod.Name)
	//2.确保抢占者有资格抢占其他Pod
	if !PodEligibleToPreemptOthers(pod, nodeLister, m[pod.Status.NominatedNodeName]) {
	}
	//3.寻找抢占候选者
	candidates, err := FindCandidates(ctx, cs, state, pod, m, ph, nodeLister, pl.pdbLister)
	//4.与注册扩展器进行交互,以便在需要时筛选出某些候选者。
	candidates, err = CallExtenders(ph.Extenders(), pod, nodeLister, candidates)
	//5.选出最佳的候选者
	bestCandidate := SelectCandidate(candidates)
	//6.在提名选定的候选人之前,先进行准备工作。
	if err := PrepareCandidate(bestCandidate, pl.fh, cs, pod); err != nil {
	}
	return bestCandidate.Name(), nil
}

下面则展开细说每个函数的细节

PodEligibleToPreemptOthers

func PodEligibleToPreemptOthers(pod *v1.Pod, nodeInfos framework.NodeInfoLister, nominatedNodeStatus *framework.Status) bool {
	if pod.Spec.PreemptionPolicy != nil && *pod.Spec.PreemptionPolicy == v1.PreemptNever {
		klog.V(5).Infof("Pod %v/%v is not eligible for preemption because it has a preemptionPolicy of %v", pod.Namespace, pod.Name, v1.PreemptNever)
		return false
	}
	nomNodeName := pod.Status.NominatedNodeName
	if len(nomNodeName) > 0 {
		// If the pod's nominated node is considered as UnschedulableAndUnresolvable by the filters,
		// then the pod should be considered for preempting again.
		if nominatedNodeStatus.Code() == framework.UnschedulableAndUnresolvable {
			return true
		}

		if nodeInfo, _ := nodeInfos.Get(nomNodeName); nodeInfo != nil {
			podPriority := podutil.GetPodPriority(pod)
			for _, p := range nodeInfo.Pods {
				if p.Pod.DeletionTimestamp != nil && podutil.GetPodPriority(p.Pod) < podPriority {
					// There is a terminating pod on the nominated node.
					return false
				}
			}
		}
	}
	return true
}

如果pod的调度策略设置成不抢占的,则这个pod不适合执行抢占机制,就会直接退出
pod.Status.NominatedNodeName这个字段不为空,则说明了当前pod已经经历过一次抢占,当pod可以抢占调度到某个节点时,pod.Status.NominatedNodeName字段就会填写上这个node的name。如果字段为空则没发生过抢占,可以让它执行;如果有抢占过改节点,则要判断该节点是否有优先级较低Pod的正在被删除(p.Pod.DeletionTimestamp != nil),有则先让当前Pod不执行抢占,因为那个抢占会引起优先级低的Pod删除,这个正在被删除的Pod有可能是上次抢占的时候被当前的Pod给挤掉的,应该要当前的Pod再等等,待正在删除的Pod清掉后能否正常调度到该节点,减少无谓的抢占。

FindCandidates

FindCandidates函数是寻找所有可供抢占的候选者集合,候选者就是有可能被抢占到的node,以及这个node中因为这次抢占而被驱逐的Pod(即牺牲者),另外还有这些牺牲者中PDB的数量。相关结构的定义如下

type candidate struct {
	victims *extenderv1.Victims
	name    string
}
type Victims struct {
	Pods             []*v1.Pod
	NumPDBViolations int64
}

FindCandidates函数的定义如下,

func FindCandidates(ctx context.Context, cs kubernetes.Interface, state *framework.CycleState, pod *v1.Pod,
	m framework.NodeToStatusMap, ph framework.PreemptHandle, nodeLister framework.NodeInfoLister,
	pdbLister policylisters.PodDisruptionBudgetLister) ([]Candidate, error) {
	allNodes, err := nodeLister.List()
	if err != nil {
		return nil, err
	}
	if len(allNodes) == 0 {
		return nil, core.ErrNoNodesAvailable
	}
	//获取所有非不可调度的节点
	potentialNodes := nodesWherePreemptionMightHelp(allNodes, m)
	if len(potentialNodes) == 0 {
		klog.V(3).Infof("Preemption will not help schedule pod %v/%v on any node.", pod.Namespace, pod.Name)
		// In this case, we should clean-up any existing nominated node name of the pod.
		if err := util.ClearNominatedNodeName(cs, pod); err != nil {
			klog.Errorf("Cannot clear 'NominatedNodeName' field of pod %v/%v: %v", pod.Namespace, pod.Name, err)
			// We do not return as this error is not critical.
		}
		return nil, nil
	}
	if klog.V(5).Enabled() {
		var sample []string
		for i := 0; i < 10 && i < len(potentialNodes); i++ {
			sample = append(sample, potentialNodes[i].Node().Name)
		}
		klog.Infof("%v potential nodes for preemption, first %v are: %v", len(potentialNodes), len(sample), sample)
	}
	pdbs, err := getPodDisruptionBudgets(pdbLister)
	if err != nil {
		return nil, err
	}
	///重点的函数
	return dryRunPreemption(ctx, ph, state, pod, potentialNodes, pdbs), nil
}

nodesWherePreemptionMightHelp

nodesWherePreemptionMightHelp函数用于从所有节点中筛选掉UnschedulableAndUnresolvable的节点,关于状态UnschedulableAndUnresolvable,注释是这样的:用于预选调度发现pod不可调度且抢占不会改变任何内容时。如果pod可以通过抢占获得调度,插件应该返回Unschedulable。随附的状态信息应解释pod不可调度的原因。

func nodesWherePreemptionMightHelp(nodes []*framework.NodeInfo, m framework.NodeToStatusMap) []*framework.NodeInfo {
	var potentialNodes []*framework.NodeInfo
	for _, node := range nodes {
		name := node.Node().Name
		// We reply on the status by each plugin - 'Unschedulable' or 'UnschedulableAndUnresolvable'
		// to determine whether preemption may help or not on the node.
		if m[name].Code() == framework.UnschedulableAndUnresolvable {
			continue
		}
		potentialNodes = append(potentialNodes, node)
	}
	return potentialNodes
}

dryRunPreemption

dryRunPreemption用于并行执行模拟抢占的函数,它对nodesWherePreemptionMightHelp筛选出来的节点执行一次模拟抢占函数,凡是可以通过模拟抢占的节点就会生成候选者信息,把节点名,牺牲者的集合及PDB数量记录下来

func dryRunPreemption(ctx context.Context, fh framework.PreemptHandle, state *framework.CycleState,
	pod *v1.Pod, potentialNodes []*framework.NodeInfo, pdbs []*policy.PodDisruptionBudget) []Candidate {
	var resultLock sync.Mutex
	var candidates []Candidate

	checkNode := func(i int) {
		nodeInfoCopy := potentialNodes[i].Clone()
		stateCopy := state.Clone()
		//通过预选调度模拟计算出可牺牲的pod列表及牺牲pod中PBD数量
		pods, numPDBViolations, fits := selectVictimsOnNode(ctx, fh, stateCopy, pod, nodeInfoCopy, pdbs)
		if fits {
			resultLock.Lock()
			victims := extenderv1.Victims{
				Pods:             pods,
				NumPDBViolations: int64(numPDBViolations),
			}
			c := candidate{
				victims: &victims,
				name:    nodeInfoCopy.Node().Name,
			}
			candidates = append(candidates, &c)
			resultLock.Unlock()
		}
	}
	parallelize.Until(ctx, len(potentialNodes), checkNode)
	return candidates
}

selectVictimsOnNode

selectVictimsOnNode是执行模拟抢占的最核心函数,大体思路就是

  1. 找出候选节点上所有优先级较低的Pod并将他们移除,这些Pod定为潜在牺牲者
  2. 将当前Pod执行预选调度到候选节点看是否合适
  3. 将潜在牺牲者按优先级排序重新执行预选调度看能否重新调回到节点上,不能调度的成为真正的牺牲者,且统计PDB的数量

在k8s中一个Pod的默认值优先级是0

func selectVictimsOnNode(...) ([]*v1.Pod, int, bool) {
	//模拟从节点上移除pod
	removePod := func(rp *v1.Pod) error {
	}
	//模拟从节点数增加Pod
	addPod := func(ap *v1.Pod) error {
	}
	//找出所有优先级较低的Pod移除,并加入潜在牺牲者集合
	for _, p := range nodeInfo.Pods {
		if podutil.GetPodPriority(p.Pod) < podPriority {
			potentialVictims = append(potentialVictims, p.Pod)
			if err := removePod(p.Pod); err != nil {
				return nil, 0, false
			}
		}
	}
	//移除了潜在牺牲者后尝试执行预选调度算法将Pod加入到节点中
	if fits, _, err := core.PodPassesFiltersOnNode(ctx, ph, state, pod, nodeInfo); !fits {
	}
	//将潜在牺牲者按优先级排序,并分辨出含有PDB和不含PDB的
	sort.Slice(potentialVictims, func(i, j int) bool { return util.MoreImportantPod(potentialVictims[i], potentialVictims[j]) })
	violatingVictims, nonViolatingVictims := filterPodsWithPDBViolation(potentialVictims, pdbs)
	//将潜在牺牲者在当前Pod加入到候选节点后尝试预选调度,如果不能调度成功的,候选牺牲者成为本节点的真正牺牲者,也统计牺牲者中PDB的数量
	reprievePod := func(p *v1.Pod) (bool, error) {
		if err := addPod(p); err != nil {
			return false, err
		}
		//执行预选调度
		fits, _, _ := core.PodPassesFiltersOnNode(ctx, ph, state, pod, nodeInfo)
		if !fits {
			if err := removePod(p); err != nil {
				return false, err
			}
			victims = append(victims, p)
			klog.V(5).Infof("Pod %v/%v is a potential preemption victim on node %v.", p.Namespace, p.Name, nodeInfo.Node().Name)
		}
		return fits, nil
	}
	for _, p := range violatingVictims {
		if fits, err := reprievePod(p); err != nil {
			klog.Warningf("Failed to reprieve pod %q: %v", p.Name, err)
			return nil, 0, false
		} else if !fits {
			numViolatingVictim++
		}
	}
	// Now we try to reprieve non-violating victims.
	for _, p := range nonViolatingVictims {
		if _, err := reprievePod(p); err != nil {
			klog.Warningf("Failed to reprieve pod %q: %v", p.Name, err)
			return nil, 0, false
		}
	}
}

core.PodPassesFiltersOnNode就是上一篇执行预选调度算法的函数,每一次调用这个函数时,预选调度的那批插件都有可能执行两次,

  • 第一次是加上这个节点中抢占Pod之后,看当前的Pod能否调度成功,抢占的Pod是那些会抢占调度到当前Node但是又没有实际调度到的Pod
  • 如果根本没有抢占Pod在这个节点,或者第一次运行根本不成功的,就完全不用执行第二次了。

执行两次主要考虑到抢占Pod与当前Pod间是否有亲缘性与反亲缘性问题,代码位于 /pkg/schduler/core/generic_scheduler.go

func PodPassesFiltersOnNode(...){
	for i := 0; i < 2; i++ {
		stateToUse := state
		nodeInfoToUse := info
		if i == 0 {
			var err error
			podsAdded, stateToUse, nodeInfoToUse, err = addNominatedPods(ctx, ph, pod, state, info)
			if err != nil {
				return false, nil, err
			}
		} else if !podsAdded || !status.IsSuccess() {
			break
		}

		statusMap := ph.RunFilterPlugins(ctx, stateToUse, pod, nodeInfoToUse)
		status = statusMap.Merge()
		if !status.IsSuccess() && !status.IsUnschedulable() {
			return false, status, status.AsError()
		}
	}
}

模拟抢占的逻辑就这样结束,逻辑执行完会产生若干个候选者节点,如果一个都没有则意味着抢占失败

CallExtenders

CallExtenders主要把候选者都经过扩展的过滤器插件筛选一遍,代码简略如下

func CallExtenders(extenders []framework.Extender, pod *v1.Pod, nodeLister framework.NodeInfoLister,
	candidates []Candidate) ([]Candidate, error) {
	victimsMap := candidatesToVictimsMap(candidates)
	for _, extender := range extenders {
		nodeNameToVictims, err := extender.ProcessPreemption(pod, victimsMap, nodeLister)
		victimsMap = nodeNameToVictims
	}
	for nodeName := range victimsMap {
		newCandidates = append(newCandidates, &candidate{
			victims: victimsMap[nodeName],
			name:    nodeName,
		})
	}
}

SelectCandidate

经过扩展的过滤器插件筛选后,则需要调用SelectCandidate从剩余的候选者中选出一个最优的节点来抢占。

  • 当发现只有一个候选者时不需要选择
  • 执行一系列筛选标准算出最优的候选者
  • 当选不出的时候就默认拿候选者集合的第一个作为最优候选者
func SelectCandidate(candidates []Candidate) Candidate {
	if len(candidates) == 0 {
		return nil
	}
	if len(candidates) == 1 {
		return candidates[0]
	}

	//将结构转成 nodeName,牺牲者数组 的map
	victimsMap := candidatesToVictimsMap(candidates)
	//按照一些列选择标准挑选出最优的候选者
	candidateNode := pickOneNodeForPreemption(victimsMap)

	// Same as candidatesToVictimsMap, this logic is not applicable for out-of-tree
	// preemption plugins that exercise different candidates on the same nominated node.
	if victims := victimsMap[candidateNode]; victims != nil {
		return &candidate{
			victims: victims,
			name:    candidateNode,
		}
	}

	// We shouldn't reach here.
	klog.Errorf("should not reach here, no candidate selected from %v.", candidates)
	// To not break the whole flow, return the first candidate.
	return candidates[0]
}

最优候选者的标准如下

  1. 选择一个PBD违规数量最少的
  2. 选择一个包含最高优先级牺牲者最小的
  3. 所有牺牲者的优先级总和最小的
  4. 最少牺牲者的
  5. 拥有所有最高优先级的牺牲者最迟才启动的

这个标准是层层筛选,选到哪一层只剩下一个候选者的,那个剩余的就是最优候选者

func pickOneNodeForPreemption(nodesToVictims map[string]*extenderv1.Victims) string {
	//计算PDB数量最少的候选者,
	for node, victims := range nodesToVictims {
		numPDBViolatingPods := victims.NumPDBViolations
		if numPDBViolatingPods < minNumPDBViolatingPods {
			minNumPDBViolatingPods = numPDBViolatingPods
			minNodes1 = nil
			lenNodes1 = 0
		}
		if numPDBViolatingPods == minNumPDBViolatingPods {
			minNodes1 = append(minNodes1, node)
			lenNodes1++
		}
	}
	
	//计算单个候选的牺牲者优先级最大的,但和其他候选者相比优先级却是最小的
	for i := 0; i < lenNodes1; i++ {
		node := minNodes1[i]
		victims := nodesToVictims[node]
		// highestPodPriority is the highest priority among the victims on this node.
		highestPodPriority := podutil.GetPodPriority(victims.Pods[0])
		if highestPodPriority < minHighestPriority {
			minHighestPriority = highestPodPriority
			lenNodes2 = 0
		}
		if highestPodPriority == minHighestPriority {
			minNodes2[lenNodes2] = node
			lenNodes2++
		}
	}

	//计算所有牺牲者优先级总和最小的
	for i := 0; i < lenNodes2; i++ {
		var sumPriorities int64
		node := minNodes2[i]
		for _, pod := range nodesToVictims[node].Pods {
			// We add MaxInt32+1 to all priorities to make all of them >= 0. This is
			// needed so that a node with a few pods with negative priority is not
			// picked over a node with a smaller number of pods with the same negative
			// priority (and similar scenarios).
			sumPriorities += int64(podutil.GetPodPriority(pod)) + int64(math.MaxInt32+1)
		}
		if sumPriorities < minSumPriorities {
			minSumPriorities = sumPriorities
			lenNodes1 = 0
		}
		if sumPriorities == minSumPriorities {
			minNodes1[lenNodes1] = node
			lenNodes1++
		}
	}

	//计算所有牺牲者数量最少的
	for i := 0; i < lenNodes1; i++ {
		node := minNodes1[i]
		numPods := len(nodesToVictims[node].Pods)
		if numPods < minNumPods {
			minNumPods = numPods
			lenNodes2 = 0
		}
		if numPods == minNumPods {
			minNodes2[lenNodes2] = node
			lenNodes2++
		}
	}
	//GetEarliestPodStartTime是获取优先级最高又跑了最久的Pod的启动时间
	latestStartTime := util.GetEarliestPodStartTime(nodesToVictims[minNodes2[0]])
	if latestStartTime == nil {
		// If the earliest start time of all pods on the 1st node is nil, just return it,
		// which is not expected to happen.
		klog.Errorf("earliestStartTime is nil for node %s. Should not reach here.", minNodes2[0])
		return minNodes2[0]
	}
	//计算GetEarliestPodStartTime,挑一个最大值,意味着找最晚启动的来牺牲,让跑得久的先稳定着。
	for i := 1; i < lenNodes2; i++ {
		node := minNodes2[i]
		// Get earliest start time of all pods on the current node.
		earliestStartTimeOnNode := util.GetEarliestPodStartTime(nodesToVictims[node])
		if earliestStartTimeOnNode == nil {
			klog.Errorf("earliestStartTime is nil for node %s. Should not reach here.", node)
			continue
		}
		if earliestStartTimeOnNode.After(latestStartTime.Time) {
			latestStartTime = earliestStartTimeOnNode
			nodeToReturn = node
		}
	}
}

上一篇文章说挑选最优候选者的时候,有6个标准,而pickOneNodeForPreemption函数只涵盖了5个,其实最后一个就是SelectCandidate调用pickOneNodeForPreemption函数调用后还找不出最优候选者时,就默认拿候选者集合的第一个作为最优候选者。

PrepareCandidate

当选定了最优候选者后,调用PrepareCandidate执行准备工作。准备工作就包含

  • 驱逐牺牲者(看源码实际是删除)
  • Reject waitingMap里面的牺牲者
  • 把抢占目标Node中其他抢占到该节点上的优先级较低的Pod也清除了(实际就更新那些Pod的Status.NominatedNodeName字段,让他们恢复抢占前的状态)
func PrepareCandidate(c Candidate, fh framework.FrameworkHandle, cs kubernetes.Interface, pod *v1.Pod) error {
	for _, victim := range c.Victims().Pods {
		//驱逐Pod
		if err := util.DeletePod(cs, victim); err != nil {
			klog.Errorf("Error preempting pod %v/%v: %v", victim.Namespace, victim.Name, err)
			return err
		}
		//拒绝Pod
		if waitingPod := fh.GetWaitingPod(victim.UID); waitingPod != nil {
			waitingPod.Reject("preempted")
		}
	}
	//清除抢占目标Node中其他优先级较低的抢占Pod
	nominatedPods := getLowerPriorityNominatedPods(fh.PreemptHandle(), pod, c.Name())
	if err := util.ClearNominatedNodeName(cs, nominatedPods...); err != nil {
		klog.Errorf("Cannot clear 'NominatedNodeName' field: %v", err)
		// We do not return as this error is not critical.
	}

	return nil
}

util.DeletePod定义如下,代码位于/pkg/scheduler/util/utils.go

func DeletePod(cs kubernetes.Interface, pod *v1.Pod) error {
	return cs.CoreV1().Pods(pod.Namespace).Delete(context.TODO(), pod.Name, metav1.DeleteOptions{})
}

回归到scheduleOne

抢占逻辑执行完毕后,回到Scheduler.scheduleOne函数,先记录抢占目标的节点名,再调用Scheduler.recordSchedulingFailure方法

	if status.IsSuccess() && result != nil {
		nominatedNode = result.NominatedNodeName
	}
	....
	sched.recordSchedulingFailure(prof, podInfo, err, v1.PodReasonUnschedulable, nominatedNode)

Scheduler.recordSchedulingFailure先把Pod加到scheduler的SchedulingQueue队列中,再把将Pod的Status.NominatedNodeName字段更新,代码位于/pkg/scheduler/scheduler.go

func (sched *Scheduler) recordSchedulingFailure(prof *profile.Profile, podInfo *framework.QueuedPodInfo, err error, reason string, nominatedNode string) {
	sched.Error(podInfo, err)

	if sched.SchedulingQueue != nil {
		sched.SchedulingQueue.AddNominatedPod(podInfo.Pod, nominatedNode)
	}

	pod := podInfo.Pod
	prof.Recorder.Eventf(pod, nil, v1.EventTypeWarning, "FailedScheduling", "Scheduling", err.Error())
	if err := updatePod(sched.client, pod, &v1.PodCondition{...}, nominatedNode); err != nil {
	}
}

func updatePod(client clientset.Interface, pod *v1.Pod, condition *v1.PodCondition, nominatedNode string) error {
	podCopy := pod.DeepCopy()
	if !podutil.UpdatePodCondition(&podCopy.Status, condition) &&
		(len(nominatedNode) == 0 || pod.Status.NominatedNodeName == nominatedNode) {
		return nil
	}
	if nominatedNode != "" {
		podCopy.Status.NominatedNodeName = nominatedNode
	}
	return util.PatchPod(client, pod, podCopy)
}

上一篇就已经提到过抢占执行完毕并非是pod马上就可以调度到节点上,还是需要重新回到Scheduler的队列中,等待把选中的节点上面牺牲者Pod驱逐掉,腾出了资源,祈求能调度到选中的节点上而已。

Scheduler的SchedulingQueue队列

上面提到把抢占成功的Pod加到scheduler的SchedulingQueue队列中,下面介绍一下这个SchedulingQueue,另外上面代码中让pod重新入队让其能在下个周期能调度的调用是包含在sched.Error,而并非sched.SchedulingQueue.AddNominatedPod。

SchedulingQueue是scheduler结构的一个成员,它是一个定义在/pkg/scheduler/internal/queue/scheduling_queue.go的接口

type SchedulingQueue interface {
	framework.PodNominator
	Add(pod *v1.Pod) error
	AddUnschedulableIfNotPresent(pod *framework.QueuedPodInfo, podSchedulingCycle int64) error
	.....
	Run()
}

它继承了/pkg/scheduler/framework/v1alpha1/interface.go的PodNominator接口

type PodNominator interface {
	AddNominatedPod(pod *v1.Pod, nodeName string)
	DeleteNominatedPodIfExists(pod *v1.Pod)
	UpdateNominatedPod(oldPod, newPod *v1.Pod)
	NominatedPodsForNode(nodeName string) []*v1.Pod
}

在recordSchedulingFailure处调用的sched.SchedulingQueue.AddNominatedPod就是调用PodNominator接口的方法,作用是记录一个节点上抢占Pod,在预选调度时能执行addNominatedPods尝试把抢占Pod也加到节点上看待调度的Pod能否正常调度到节点上。

实现了SchedulingQueue接口的是位于/pkg/scheduler/internal/queue/scheduling_queue.go的PriorityQueue结构,它包含3个队列

type PriorityQueue struct {
	...
	// activeQ is heap structure that scheduler actively looks at to find pods to
	// schedule. Head of heap is the highest priority pod.
	activeQ *heap.Heap
	// podBackoffQ is a heap ordered by backoff expiry. Pods which have completed backoff
	// are popped from this heap before the scheduler looks at activeQ
	podBackoffQ *heap.Heap
	// unschedulableQ holds pods that have been tried and determined unschedulable.
	unschedulableQ *UnschedulablePodsMap
	...
}

activeQ队列是马上可以调度的队列,上篇介绍Pod调度流程时从sched.NextPod中取出Pod就是来源于activeQ;

剩余两个队列podBackoffQ和unschedulableQ是用来存放调度失败的Pod

activeQ队列中Pod的来源有几个,外部来源的则是从podInformer监听到apiserver有pod创建,调用链如下

informerFactory.Core().V1().Pods().Informer().AddEventHandler	/pkg/scheduler/eventhandlers.go
|--Scheduler.addPodToSchedulingQueue
   |--sched.SchedulingQueue.Add(pod)

内部来源则是从podBackoffQ和unschedulableQ转过去的,其中一个转移的地方在 Scheduler.Run的地方,它调用sched.SchedulingQueue.Run(),这就包含了两个定时调用函数,分别是把podBackoffQ移到activeQ,和把unschedulableQ移到activeQ或podBackoffQ中,
代码位于/pkg/scheduler/scheduler.go

func (sched *Scheduler) Run(ctx context.Context) {
	sched.SchedulingQueue.Run()
	wait.UntilWithContext(ctx, sched.scheduleOne, 0)
	sched.SchedulingQueue.Close()
}

代码位于/pkg/scheduler/internal/queue/scheduling_queue.go

func (p *PriorityQueue) Run() {
	//不断将BackoffQ里面的Pod,超过backoff time的Pod加到ActiveQ
	go wait.Until(p.flushBackoffQCompleted, 1.0*time.Second, p.stop)
	//筛选出UnschedulableQ中不可调度时间持续1分钟的,按其backofftime来分辨加入ActiveQ还是BackOffQ
	go wait.Until(p.flushUnschedulableQLeftover, 30*time.Second, p.stop)
}
func (p *PriorityQueue) flushBackoffQCompleted() {
	p.lock.Lock()
	defer p.lock.Unlock()
	for {
		rawPodInfo := p.podBackoffQ.Peek()
		if rawPodInfo == nil {
			return
		}
		pod := rawPodInfo.(*framework.QueuedPodInfo).Pod
		//pod backoff 的时间指数级增长
		boTime := p.getBackoffTime(rawPodInfo.(*framework.QueuedPodInfo))
		if boTime.After(p.clock.Now()) {
			return
		}
		_, err := p.podBackoffQ.Pop()
		if err != nil {
			klog.Errorf("Unable to pop pod %v from backoff queue despite backoff completion.", nsNameForPod(pod))
			return
		}
		p.activeQ.Add(rawPodInfo)
		metrics.SchedulerQueueIncomingPods.WithLabelValues("active", BackoffComplete).Inc()
		defer p.cond.Broadcast()
	}
}
func (p *PriorityQueue) flushUnschedulableQLeftover() {
	p.lock.Lock()
	defer p.lock.Unlock()

	var podsToMove []*framework.QueuedPodInfo
	currentTime := p.clock.Now()
	for _, pInfo := range p.unschedulableQ.podInfoMap {
		lastScheduleTime := pInfo.Timestamp
		if currentTime.Sub(lastScheduleTime) > unschedulableQTimeInterval {
			podsToMove = append(podsToMove, pInfo)
		}
	}

	if len(podsToMove) > 0 {
		p.movePodsToActiveOrBackoffQueue(podsToMove, UnschedulableTimeout)
	}
}
func (p *PriorityQueue) movePodsToActiveOrBackoffQueue(podInfoList []*framework.QueuedPodInfo, event string) {
	for _, pInfo := range podInfoList {
		pod := pInfo.Pod
		//按BackOff time来分辨 boTime.After(p.clock.Now())就留backOffQ
		//因为判断为否的时候flushBackoffQCompleted也会将其移到ActiveQ
		if p.isPodBackingoff(pInfo) {
			if err := p.podBackoffQ.Add(pInfo); err != nil {
				klog.Errorf("Error adding pod %v to the backoff queue: %v", pod.Name, err)
			} else {
				metrics.SchedulerQueueIncomingPods.WithLabelValues("backoff", event).Inc()
				p.unschedulableQ.delete(pod)
			}
		} else {
			if err := p.activeQ.Add(pInfo); err != nil {
				klog.Errorf("Error adding pod %v to the scheduling queue: %v", pod.Name, err)
			} else {
				metrics.SchedulerQueueIncomingPods.WithLabelValues("active", event).Inc()
				p.unschedulableQ.delete(pod)
			}
		}
	}
	p.moveRequestCycle = p.schedulingCycle
	p.cond.Broadcast()
}

将Pod加到podBackoffQ和unschedulableQ两个队列的地方仅有PriorityQueue的AddUnschedulableIfNotPresent方法,

func (p *PriorityQueue) AddUnschedulableIfNotPresent(pInfo *framework.QueuedPodInfo, podSchedulingCycle int64) error {
	...
	if p.moveRequestCycle >= podSchedulingCycle {
		if err := p.podBackoffQ.Add(pInfo); err != nil {
			return fmt.Errorf("error adding pod %v to the backoff queue: %v", pod.Name, err)
		}
	} else {
		p.unschedulableQ.addOrUpdate(pInfo)
	}

	p.PodNominator.AddNominatedPod(pod, "")
	return nil
}

而调用这个方法的来源也仅有SchedulerOne调度失败时调用了sched.Error,调用链如下

sched.recordSchedulingFailure	/pkg/scheduler/scheduler.go
|--sched.Error(podInfo, err)
|==MakeDefaultErrorFunc		/pkg/scheduler/factory.go
   |--podQueue.AddUnschedulableIfNotPresent

调用sched.recordSchedulingFailure的地方有多处,都在Scheduler.scheduleOne中,执行完预选调度或优选调度后,至绑定到某个节点前。

小结

本篇摊开讲了scheduler的抢占机制,抢占触发在一个Pod在预选调度失败之后,试图从现有节点中挑选可抢占的节点及抢占时需要驱逐牺牲的Pod,经过一系列计算筛选后选出一个最优的节点,驱逐上面的牺牲者后,将抢占的Pod放回scheduler的调度队列中,等待抢占Pod下次调度的一个流程。还额外的提及到scheduler的调度队列的类别,以及各个队列入队列和出队列的场景。

如有兴趣,可阅读鄙人“k8s源码之旅”系列的其他文章
kubelet源码分析——kubelet简介与启动
kubelet源码分析——启动Pod
kubelet源码分析——关闭Pod
kubelet源码分析——监控Pod变更
scheduler源码分析——调度流程
scheduler源码分析——preempt抢占
apiserver源码分析——启动流程
apiserver源码分析——处理请求

参考文章

kube-scheduler源码分析(六)之 preempt
scheduler之调度之优选(priority)与抢占(preempt)
kube-scheduler 优先级与抢占机制源码分析

posted @ 2021-10-09 16:34 猴健居士 阅读(605) 评论(0) 推荐(0) 编辑
摘要: 前言 上一篇说道k8s-apiserver如何启动,本篇则介绍apiserver启动后,接收到客户端请求的处理流程。如下图所示 认证与授权一般系统都会使用到,认证是鉴别访问apiserver的请求方是谁,一般情况下服务端是需要知晓客户端是谁方可接受请求,除了允许匿名访问这种场景,同时认证也为后续的授 阅读全文
posted @ 2021-10-06 09:27 猴健居士 阅读(1979) 评论(0) 推荐(0) 编辑
摘要: 前言 apiserver是k8s控制面的一个组件,在众多组件中唯一一个对接etcd,对外暴露http服务的形式为k8s中各种资源提供增删改查等服务。它是RESTful风格,每个资源的URI都会形如 /apis/{apiGroup}/{version}/namsspaces/{ns-name}/{re 阅读全文
posted @ 2021-10-04 15:59 猴健居士 阅读(1968) 评论(0) 推荐(0) 编辑
摘要: 前言 当api-server处理完一个pod的创建请求后,此时可以通过kubectl把pod get出来,但是pod的状态是Pending。在这个Pod能运行在节点上之前,它还需要经过scheduler的调度,为这个pod选择合适的节点运行。调度的整理流程如下图所示 本篇阅读源码版本1.19 调度的 阅读全文
posted @ 2021-10-03 09:22 猴健居士 阅读(743) 评论(0) 推荐(0) 编辑
摘要: 前言 前文介绍Pod无论是启动时还是关闭时,处理是由kubelet的主循环syncLoop开始执行逻辑,而syncLoop的入参是一条传递变更Pod的通道,显然syncLoop往后的逻辑属于消费者一方,如何发现Pod的变更往通道里面传递变更消息的一方目前还没明朗,故本次来看一下kubelet是如何发 阅读全文
posted @ 2021-10-02 08:53 猴健居士 阅读(922) 评论(0) 推荐(1) 编辑
摘要: 上一篇说到kublet如何启动一个pod,本篇讲述如何关闭一个Pod,引用一段来自官方文档介绍pod的生命周期的话 你使用 kubectl 工具手动删除某个特定的 Pod,而该 Pod 的体面终止限期是默认值(30 秒)。 API 服务器中的 Pod 对象被更新,记录涵盖体面终止限期在内 Pod 的 阅读全文
posted @ 2021-10-01 08:07 猴健居士 阅读(1126) 评论(0) 推荐(1) 编辑
摘要: 前文说到Kubelet启动时,调用到kubelet.Run方法,里面最核心的就是调用到kubelet.syncLoop。它是一个循环,这个循环里面有若干个检查和同步操作,其中一个是地在监听Pod的增删改事件,当一个Pod被Scheduler调度到某个Node之后,就会触发到kubelet.syncL 阅读全文
posted @ 2021-09-30 08:27 猴健居士 阅读(1520) 评论(0) 推荐(0) 编辑
摘要: kubelet是k8s集群中一个组件,其作为一个agent的角色分布在各个节点上,无论是master还是worker,功能繁多,逻辑复杂。主要功能有 节点状态同步:kublet给api-server同步当前节点的状态,会同步当前节点的CPU,内存及磁盘空间等资源到api-server,为schedu 阅读全文
posted @ 2021-09-29 09:17 猴健居士 阅读(2965) 评论(0) 推荐(0) 编辑
摘要: 经过前两篇的学习与实操,也大致掌握了一个k8s资源的Controller写法了,如有不熟,可回顾 自己实现一个Controller——标准型 自己实现一个Controller——精简型 但是目前也只能对k8s现有资源再继续扩展controller,万一遇到了CRD呢,进过本篇的学习与实操,你就懂了。 阅读全文
posted @ 2021-09-22 11:28 猴健居士 阅读(1563) 评论(0) 推荐(2) 编辑
摘要: 标准Controller 上一篇通过一个简单的例子,编写了一个controller-manager,以及一个极简单的controller。从而对controller的开发有个最基本的认识,但是细心观察前一篇实现的controller仅仅是每次全量获取了所有资源,虽然都是从缓存中获取速度是比较快的,如 阅读全文
posted @ 2021-09-21 08:11 猴健居士 阅读(816) 评论(0) 推荐(0) 编辑
摘要: 写在最前 controller-manager作为K8S master的其中一个组件,负责众多controller的启动和终止,这些controller负责监控着k8s中各种资源,执行调谐,使他们的实际状态能不断趋近与期望状态。这些controller包括servercontroller,nodec 阅读全文
posted @ 2021-09-20 08:05 猴健居士 阅读(937) 评论(0) 推荐(0) 编辑
摘要: 一个监控及告警的系统,内含一个TSDB(时序数据库)。在我而言是一个数采程序 重要成员分三块 exploter:实际是外部接口,让各个程序实现这个接口,供普罗米修斯定时从此接口中取数 alert:告警模块 prometheus:实际上是数采模块+存储模块,但是它的存储不是持久化的 普罗米修斯的数据是 阅读全文
posted @ 2019-01-25 08:32 猴健居士 阅读(835) 评论(0) 推荐(0) 编辑
摘要: Docker的容器环境实际上是借助类Linux命名空间,将各种系统资源按照容器不同划分了不同的命名空间进行隔离,为各个进程提供独立的运行环境关键概念:容器,镜像两个概念一起看,镜像好比平常系统中的各个可执行文件exe,每个可执行文件都会通过一个进程运行起来,容器则好比进程。镜像有镜像仓库,好比各大应 阅读全文
posted @ 2019-01-22 08:32 猴健居士 阅读(307) 评论(0) 推荐(0) 编辑
摘要: 在正式讲述虚拟内存之前需要提及存储器的层级结构以及进程在内存中的结构。 存储器的层级结构速度从快到慢排列如下 寄存器——L1高速缓存——L2高速缓存——L3高速缓存——主存——磁盘——分布式文件系统 而成本也是从高到低,空间是从低到高。 两个相邻的存储设备,前者往往是充当后者的高速缓存,后者往往存储 阅读全文
posted @ 2018-09-03 13:29 猴健居士 阅读(981) 评论(0) 推荐(0) 编辑
摘要: Windows的gcc环境,往官网http://sourceforge.net/project/showfiles.php?group_id=2435 下载MinGW,安装,安装完毕后按照包 配置环境变量 a.在PATH的值中加入"C:\Program Files\MinGWStudio\MinGW 阅读全文
posted @ 2017-09-19 08:39 猴健居士 阅读(3230) 评论(0) 推荐(0) 编辑
摘要: 先看这段代码 通过指针可以直接访问内存,而在C#中这属于不安全操作,为了能让代码编译运行因此都要带上unsafe,这个不用管它。这段代码主要是借助单字节数据类型byte,直接访问内存,查看各种数据类型的数据在内存的存放情况。在往常认为整数0,1,2,3,4,5……之类的存放在内存的就是转换成二进制再 阅读全文
posted @ 2017-05-25 08:23 猴健居士 阅读(795) 评论(0) 推荐(3) 编辑
摘要: 专用线程 计算限制的异步操作 CLR线程池,管理线程 Task 协作式取消 Timer await与async关键字 IO限制的异步操作 Windows的异步IO APM(APM与Task) EAP 专用线程 当初学习多线程编程的时候,第一步就是怎么去开一条新的线程,就是new一个Thread的实例 阅读全文
posted @ 2017-04-10 13:00 猴健居士 阅读(4201) 评论(0) 推荐(0) 编辑
摘要: 先从传统的Windows进程说起,传统的进程用来描述一组资源和程序运行所必需的内存分配。对于每个被加载到内存的可执行程序,在她的生命周期中操作系统会为之单独且隔离的进程。由于一个进程的失败不会影响其他的进程,使用这种方式,运行库环境将更加稳定。 而一个.NET的应用程序并非直接承载于一个传统的Win 阅读全文
posted @ 2017-02-21 08:18 猴健居士 阅读(3126) 评论(1) 推荐(0) 编辑
摘要: 多线程内容大致分两部分,其一是异步操作,可通过专用,线程池,Task,Parallel,PLINQ等,而这里又涉及工作线程与IO线程;其二是线程同步问题,鄙人现在学习与探究的是线程同步问题。 通过学习《CLR via C#》里面的内容,对线程同步形成了脉络较清晰的体系结构,在多线程中实现线程同步的是 阅读全文
posted @ 2017-01-19 12:13 猴健居士 阅读(2593) 评论(4) 推荐(1) 编辑
摘要: 本篇的内容在MSND中标注已是一项旧技术,而取而代之的是WCF, 那么我也放弃吧!但是这个属于Web服务的范畴,而WCF本质上也是一个Web服务来的,所以对于基础的东西还是不变的。那么这次就着重看看这个Web服务的基础知识。 首先还是形式上的列举一下webServices配置节的内容,尽管它现在没用 阅读全文
posted @ 2016-12-25 14:54 猴健居士 阅读(817) 评论(0) 推荐(0) 编辑
摘要: web部件是ASP.NET WebForm里面的服务器控件,它涵盖的内容比较多,鉴于这种状况的话鄙人不打算深究下去了,只是局限于了解web.config配置里面的配置内容则可。 那么也得稍微说说啥是Web部件。引用MSDN的话:ASP.NET Web 部件是一组集成控件,用于创建网站使最终用户可以直 阅读全文
posted @ 2016-12-23 12:49 猴健居士 阅读(2167) 评论(0) 推荐(0) 编辑
摘要: 此配置节只有一个属性——mode,该特性为 ASP.NET 应用程序指定 XHTML 呈现模式。它包含三个值 要让此配置生效,需要把<pages>配置节中的controlRenderingCompatibilityVersion 特性设置为 3.5 或网站针对 ASP.NET 3.5 或早期版本。否 阅读全文
posted @ 2016-12-13 08:00 猴健居士 阅读(602) 评论(0) 推荐(0) 编辑
摘要: 网上有用的资料不多,在一本电子书中摘抄了内容如下 webControls配置节只有一个clientScriptsLocation属性,此属性用于指定ASP.NET客户端脚本的默认存放路径。这些文件是包含在HTML代码生成的ASPX页面时这些需要的客户端功能,如智能导航和客户端控件验证。 <webCo 阅读全文
posted @ 2016-12-12 08:32 猴健居士 阅读(499) 评论(0) 推荐(0) 编辑
摘要: 此配置节的作用就是往Web程序中添加URL的映射,从而达到用户访问映射后的URL(如/Page/AAA)也能访问到源URL(如/Page/PageAAA.aspx)的效果。这也是URL映射本来的作用。 详细配置如下 其中要启用这个URL映射的必须要把enabled设置成true,add和remove 阅读全文
posted @ 2016-12-08 12:10 猴健居士 阅读(2269) 评论(0) 推荐(0) 编辑
摘要: 首先开篇引用《MVC2 2 in action》里面一段关于这个跟踪服务的话 When you called Trace.Write() in Web Forms, you were interacting with the Trace- Context class. This exists on 阅读全文
posted @ 2016-12-02 09:22 猴健居士 阅读(1960) 评论(0) 推荐(1) 编辑
摘要: ASP.NET 站点导航主要由与站点地图数据源通信的站点地图提供程序以及公开站点地图提供程序的功能的类构成。ASP.NET 站点导航使您能够将到您所有页面的链接存储在一个中心位置,并通过包含一个用于读取站点信息的 SiteMapDataSource 控件以及用于显示站点信息的导航 Web 服务器控件 阅读全文
posted @ 2016-11-29 08:37 猴健居士 阅读(586) 评论(0) 推荐(0) 编辑
摘要: HTTP 是一种无状态协议。这意味着 Web 服务器会将针对页面的每个 HTTP 请求作为独立的请求进行处理。ASP.NET 会话状态将来自限定时间范围内的同一浏览器的请求标识为一个会话,并提供用于在该会话持续期间内保留变量值的方法。默认情况下,将为所有 ASP.NET 应用程序启用 ASP.NET 阅读全文
posted @ 2016-11-26 11:22 猴健居士 阅读(1428) 评论(0) 推荐(0) 编辑
摘要: 这个配置节甚是简单,在MSDN中的介绍也甚是简单:为 ASP.NET 应用程序配置页的视图状态设置。 historySize的作用是设置要存储在页历史记录中的项数。 但是这根本是看不明白他是干嘛的,百度上一大串都是单纯说说配置节的意思,根本没再进一步阐述他的作用,我就不信其他人都懂了。还好有谷歌。看 阅读全文
posted @ 2016-11-19 10:53 猴健居士 阅读(664) 评论(0) 推荐(1) 编辑
摘要: securityPolicy配置节是定义一个安全策略文件与其信任级别名称之间的映射的集合。配置如下所示 其中name是指定映射到策略文件的命名的安全级别,一般的值有Full,Hight,Medium,Low,Minimal,UserDefined;policyFile指的是当前安全级别中对应的配置文 阅读全文
posted @ 2016-11-16 23:24 猴健居士 阅读(1846) 评论(0) 推荐(1) 编辑
摘要: 配置 Microsoft Internet 信息服务 (IIS) Web 服务器上的 ASP.NET 进程模型设置。其作用是配置IIS或IIS中的应用程序池(IIS7及以后版本)的安全性,性能,健壮性,可靠性。 processModel 节只能在 Machine.config 文件中进行设置,它影响 阅读全文
posted @ 2016-11-13 23:15 猴健居士 阅读(3415) 评论(0) 推荐(2) 编辑
摘要: 全局定义页特定配置设置,如配置文件范围内的页和控件的 ASP.NET 指令。能配置当前Web.config目录下的所有页面的设置。 与Pages上的部分设置可以在单独页面上通过@Page指令进行设置,Pages配置节的属性是@Page指令的子集,两者的属性说明则参考MSDN《pages 元素(ASP 阅读全文
posted @ 2016-11-06 21:27 猴健居士 阅读(3808) 评论(0) 推荐(0) 编辑
摘要: 此配置节的作用在于指定各种控件在不同类型的移动设备显示的适配器,以达到适应各种设备不同的展示形式。例子如下, 实际上这也是本配置节的默认配置的精简版。 各个节点和属性含义如下 device节点中,通过predicateClass中指定的类里面的predicateMethod指定的方法来判定当前这个设 阅读全文
posted @ 2016-11-02 08:59 猴健居士 阅读(584) 评论(0) 推荐(0) 编辑
摘要: membership成员资格是ASP.NET 成员资格为您提供了一种验证和存储用户凭据的内置方法。因此,ASP.NET 成员资格可帮助您管理网站中的用户身份验证。它包含以下功能 创建新用户和密码。 将成员资格信息(用户名、密码和支持数据)存储在 Microsoft SQL Server、Active 阅读全文
posted @ 2016-10-26 09:03 猴健居士 阅读(1299) 评论(2) 推荐(0) 编辑
摘要: 配置 ASP.NET HTTP 运行时设置,以确定如何处理对 ASP.NET 应用程序的请求,配置节及其描述如下所示。 <httpRuntime executionTimeout="110" 指定在被 ASP.NET 自动关闭前,允许执行请求的最大秒数 maxRequestLength="4096" 阅读全文
posted @ 2016-10-13 08:32 猴健居士 阅读(3562) 评论(2) 推荐(0) 编辑
摘要: ASP.NET HTTP 处理程序是响应对 ASP.NET Web 应用程序的请求而运行的过程(通常称为"终结点")。最常用的处理程序是处理 .aspx 文件的 ASP.NET 页处理程序。用户请求 .aspx 文件时,页通过页处理程序来处理请求。 ASP.NET 页处理程序仅仅是一种类型的处理程序 阅读全文
posted @ 2016-09-25 10:43 猴健居士 阅读(2436) 评论(1) 推荐(0) 编辑
摘要: httpModules是往当前应用程序添加HttpModule(http模块)的标签。配置节如下 提起httpModule不得不提一下Http请求处理流程 ASP.NET对请求处理的过程: 当请求一个*.aspx文件的时候,这个请求会被inetinfo.exe进程截获,它判断文件的后缀(aspx)之 阅读全文
posted @ 2016-09-24 16:53 猴健居士 阅读(2006) 评论(0) 推荐(0) 编辑
摘要: Web 应用程序使用的 Cookie 个人认为这里设置的cookie与访问cookie的安全性关联大一点,配置节如下 httpOnlyCookies:默认是false,作用是是否禁用浏览器脚本访问cookie。在Form认证时会颁发一个认证票写在cookie,最开始我以为这里设置了则可以访问,结果并 阅读全文
posted @ 2016-09-22 09:12 猴健居士 阅读(1857) 评论(0) 推荐(1) 编辑
摘要: 定义用来控制应用程序宿主环境的行为的配置设置。 配置如下 shadowCopyBinAssemblies:该值指示 Bin 目录中的应用程序的程序集是否影像复制到该应用程序的 ASP.NET 临时文件目录中。但纯看这句话我是一面懵懂的,幸亏看了一篇老外的文章经过自己实践才明白其作用。平时我们更新bi 阅读全文
posted @ 2016-09-21 16:36 猴健居士 阅读(2663) 评论(0) 推荐(3) 编辑
摘要: 配置针对应用程序的运行状况监视的一个服务 配置节内容比以往的较为复杂,如下 实际上这是运行状况监视是一个事件定义与处理的模型,简单来看整个运行状况监视基本点有以下三个 1.在eventMappings定义事件 2.在providers定义事件的处理 3.通过rules绑定事件给某个处理程序去处理。 阅读全文
posted @ 2016-09-20 09:09 猴健居士 阅读(1069) 评论(0) 推荐(0) 编辑
摘要: 本配置节是关于配置应用程序的全球化设置。 例如如下设置 请求时出现中文(字符编码不对),则会出现乱码,同样响应的页面中的中文也会出现乱码,正常配置时如下图所示 设置了响应编码为iso-8859-1或其他诸如此类ASCII让中文显示乱码的编码,则会出现 其响应的内容均使用了iso-8859-1而使得中 阅读全文
posted @ 2016-09-19 09:16 猴健居士 阅读(1330) 评论(0) 推荐(0) 编辑
点击右上角即可分享
微信分享提示