转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com

源码版本是1.19

上一篇我们将了获取node成功的情况,如果是一个优先pod获取node失败,那么就会进入到抢占环节中,那么抢占环节k8s会做什么呢,抢占是如何发生的,哪些资源会被抢占这些都是我们这篇要研究的内容。

调度的优先级与抢占机制

正常情况下,当一个 Pod 调度失败后,它就会被暂时“搁置”起来,直到 Pod 被更新,或者集群状态发生变化,调度器才会对这个 Pod 进行重新调度。但是我们可以通过PriorityClass优先级来避免这种情况。通过设置优先级一些优先级比较高的pod,如果pod 调度失败,那么并不会被”搁置”,而是会”挤走”某个 node 上的一些低优先级的 pod,这样就可以保证高优先级的 pod 调度成功。

要使用PriorityClass,首先我们要定义一个PriorityClass对象,例如:

  1. apiVersion: v1
  2. kind: PriorityClass
  3. metadata:
  4. name: high-priority
  5. value: 1000000
  6. globalDefault: false
  7. description: "This priority class should be used for XYZ service pods only."

value越高则优先级越高;globalDefault 被设置为 true 的话,那就意味着这个 PriorityClass 的值会成为系统的默认值,如果是false则表示我们只希望声明使用该 PriorityClass 的 Pod 拥有值为 1000000 的优先级,而对于没有声明 PriorityClass 的 Pod 来说,它们的优先级就是 0。

Pod 就可以声明使用它了:

  1. apiVersion: v1
  2. kind: Pod
  3. metadata:
  4. name: nginx
  5. labels:
  6. env: test
  7. spec:
  8. containers:
  9. - name: nginx
  10. image: nginx
  11. imagePullPolicy: IfNotPresent
  12. priorityClassName: high-priority

高优先级的 Pod 调度失败的时候,调度器的抢占能力就会被触发。调度器就会试图从当前集群里寻找一个节点,使得当这个节点上的一个或者多个低优先级 Pod 被删除后,待调度的高优先级 Pod 就可以被调度到这个节点上。

高优先级Pod进行抢占的时候会将pod的 nominatedNodeName 字段,设置为被抢占的 Node 的名字。然后,在下一周期中决定是不是要运行在被抢占的节点上,当这个Pod在等待的时候,如果有其他更高优先级的 Pod 也要抢占同一个节点,那么调度器就会清空原抢占者的 spec.nominatedNodeName 字段,从而允许更高优先级的抢占者执行抢占。

源码解析

这里我依旧拿出这张图来进行讲解,上一篇我们将了获取node成功的情况,如果是一个优先pod获取node失败,那么就会进入到抢占环节中。

通过上一篇的分析,我们知道,在scheduleOne方法中执行sched.Algorithm.Schedule会选择一个合适的node节点,如果获取node失败,那么就会进入到一个if逻辑中执行抢占。

代码路径:pkg/scheduler/scheduler.go

  1. func (sched *Scheduler) scheduleOne(ctx context.Context) {
  2. ...
  3. //为pod资源对象选择一个合适的节点
  4. scheduleResult, err := sched.Algorithm.Schedule(schedulingCycleCtx, prof, state, pod)
  5. //获取node失败,抢占逻辑
  6. if err != nil {
  7. //上面调用失败之后,下面会根据pod执行抢占
  8. nominatedNode := ""
  9. if fitError, ok := err.(*core.FitError); ok {
  10. if !prof.HasPostFilterPlugins() {
  11. klog.V(3).Infof("No PostFilter plugins are registered, so no preemption will be performed.")
  12. } else {
  13. result, status := prof.RunPostFilterPlugins(ctx, state, pod, fitError.FilteredNodesStatuses)
  14. if status.Code() == framework.Error {
  15. klog.Errorf("Status after running PostFilter plugins for pod %v/%v: %v", pod.Namespace, pod.Name, status)
  16. } else {
  17. klog.V(5).Infof("Status after running PostFilter plugins for pod %v/%v: %v", pod.Namespace, pod.Name, status)
  18. }
  19. //抢占成功后,将nominatedNodeName设置为被抢占的 Node 的名字,然后重新进入下一个调度周期
  20. if status.IsSuccess() && result != nil {
  21. nominatedNode = result.NominatedNodeName
  22. }
  23. }
  24. metrics.PodUnschedulable(prof.Name, metrics.SinceInSeconds(start))
  25. } else if err == core.ErrNoNodesAvailable {
  26. metrics.PodUnschedulable(prof.Name, metrics.SinceInSeconds(start))
  27. } else {
  28. klog.ErrorS(err, "Error selecting node for pod", "pod", klog.KObj(pod))
  29. metrics.PodScheduleError(prof.Name, metrics.SinceInSeconds(start))
  30. }
  31. sched.recordSchedulingFailure(prof, podInfo, err, v1.PodReasonUnschedulable, nominatedNode)
  32. return
  33. }
  34. ...
  35. }

在这个方法里面RunPostFilterPlugins会执行具体的抢占逻辑,然后返回被抢占的node节点。抢占者并不会立刻被调度到被抢占的 node 上,调度器只会将抢占者的 status.nominatedNodeName 字段设置为被抢占的 node 的名字。然后,抢占者会重新进入下一个调度周期,在新的调度周期里来决定是不是要运行在被抢占的节点上,当然,即使在下一个调度周期,调度器也不会保证抢占者一定会运行在被抢占的节点上。

这样设计的一个重要原因是调度器只会通过标准的 DELETE API 来删除被抢占的 pod,所以,这些 pod 必然是有一定的“优雅退出”时间(默认是 30s)的。而在这段时间里,其他的节点也是有可能变成可调度的,或者直接有新的节点被添加到这个集群中来。

而在抢占者等待被调度的过程中,如果有其他更高优先级的 pod 也要抢占同一个节点,那么调度器就会清空原抢占者的 status.nominatedNodeName 字段,从而允许更高优先级的抢占者执行抢占,并且,这也使得原抢占者本身也有机会去重新抢占其他节点。

接着我们继续看,RunPostFilterPlugins会遍历所有的postFilterPlugins,然后执行runPostFilterPlugin方法:

  1. func (f *frameworkImpl) RunPostFilterPlugins(ctx context.Context, state *framework.CycleState, pod *v1.Pod, filteredNodeStatusMap framework.NodeToStatusMap) (_ *framework.PostFilterResult, status *framework.Status) {
  2. startTime := time.Now()
  3. defer func() {
  4. metrics.FrameworkExtensionPointDuration.WithLabelValues(postFilter, status.Code().String(), f.profileName).Observe(metrics.SinceInSeconds(startTime))
  5. }()
  6. statuses := make(framework.PluginToStatus)
  7. //postFilterPlugins里面只有一个defaultpreemption
  8. for _, pl := range f.postFilterPlugins {
  9. r, s := f.runPostFilterPlugin(ctx, pl, state, pod, filteredNodeStatusMap)
  10. if s.IsSuccess() {
  11. return r, s
  12. } else if !s.IsUnschedulable() {
  13. // Any status other than Success or Unschedulable is Error.
  14. return nil, framework.NewStatus(framework.Error, s.Message())
  15. }
  16. statuses[pl.Name()] = s
  17. }
  18. return nil, statuses.Merge()
  19. }

根据我们上一节看的scheduler的初始化可以知道设置的PostFilter如下:

代码路径:pkg/scheduler/algorithmprovider/registry.go

  1. PostFilter: &schedulerapi.PluginSet{
  2. Enabled: []schedulerapi.Plugin{
  3. {Name: defaultpreemption.Name},
  4. },
  5. },

可见,目前只有一个defaultpreemption来执行抢占逻辑,在postFilterPlugins循环里面会调用到runPostFilterPlugin然后运行defaultpreemption的PostFilter方法,最后执行到preempt执行具体抢占逻辑。

代码路径:pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.go

  1. func (pl *DefaultPreemption) PostFilter(...) (*framework.PostFilterResult, *framework.Status) {
  2. ...
  3. //执行抢占
  4. nnn, err := pl.preempt(ctx, state, pod, m)
  5. ...
  6. return &framework.PostFilterResult{NominatedNodeName: nnn}, framework.NewStatus(framework.Success)
  7. }

抢占的执行流程图如下:

代码路径:pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.go

  1. func (pl *DefaultPreemption) preempt(ctx context.Context, state *framework.CycleState, pod *v1.Pod, m framework.NodeToStatusMap) (string, error) {
  2. cs := pl.fh.ClientSet()
  3. ph := pl.fh.PreemptHandle()
  4. //返回node列表
  5. nodeLister := pl.fh.SnapshotSharedLister().NodeInfos()
  6. pod, err := util.GetUpdatedPod(cs, pod)
  7. if err != nil {
  8. klog.Errorf("Error getting the updated preemptor pod object: %v", err)
  9. return "", err
  10. }
  11. //确认抢占者是否能够进行抢占,如果对应的node节点上的pod正在优雅退出(Graceful Termination ),那么就不应该进行抢占
  12. if !PodEligibleToPreemptOthers(pod, nodeLister, m[pod.Status.NominatedNodeName]) {
  13. klog.V(5).Infof("Pod %v/%v is not eligible for more preemption.", pod.Namespace, pod.Name)
  14. return "", nil
  15. }
  16. // 查找所有抢占候选者
  17. candidates, err := FindCandidates(ctx, cs, state, pod, m, ph, nodeLister, pl.pdbLister)
  18. if err != nil || len(candidates) == 0 {
  19. return "", err
  20. }
  21. //若有 extender 则执行
  22. candidates, err = CallExtenders(ph.Extenders(), pod, nodeLister, candidates)
  23. if err != nil {
  24. return "", err
  25. }
  26. // 查找最佳抢占候选者
  27. bestCandidate := SelectCandidate(candidates)
  28. if bestCandidate == nil || len(bestCandidate.Name()) == 0 {
  29. return "", nil
  30. }
  31. // 在抢占一个node之前做一些准备工作
  32. if err := PrepareCandidate(bestCandidate, pl.fh, cs, pod); err != nil {
  33. return "", err
  34. }
  35. return bestCandidate.Name(), nil
  36. }

preempt方法首先会去获取node列表,然后获取最新的要执行抢占的pod信息,接着分下面几步执行抢占:

  1. 调用PodEligibleToPreemptOthers方法,检查抢占者是否能够进行抢占,如果当前的pod已经抢占了一个node节点或者在被抢占node节点中有pod正在执行优雅退出,那么不应该执行抢占;
  2. 调用FindCandidates找到所有node中能被抢占的node节点,并返回候选列表以及node节点中需要被删除的pod;
  3. 若有 extender 则执行CallExtenders;
  4. 调用SelectCandidate方法在所有候选列表中找出最合适的node节点执行抢占;
  5. 调用PrepareCandidate方法删除被抢占的node节点中victim(牺牲者),以及清除NominatedNodeName字段信息;

PodEligibleToPreemptOthers

  1. func PodEligibleToPreemptOthers(pod *v1.Pod, nodeInfos framework.NodeInfoLister, nominatedNodeStatus *framework.Status) bool {
  2. if pod.Spec.PreemptionPolicy != nil && *pod.Spec.PreemptionPolicy == v1.PreemptNever {
  3. 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)
  4. return false
  5. }
  6. //查看抢占者是否已经抢占过
  7. nomNodeName := pod.Status.NominatedNodeName
  8. if len(nomNodeName) > 0 {
  9. if nominatedNodeStatus.Code() == framework.UnschedulableAndUnresolvable {
  10. return true
  11. }
  12. //获取被抢占的node节点
  13. if nodeInfo, _ := nodeInfos.Get(nomNodeName); nodeInfo != nil {
  14. //查看是否存在正在被删除并且优先级比抢占者pod低的pod
  15. podPriority := podutil.GetPodPriority(pod)
  16. for _, p := range nodeInfo.Pods {
  17. if p.Pod.DeletionTimestamp != nil && podutil.GetPodPriority(p.Pod) < podPriority {
  18. // There is a terminating pod on the nominated node.
  19. return false
  20. }
  21. }
  22. }
  23. }
  24. return true
  25. }

这个方法会检查该pod是否已经抢占过其他node节点,如果是的话就遍历节点上的所有pod对象,如果发现节点上有pod资源对象的优先级小于待调度pod资源对象并处于终止状态,则返回false,不会发生抢占。

接下来看FindCandidates方法:

FindCandidates

  1. func FindCandidates(ctx context.Context, cs kubernetes.Interface, state *framework.CycleState, pod *v1.Pod,
  2. m framework.NodeToStatusMap, ph framework.PreemptHandle, nodeLister framework.NodeInfoLister,
  3. pdbLister policylisters.PodDisruptionBudgetLister) ([]Candidate, error) {
  4. allNodes, err := nodeLister.List()
  5. if err != nil {
  6. return nil, err
  7. }
  8. if len(allNodes) == 0 {
  9. return nil, core.ErrNoNodesAvailable
  10. }
  11. //找 predicates 阶段失败但是通过抢占也许能够调度成功的 nodes
  12. potentialNodes := nodesWherePreemptionMightHelp(allNodes, m)
  13. if len(potentialNodes) == 0 {
  14. klog.V(3).Infof("Preemption will not help schedule pod %v/%v on any node.", pod.Namespace, pod.Name)
  15. if err := util.ClearNominatedNodeName(cs, pod); err != nil {
  16. klog.Errorf("Cannot clear 'NominatedNodeName' field of pod %v/%v: %v", pod.Namespace, pod.Name, err)
  17. }
  18. return nil, nil
  19. }
  20. if klog.V(5).Enabled() {
  21. var sample []string
  22. for i := 0; i < 10 && i < len(potentialNodes); i++ {
  23. sample = append(sample, potentialNodes[i].Node().Name)
  24. }
  25. klog.Infof("%v potential nodes for preemption, first %v are: %v", len(potentialNodes), len(sample), sample)
  26. }
  27. //获取PDB对象,PDB能够限制同时终端的pod资源对象的数量,以保证集群的高可用性
  28. pdbs, err := getPodDisruptionBudgets(pdbLister)
  29. if err != nil {
  30. return nil, err
  31. }
  32. //寻找符合条件的node,并封装成candidate数组返回
  33. return dryRunPreemption(ctx, ph, state, pod, potentialNodes, pdbs), nil
  34. }

FindCandidates方法首先会获取node列表,然后调用nodesWherePreemptionMightHelp方法来找出predicates 阶段失败但是通过抢占也许能够调度成功的nodes,因为并不是所有的node都可以通过抢占来调度成功。最后调用dryRunPreemption方法来获取符合条件的node节点。

dryRunPreemption

  1. func dryRunPreemption(ctx context.Context, fh framework.PreemptHandle, state *framework.CycleState,
  2. pod *v1.Pod, potentialNodes []*framework.NodeInfo, pdbs []*policy.PodDisruptionBudget) []Candidate {
  3. var resultLock sync.Mutex
  4. var candidates []Candidate
  5. checkNode := func(i int) {
  6. nodeInfoCopy := potentialNodes[i].Clone()
  7. stateCopy := state.Clone()
  8. //找到node上被抢占的pod,也就是victims
  9. pods, numPDBViolations, fits := selectVictimsOnNode(ctx, fh, stateCopy, pod, nodeInfoCopy, pdbs)
  10. if fits {
  11. resultLock.Lock()
  12. victims := extenderv1.Victims{
  13. Pods: pods,
  14. NumPDBViolations: int64(numPDBViolations),
  15. }
  16. c := candidate{
  17. victims: &victims,
  18. name: nodeInfoCopy.Node().Name,
  19. }
  20. candidates = append(candidates, &c)
  21. resultLock.Unlock()
  22. }
  23. }
  24. parallelize.Until(ctx, len(potentialNodes), checkNode)
  25. return candidates
  26. }

这里会开启16个线程调用checkNode方法,checkNode方法里面会调用selectVictimsOnNode方法来检查这个node是不是能被执行抢占,如果能执行抢占返回的pods表示需要删除的节点,然后封装成candidate添加到candidates列表中返回。

selectVictimsOnNode

  1. func selectVictimsOnNode(
  2. ctx context.Context,
  3. ph framework.PreemptHandle,
  4. state *framework.CycleState,
  5. pod *v1.Pod,
  6. nodeInfo *framework.NodeInfo,
  7. pdbs []*policy.PodDisruptionBudget,
  8. ) ([]*v1.Pod, int, bool) {
  9. var potentialVictims []*v1.Pod
  10. //移除node节点的pod
  11. removePod := func(rp *v1.Pod) error {
  12. if err := nodeInfo.RemovePod(rp); err != nil {
  13. return err
  14. }
  15. status := ph.RunPreFilterExtensionRemovePod(ctx, state, pod, rp, nodeInfo)
  16. if !status.IsSuccess() {
  17. return status.AsError()
  18. }
  19. return nil
  20. }
  21. //将node节点添加pod
  22. addPod := func(ap *v1.Pod) error {
  23. nodeInfo.AddPod(ap)
  24. status := ph.RunPreFilterExtensionAddPod(ctx, state, pod, ap, nodeInfo)
  25. if !status.IsSuccess() {
  26. return status.AsError()
  27. }
  28. return nil
  29. }
  30. // 获取pod的优先级,并将node中所有优先级低于该pod的调用removePod方法pod移除
  31. podPriority := podutil.GetPodPriority(pod)
  32. for _, p := range nodeInfo.Pods {
  33. if podutil.GetPodPriority(p.Pod) < podPriority {
  34. potentialVictims = append(potentialVictims, p.Pod)
  35. if err := removePod(p.Pod); err != nil {
  36. return nil, 0, false
  37. }
  38. }
  39. }
  40. //没有优先级低的node,直接返回
  41. if len(potentialVictims) == 0 {
  42. return nil, 0, false
  43. }
  44. if fits, _, err := core.PodPassesFiltersOnNode(ctx, ph, state, pod, nodeInfo); !fits {
  45. if err != nil {
  46. klog.Warningf("Encountered error while selecting victims on node %v: %v", nodeInfo.Node().Name, err)
  47. }
  48. return nil, 0, false
  49. }
  50. var victims []*v1.Pod
  51. numViolatingVictim := 0
  52. //将potentialVictims集合里的pod按照优先级进行排序
  53. sort.Slice(potentialVictims, func(i, j int) bool { return util.MoreImportantPod(potentialVictims[i], potentialVictims[j]) })
  54. //将pdb的pod分离出来
  55. //基于 pod 是否有 PDB 被分为两组 violatingVictims 和 nonViolatingVictims
  56. //PDB:https://kubernetes.io/docs/concepts/workloads/pods/disruptions/
  57. violatingVictims, nonViolatingVictims := filterPodsWithPDBViolation(potentialVictims, pdbs)
  58. reprievePod := func(p *v1.Pod) (bool, error) {
  59. if err := addPod(p); err != nil {
  60. return false, err
  61. }
  62. fits, _, _ := core.PodPassesFiltersOnNode(ctx, ph, state, pod, nodeInfo)
  63. if !fits {
  64. if err := removePod(p); err != nil {
  65. return false, err
  66. }
  67. // 加入到 victims 中
  68. victims = append(victims, p)
  69. klog.V(5).Infof("Pod %v/%v is a potential preemption victim on node %v.", p.Namespace, p.Name, nodeInfo.Node().Name)
  70. }
  71. return fits, nil
  72. }
  73. //删除pod,并记录删除个数
  74. for _, p := range violatingVictims {
  75. if fits, err := reprievePod(p); err != nil {
  76. klog.Warningf("Failed to reprieve pod %q: %v", p.Name, err)
  77. return nil, 0, false
  78. } else if !fits {
  79. numViolatingVictim++
  80. }
  81. }
  82. //删除pod
  83. for _, p := range nonViolatingVictims {
  84. if _, err := reprievePod(p); err != nil {
  85. klog.Warningf("Failed to reprieve pod %q: %v", p.Name, err)
  86. return nil, 0, false
  87. }
  88. }
  89. return victims, numViolatingVictim, true
  90. }

这个方法首先定义了两个方法,一个是removePod,另一个是addPod,这两个方法都差不多,如果是removePod就会将pod从node中移除,然后修改node一些属性,如将Requested.MilliCPU、Requested.Memory中减去,表示已用资源大小,将该pod从node节点的Pods列表中移除等等。

回到selectVictimsOnNode继续往下,会遍历node里面的pod列表,如果找到优先级小于抢占pod的就加入到potentialVictims集合中,并调用removePod方法,将当前被遍历的pod从node中移除。

接着会调用PodPassesFiltersOnNode方法,这个方法会运行两次。第一次会调用addNominatedPods方法将调度队列中找到节点上优先级大于或等于当前pod资源对象的nominatedPods加入到nodeInfo对象中,然后执行FilterPlugin列表;第二次则直接执行FilterPlugins列表。之所以要这么做,是由于亲和性的关系,k8s需要判断当前调度的pod亲和性是否依赖了nominatedPods。

继续往下会对potentialVictims按照优先级进行排序,优先级高的在前面。

接着会调用filterPodsWithPDBViolation方法,将 PDB 约束的 Pod和未约束的Pod分离成两个组,然后会分别遍历violatingVictims和nonViolatingVictims调用reprievePod方法对pod进行移除。这里我们在官方文档也可以看其设计理念,PodDisruptionBudget 是在抢占中被支持的,但不提供保证,然后将被移除的pod添加到victims列表中,并记录好被删除的删除pod个数,最后返回。

到这里整个FindCandidates方法就探索完毕了,还是比较长的,我们继续回到preempt方法中往下看,SelectCandidate方法会查找最佳抢占候选者。

SelectCandidate

  1. func SelectCandidate(candidates []Candidate) Candidate {
  2. if len(candidates) == 0 {
  3. return nil
  4. }
  5. if len(candidates) == 1 {
  6. return candidates[0]
  7. }
  8. victimsMap := candidatesToVictimsMap(candidates)
  9. // 选择1个 node 用于 schedule
  10. candidateNode := pickOneNodeForPreemption(victimsMap)
  11. for _, candidate := range candidates {
  12. if candidateNode == candidate.Name() {
  13. return candidate
  14. }
  15. }
  16. klog.Errorf("None candidate can be picked from %v.", candidates)
  17. return candidates[0]
  18. }

这个方法里面会调用candidatesToVictimsMap方法做一个name和victims映射map,然后调用pickOneNodeForPreemption执行主要过滤逻辑。

pickOneNodeForPreemption

  1. func pickOneNodeForPreemption(nodesToVictims map[string]*extenderv1.Victims) string {
  2. //若该 node 没有 victims 则返回
  3. if len(nodesToVictims) == 0 {
  4. return ""
  5. }
  6. minNumPDBViolatingPods := int64(math.MaxInt32)
  7. var minNodes1 []string
  8. lenNodes1 := 0
  9. //寻找 PDB violations 数量最小的 node
  10. for node, victims := range nodesToVictims {
  11. numPDBViolatingPods := victims.NumPDBViolations
  12. if numPDBViolatingPods < minNumPDBViolatingPods {
  13. minNumPDBViolatingPods = numPDBViolatingPods
  14. minNodes1 = nil
  15. lenNodes1 = 0
  16. }
  17. if numPDBViolatingPods == minNumPDBViolatingPods {
  18. minNodes1 = append(minNodes1, node)
  19. lenNodes1++
  20. }
  21. }
  22. //如果最小的node只有一个,直接返回
  23. if lenNodes1 == 1 {
  24. return minNodes1[0]
  25. }
  26. minHighestPriority := int32(math.MaxInt32)
  27. var minNodes2 = make([]string, lenNodes1)
  28. lenNodes2 := 0
  29. // 找到node里面pods 最高优先级最小的
  30. for i := 0; i < lenNodes1; i++ {
  31. node := minNodes1[i]
  32. victims := nodesToVictims[node]
  33. highestPodPriority := podutil.GetPodPriority(victims.Pods[0])
  34. if highestPodPriority < minHighestPriority {
  35. minHighestPriority = highestPodPriority
  36. lenNodes2 = 0
  37. }
  38. if highestPodPriority == minHighestPriority {
  39. minNodes2[lenNodes2] = node
  40. lenNodes2++
  41. }
  42. }
  43. if lenNodes2 == 1 {
  44. return minNodes2[0]
  45. }
  46. // 找出node里面Victims列表优先级加和最小的
  47. minSumPriorities := int64(math.MaxInt64)
  48. lenNodes1 = 0
  49. for i := 0; i < lenNodes2; i++ {
  50. var sumPriorities int64
  51. node := minNodes2[i]
  52. for _, pod := range nodesToVictims[node].Pods {
  53. sumPriorities += int64(podutil.GetPodPriority(pod)) + int64(math.MaxInt32+1)
  54. }
  55. if sumPriorities < minSumPriorities {
  56. minSumPriorities = sumPriorities
  57. lenNodes1 = 0
  58. }
  59. if sumPriorities == minSumPriorities {
  60. minNodes1[lenNodes1] = node
  61. lenNodes1++
  62. }
  63. }
  64. if lenNodes1 == 1 {
  65. return minNodes1[0]
  66. }
  67. // 找到node列表中需要牺牲的pod数量最小的
  68. minNumPods := math.MaxInt32
  69. lenNodes2 = 0
  70. for i := 0; i < lenNodes1; i++ {
  71. node := minNodes1[i]
  72. numPods := len(nodesToVictims[node].Pods)
  73. if numPods < minNumPods {
  74. minNumPods = numPods
  75. lenNodes2 = 0
  76. }
  77. if numPods == minNumPods {
  78. minNodes2[lenNodes2] = node
  79. lenNodes2++
  80. }
  81. }
  82. if lenNodes2 == 1 {
  83. return minNodes2[0]
  84. }
  85. //若多个 node 的 pod 数量相等,则选出高优先级 pod 启动时间最短的
  86. latestStartTime := util.GetEarliestPodStartTime(nodesToVictims[minNodes2[0]])
  87. if latestStartTime == nil {
  88. klog.Errorf("earliestStartTime is nil for node %s. Should not reach here.", minNodes2[0])
  89. return minNodes2[0]
  90. }
  91. nodeToReturn := minNodes2[0]
  92. for i := 1; i < lenNodes2; i++ {
  93. node := minNodes2[i]
  94. earliestStartTimeOnNode := util.GetEarliestPodStartTime(nodesToVictims[node])
  95. if earliestStartTimeOnNode == nil {
  96. klog.Errorf("earliestStartTime is nil for node %s. Should not reach here.", node)
  97. continue
  98. }
  99. if earliestStartTimeOnNode.After(latestStartTime.Time) {
  100. latestStartTime = earliestStartTimeOnNode
  101. nodeToReturn = node
  102. }
  103. }
  104. return nodeToReturn
  105. }

这个方法看起来很长,其实逻辑十分的清晰:

  1. 找出最少的的PDB violations的node节点,如果找出的node集合大于1则往下走;
  2. 找出找到node里面pods 最高优先级最小的node,如果还是找出的node集合大于1则往下走;
  3. 找出node里面Victims列表优先级加和最小的,如果还是找出的node集合大于1则往下走;
  4. 找到node列表中需要牺牲的pod数量最小的,如果还是找出的node集合大于1则往下走;
  5. 若多个 node 的 pod 数量相等,则选出高优先级 pod 启动时间最短的,然后返回。

然后preempt方法往下走到调用PrepareCandidate方法:

PrepareCandidate

  1. func PrepareCandidate(c Candidate, fh framework.FrameworkHandle, cs kubernetes.Interface, pod *v1.Pod) error {
  2. for _, victim := range c.Victims().Pods {
  3. if err := util.DeletePod(cs, victim); err != nil {
  4. klog.Errorf("Error preempting pod %v/%v: %v", victim.Namespace, victim.Name, err)
  5. return err
  6. }
  7. if waitingPod := fh.GetWaitingPod(victim.UID); waitingPod != nil {
  8. waitingPod.Reject("preempted")
  9. }
  10. fh.EventRecorder().Eventf(victim, pod, v1.EventTypeNormal, "Preempted", "Preempting", "Preempted by %v/%v on node %v",
  11. pod.Namespace, pod.Name, c.Name())
  12. }
  13. metrics.PreemptionVictims.Observe(float64(len(c.Victims().Pods)))
  14. //移除低优先级 pod 的 Nominated,更新这些 pod,移动到 activeQ 队列中,让调度器为这些 pod 重新 bind node
  15. nominatedPods := getLowerPriorityNominatedPods(fh.PreemptHandle(), pod, c.Name())
  16. if err := util.ClearNominatedNodeName(cs, nominatedPods...); err != nil {
  17. klog.Errorf("Cannot clear 'NominatedNodeName' field: %v", err)
  18. // We do not return as this error is not critical.
  19. }
  20. return nil
  21. }

这个方法会调用DeletePod删除Victims列表里面的pod,然后将这些pod中的Status.NominatedNodeName属性置空。

到这里整个抢占过程就讲解完毕了~

总结

看完这一篇我们对k8s的抢占可以说有一个全局的了解,心里应该非常清楚k8s在抢占的时候会发生什么,例如什么时候时候哪些pod会执行抢占,以及为什么执行抢占,以及抢占了哪些pod的资源,对于被抢占的pod会不会重新被调度等等。

Reference

https://kubernetes.io/docs/concepts/scheduling-eviction/scheduling-framework/

https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/

https://kubernetes.io/docs/concepts/configuration/pod-overhead/

https://kubernetes.io/docs/concepts/workloads/pods/disruptions/

https://kubernetes.io/docs/tasks/run-application/configure-pdb/

https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes-using-node-affinity/

10.深入k8s:调度的优先级及抢占机制源码分析的更多相关文章

  1. 【freertos】011-信号量、互斥量及优先级继承机制源码分析

    目录 前言 11.1 任务同步 11.2 信号量概念 11.3 二值信号量 11.3.1 二值信号量概念 11.3.2 优先级翻转 11.3.3 二值信号量运作机制 11.4 计数信号量 11.4.1 ...

  2. scheduler源码分析——preempt抢占

    前言 之前探讨scheduler的调度流程时,提及过preempt抢占机制,它发生在预选调度失败的时候,当时由于篇幅限制就没有展开细说. 回顾一下抢占流程的主要逻辑在DefaultPreemption ...

  3. kube-scheduler源码分析(3)-抢占调度分析

    kube-scheduler源码分析(3)-抢占调度分析 kube-scheduler简介 kube-scheduler组件是kubernetes中的核心组件之一,主要负责pod资源对象的调度工作,具 ...

  4. kernel 3.10内核源码分析--hung task机制

    kernel 3.10内核源码分析--hung task机制 一.相关知识: 长期以来,处于D状态(TASK_UNINTERRUPTIBLE状态)的进程 都是让人比较烦恼的问题,处于D状态的进程不能接 ...

  5. 鸿蒙内核源码分析(任务调度篇) | 任务是内核调度的单元 | 百篇博客分析OpenHarmony源码 | v4.05

    百篇博客系列篇.本篇为: v04.xx 鸿蒙内核源码分析(任务调度篇) | 任务是内核调度的单元 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度 ...

  6. 鸿蒙内核源码分析(时钟任务篇) | 触发调度谁的贡献最大 | 百篇博客分析OpenHarmony源码 | v3.05

    百篇博客系列篇.本篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度谁的贡献最大 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度 ...

  7. scheduler源码分析——调度流程

    前言 当api-server处理完一个pod的创建请求后,此时可以通过kubectl把pod get出来,但是pod的状态是Pending.在这个Pod能运行在节点上之前,它还需要经过schedule ...

  8. quartz集群调度机制调研及源码分析---转载

    quartz2.2.1集群调度机制调研及源码分析引言quartz集群架构调度器实例化调度过程触发器的获取触发trigger:Job执行过程:总结:附: 引言 quratz是目前最为成熟,使用最广泛的j ...

  9. 定时组件quartz系列<三>quartz调度机制调研及源码分析

    quartz2.2.1集群调度机制调研及源码分析引言quartz集群架构调度器实例化调度过程触发器的获取触发trigger:Job执行过程:总结:附: 引言 quratz是目前最为成熟,使用最广泛的j ...

随机推荐

  1. Java 8中Lambda表达式默认方法的模板方法模式,你够了解么?

    为了以更简单的术语描述模板方法,考虑这个场景:假设在一个工作流系统中,为了完成任务,有4个任务必须以给定的执行顺序执行.在这4个任务中,不同工作流系统的实现可以根据自身情况自定义任务的执行内容. 模板 ...

  2. Mybatis中<![cdata[ ]]>

    1.<![cdata[ ]]>介绍 <![cdata[ 内容 ]]>是一种xml语法,在CDATA标记中的信息被解析器原封不动地传给应用程序,并且不解析该段信息中的任何控制标记 ...

  3. 牛X!看完阿里P8架构师推荐的spring三剑客,成功涨薪5k

    一直以来,Spring都被Java程序员视为杀手级别的应用,是为简化Java EE应用程序的开发为目标而创建的.Spring可以做很多事情,它为企业级开发提供给了丰富的功能,但是这些功能的底层都依赖于 ...

  4. 关于Java中for循环的i++和++i区别

    我们应该都知道i++和++i的区别是: ++i是先执行 i = i +1 再使用 i 的值,而 i++ 是先使用 i 的值再执行 i = i + 1: for循环的执行顺序如下: for(a;b;c) ...

  5. 要有一颗理财的心 - 读<富爸爸.穷爸爸>

    记得工作没多久后的一次加薪的例行谈话.部门经理和我说,不能靠工资过日子,要多想想怎么投资,我的主要财富就是靠投资赚来的.当时第一反应,老板,你不给我加薪找这借口也太牵强了吧.我的收入只有工资,我的工资 ...

  6. 通过自定义资源扩展Kubernetes

    原文链接:通过自定义资源扩展Kubernetes

  7. 算法-搜索(6)B树

    B树是平衡的m路搜索树. 根结点至少两个子女,根结点以外的非失败结点至少⌈m/2⌉个子女,所有失败结点都在h+1层. 第h层至少2⌈m/2⌉h-1个结点,因此失败结点数n+1≥2⌈m/2⌉h-1个. ...

  8. 2020.5.28 第八篇 Scrum冲刺博客

    Team:银河超级无敌舰队 Project:招新通 项目冲刺集合贴:链接 目录 一.每日站立会议 1.1 会议照片 1.2 项目完成情况 二.项目燃尽图 三.签入记录 3.1 代码/文档签入记录 3. ...

  9. 玩转Spring——Spring整合JDBC

    传统JDBC代码的弊端在传统的jdbc代码中,即使是执行一条简单的SQL语句,其实现的整个流程也是极为繁琐的,先打开数据库连接执行sql,然后组装结果,最后关闭数据库资源,这中间还有大量的try... ...

  10. 01.arduino uno开发板入门

    01.所需工具 -Ariduino uno开发板一块 -对应的usb数据线 -杜邦线若干 -一些用以测试的电子元器件 02.安装arduino IDE 打开官网链接https://www.arduin ...