kube-schedulerのPodNominatorの分析

序文

著者は、 kube -schedulerのSchedulingQueueの分析」の記事でPodNominatorについて言及しました。これは、スケジューリングキューがPodNominatorを継承するためですが、その時点では、不要なコンテンツの拡張を避けるために、PodNominatorの説明はありません。この記事では、PodNominatorの定義と実装から、kube-schedulerでのアプリケーションまで、より包括的な分析を行います。

この記事で使用されているKubenetesソースコードのrelease-1.20ブランチ、最新のKubernetesバージョンのドキュメントリンク:https//github.com/jindezgm/k8s-src-analysis/blob/master/kube-scheduler/PodNominator.md

PodNominatorの定義

まず、PodNominatorが何をするのかを理解する必要があります。英語の翻訳はPod Nominatorですが、指名とは何ですか?これは、ポッドAPIで定義されたアノテーションの一部から取得できます。ソースリンク:https//github.com/kubernetes/kubernetes/blob/release-1.20/staging/src/k8s.io/api/core/v1/types.go#L3594

    // nominatedNodeName is set only when this pod preempts other pods on the node, but it cannot be
	// scheduled right away as preemption victims receive their graceful termination periods.
	// This field does not guarantee that the pod will be scheduled on this node. Scheduler may decide
	// to place the pod elsewhere if other nodes become available sooner. Scheduler may also decide to
	// give the resources on this node to a higher priority pod that is created after preemption.
	// As a result, this field may be different than PodSpec.nodeName when the pod is
	// scheduled.
	// +optional
	NominatedNodeName string `json:"nominatedNodeName,omitempty" protobuf:"bytes,11,opt,name=nominatedNodeName"`

ソースコードコメントの簡単な要約は、ポッドがノード上の他のポッドをプリエンプトすると、Pod.Status.NominatedNodeNameがノードの名前に設定されることです。スケジューラーは、プリエンプトされたポッドが正常に終了するまで待機する必要があるため、ポッドをノードにすぐにスケジュールしません。それだけです。他のコンテンツはスケジューリングに関連しています。私たちは、推薦が何のためにあるのかを知る必要があるだけです。

これで、指名とは、ポッドを指名されたノードにスケジュールできることを意味しますが、スケジュールを実行する前に、プリエンプトされたポッドがリソースを空にするのを待つ必要があります。そして、PodNominatorは、どのポッドがノードによって指名されたかを記録します。つまり、問題は、Pod.Status.NominatedNodeNameがすでに記録されていないか、PodNominatorは何をするのかということです。著者は最初にこの質問に答えるのではなく、要約の章で答えを出します。

ソースコードリンクであるPodNominatorの定義を見てみましょう:https//github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/framework/interface.go#L562

type PodNominator interface {
	// 提名pod调度到nodeName的Node上
	AddNominatedPod(pod *v1.Pod, nodeName string)
	// 删除pod提名的nodeName 
	DeleteNominatedPodIfExists(pod *v1.Pod)
	// PodNominator处理pod更新事件
	UpdateNominatedPod(oldPod, newPod *v1.Pod)
	// 获取提名到nodeName上的所有Pod,这个接口是不是感觉到PodNominator存在的意义了?
    // 毕竟PodNominator有统计功能,否则就需要遍历所有的Pod.Status.NominatedNodeName才能统计出来
	NominatedPodsForNode(nodeName string) []*v1.Pod
}

PodNominatorの定義の観点からは、非常に単純であり、マップを使用して実現できるように感じますが、実際はそうです。

PodNominatorの実装

PodNominatorの実装はスケジューリングキューにあり、その目的は検討する価値があります。最初にコードの実装を見て、それがディスパッチキューに実装されている理由を調べてみましょう。ソースリンク:https//github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/internal/queue/scheduling_queue.go#L723

// 确实非常简单,用map加读写锁实现的
type nominatedPodMap struct {
	// nominatedPods的key是提名的Node的名字,value是所有的Pod,这就是为NominatedPodsForNode()接口专门设计的
	nominatedPods map[string][]*v1.Pod
	// nominatedPodToNode的key是Pod的UID,value是提名Node的名字,这是为增、删、改接口设计的
    // 为什么调度队列用NS+NAME,而这里用UID,其中可能有版本更新造成的历史原因,关键要看是否有用名字访问的需求。
    // 当然,当前版本调度队列的接口没有直接用NS+NAME访问Pod,但是不排除以前是有这个设计考虑的。
    // 以上只是笔者猜测,仅供参考,有哪位小伙伴有更靠谱的答案欢迎在留言区回复。
	nominatedPodToNode map[ktypes.UID]string

	sync.RWMutex
}

// nominatedPodMap一共就三个核心函数,add、delete、和UpdateNominatedPod
func (npm *nominatedPodMap) add(p *v1.Pod, nodeName string) {
	// 避免Pod已经存在先执行删除,确保同一个Pod不会存在两个实例,这个主要是为了nominatedPods考虑的,毕竟它的value是slice,没有去重能力。
	npm.delete(p)

    // NominatedNodeName()函数就是获取p.Status.NominatedNodeName,
    // 那么下面的代码的意思就是nodeName和p.Status.NominatedNodeName优先用nodeName,如果二者都没有指定则返回
    // 这里需要知道的是nodeName代表着最新的状态,所以需要优先,这一点在PodNominator应用章节会进一步说明
	nnn := nodeName
	if len(nnn) == 0 {
		nnn = NominatedNodeName(p)
		if len(nnn) == 0 {
			return
		}
	}
    // 下面的代码没什么难度,就是把Pod放到两个map中
	npm.nominatedPodToNode[p.UID] = nnn
	for _, np := range npm.nominatedPods[nnn] {
		if np.UID == p.UID {
			klog.V(4).Infof("Pod %v/%v already exists in the nominated map!", p.Namespace, p.Name)
			return
		}
	}
	npm.nominatedPods[nnn] = append(npm.nominatedPods[nnn], p)
}

func (npm *nominatedPodMap) delete(p *v1.Pod) {
    // 如果Pod不存在就返回
	nnn, ok := npm.nominatedPodToNode[p.UID]
	if !ok {
		return
	}
    // 然后从两个map中删除
	for i, np := range npm.nominatedPods[nnn] {
		if np.UID == p.UID {
			npm.nominatedPods[nnn] = append(npm.nominatedPods[nnn][:i], npm.nominatedPods[nnn][i+1:]...)
			if len(npm.nominatedPods[nnn]) == 0 {
				delete(npm.nominatedPods, nnn)
			}
			break
		}
	}
	delete(npm.nominatedPodToNode, p.UID)
}

func (npm *nominatedPodMap) UpdateNominatedPod(oldPod, newPod *v1.Pod) {
	npm.Lock()
	defer npm.Unlock()
    // 首选需要知道一个知识点,kube-scheduler什么时候会调用UpdateNominatedPod()?这个问题貌似应该是PodNominator应用章节的内容。
    // 为了便于理解下面的代码,笔者需要提前剧透一下,答案是在调度队列的更新接口中,感兴趣的同学可以回看《kube-scheduler的SchedulingQueue解析》的源码注释
    // 而调度队列的更新是kube-scheduler在watch apiserver的Pod的时候触发调用的,所以此时默认是没有提名Node的
	nodeName := ""
    // 有一些情况,在Pod刚好提名了Node之后收到了Pod的更新事件并且新Pod.Status.NominatedNodeName="",此时需要保留提名的Node。
    // 以下几种情况更新事件是不会保留提名的Node
    // 1.设置Status.NominatedNodeName:表现为NominatedNodeName(oldPod) == "" && NominatedNodeName(newPod) != ""
    // 2.更新Status.NominatedNodeName:表现为NominatedNodeName(oldPod) != "" && NominatedNodeName(newPod) != ""
    // 3.删除Status.NominatedNodeName:表现为NominatedNodeName(oldPod) != "" && NominatedNodeName(newPod) == ""
	if NominatedNodeName(oldPod) == "" && NominatedNodeName(newPod) == "" {
		if nnn, ok := npm.nominatedPodToNode[oldPod.UID]; ok {
			// 这是唯一一种情况保留提名
			nodeName = nnn
		}
	}
    // 无论提名的Node名字是否修改都需要更新,因为需要确保Pod的指针也被更新
	npm.delete(oldPod)
	npm.add(newPod, nodeName)
}

元の指名されたノード部分を更新して保持することに加えて、それはもう少し複雑です。PodNominator全体の実装は単純すぎて単純ではないと言えます。 PodNominator、あなたはその実装を想像することができます。PodNominatorは非常に単純に見えますが、多くの複雑な関数はこれらの単純なモジュールから構築されています。PodNominatorは、kube-schedulerがプリエンプティブスケジューリングを実現するための鍵ですが、現時点ではそれほど単純ではないと感じていますか?次に、プリエンプティブスケジューリングを実現するためにPodNominatorがkube-schedulerにどのように適用されるかを見ていきます。

サブノミネーター

kube-schedulerには、PodNominatorを直接継承する2つのタイプがあります。SchedulingQueueとPreemptHandleです。前者の作者は「kube-schedulerのSchedulingQueueの分析」の記事で言及されていますが、直接無視されるため、この記事はスケジューリングキューの続きです。PreemptHandleはプリエンプションハンドルであり、この記事でも紹介しません。多くの、または古いルーチンがフォローアップのために残されています記事は解析されますが、タイプ名から、プリエンプションを達成するために使用されていることがわかります。したがって、PodNominatorはプリエンプティブスケジューリングに関連しています。

SchedulingQueueとPreemptHandleはどちらもPodNominatorを継承しますが、どちらも同じオブジェクトを指しているため(これは、golang言語、ポインター継承の機能でもあります)、kube-schedulerにはPodNominatorオブジェクトが1つだけあります。これは、複数のコルーチンによる同時アクセスが必要なため、nominationPodMapがロックされる理由の1つでもあります。

ディスパッチキューでのPodNominatorの適用

PodNominator.AddNominatedPod()を呼び出すディスパッチキューには、次の3つの状況があります。

  1. PriorityQueue.Add():ソースリンクhttps://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/internal/queue/scheduling_queue.go#L265、ここでの目的は、ノードを指定する場合にポッドを追加することです、次にそれをPodNominatorに追加します。著者は、新しいポッドをどのように指名できるので、この状況は通常のロジックでは発生しない可能性があると考えています。SharedInformerのResyncPeriodを覚えていますか?SharedInformerは定期的に全額を同期します。このときのイベントはAddです。上記は私の推測にすぎません。
  2. PriorityQueue.AddUnschedulableIfNotPresent():ソースリンクhttps://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/internal/queue/scheduling_queue.go#L326、ここでの目的は、 Pod、この関数はPodをunschedulableQまたはbackoffQに追加するため、どちらもスケジュールできません。元の指定を削除して、Pod.Status.NominatedNodeNameに復元する必要があります。
  3. PriorityQueue.Update():ソースリンクhttps://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/internal/queue/scheduling_queue.go#L460、ここでの目的はPriorityQueue.Addと同じです。 ()同じです。更新時にポッドがサブキューにない場合は、追加として扱われるためです。

スケジューリングキューはPriorityQueue.Update()関数にあります。ポッドがすでにキューに存在する場合は、PodNominator.UpdateNominatedPod()が呼び出されます。対応するコードは作成者によってコピーされなくなり、読者は上記から見つけることができます。接続。結局のところ、ポッドは更新されており、対応する状態をより詳細にする必要があります。PodNominatorでは、ポッドのポインターを更新することです。同様に、PodNominator.DeleteNominatedPodIfExists()はPriorityQueue.Delete()関数で呼び出され、作成者はこれ以上説明しません。

スケジューラーでのPodNominatorの適用

kube-schedulerがプリエンプションを介してポッドのノードを指定する必要がある場合、ノード上のプリエンプションされたポッドが終了するまでポッドを待機する必要があるため、この時点でのポッドはまだスケジュールされていない状態です。そのため、現時点でPodの場合、スケジューリングは失敗します。そのため、PodNominator.AddNominatedPod()がrecordSchedulingFailure()関数に表示されます。コードリンク:https//github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/scheduler.go#L328

func (sched *Scheduler) recordSchedulingFailure(fwk framework.Framework, podInfo *framework.QueuedPodInfo, err error, reason string, nominatedNode string) {
	// sched.Error是Pod调度出错后的处理,即便是抢占成功并提名,依然是资源不满足错误,因为等待被抢占Pod退出
    // 这个函数会调用PriorityQueue.AddUnschedulableIfNoPresent()函数将Pod放入不可调度子队列
    sched.Error(podInfo, err)

	// Update the scheduling queue with the nominated pod information. Without
	// this, there would be a race condition between the next scheduling cycle
	// and the time the scheduler receives a Pod Update for the nominated pod.
	// Here we check for nil only for tests.
	if sched.SchedulingQueue != nil {
		sched.SchedulingQueue.AddNominatedPod(podInfo.Pod, nominatedNode)
	}

	......
}

スケジューラーでrecordSchedulingFailure()関数を呼び出して、有効なノード名( ""ではない)を渡す唯一の場所は、https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/schedulerです。 go#L500、プリエンプションが成功したことを示します。他の場所では、空の文字列が渡され、スケジューリングが失敗したことを示します。この記事はスケジューラーであまり分析を行いません。特別な記事分析があるため、興味のある学生は自分でソースコードを分析できます。もちろん、著者の関連記事がリリースされるのを待つこともできます。作成者は、スケジューラによるプリエンプションの原則について簡単に説明します。ポッドのニーズを満たすノードがない場合、スケジューラはポッドの優先度が低いポッドのプリエンプションを開始します。プリエンプションが成功した場合は、プリエンプションされたポッドが配置されているノードを指定します。次に、ポッドはスケジュール不可能なキューに入れられ、プリエンプトされたポッドが終了するのを待ちます。

もちろん、スケジュール不可能なキューに入れられたポッドは、一定期間後にactiveQに戻されます。このとき、ポッドのニーズを満たすノードがある場合、kube-schedulerはポッドをスケジュールします。ノードをクリックし、ポッドによって指定されたノードをクリアします。コードリンク:https//github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/scheduler.go#L384

// 还记得《kube-scheduler的Cache解析》对于assume的解释么?如果忘记了请复习一遍
func (sched *Scheduler) assume(assumed *v1.Pod, host string) error {
	// 设置Pod调度的Node
	assumed.Spec.NodeName = host
    // Cache中假定Pod已经调度到了Node上
	if err := sched.SchedulerCache.AssumePod(assumed); err != nil {
		klog.Errorf("scheduler cache AssumePod failed: %v", err)
		return err
	}
	// 已经假定Pod调度到了Node上,应该移除以前提名的Node了(如果有提名的Node)
	if sched.SchedulingQueue != nil {
		sched.SchedulingQueue.DeleteNominatedPodIfExists(assumed)
	}

	return nil
}

PodはPodNominatorを介してノードを指定できますか?新しいPodが次のスケジューリングラウンドで繰り返しプリエンプションされないようにするにはどうすればよいですか(Pod2の優先度がPod1より高くなく、Pod0が待機している間にPod0より高い場合、Pod1はPod0をプリエンプションしません)。これがPodNominator.NominatedPodsForNode()の目的です。スケジューラーがノードをトラバースすると、リソースのこの部分がノードに追加され、プリエンプションが繰り返される問題が回避されます。これは、kube-schedulerの一般的な用語「予約」でもあります。コードのこの部分は比較的複雑なので、ここではコメントしません。

上記はkube-schedulerでのPodNominatorのアプリケーションです。知識ポイントが少し散らばっていて体系的でないと感じた場合は、以下の要約を参照してください。

総括する

  1. PodNominatorは、プリエンプションスケジューリングを実現するために、kube-schedulerによって定義された「指名」マネージャーです。プリエンプションに成功したが、プリエンプションされたポッドが終了するのを待つ必要があるすべてのポッドを記録します。

  2. PodNominator.NominatedPodsForNode()を介して、kube-schedulerは、指定されたノードに指定されたすべてのポッドを取得します。スケジューリングを計算するとき、ポッドのこの部分によって適用されるリソースは、ポッドをプリエンプトするためにノードによって予約されたリソースと見なされます。

  3. ポッドのニーズを満たすノードがない場合、kube-schedulerはプリエンプティブスケジューリングの実行を開始します。プリエンプションが成功した場合は、PodNominatorを使用して、プリエンプションされたポッドが配置されているノードをポッドによって実行されているノードとして指定します。

  4. 指名が成功したからといって、スケジュールされているわけではないため、ポッドは現時点ではまだスケジュール不可能な状態にあり、スケジュールキューのunschedulableQサブキューに配置されます。

  5. ポッドがunschedulableQからactiveQに移行し、ポッドのニーズを満たすノードがある場合、ポッドはノードにスケジュールされ、以前に指定されたノードは削除されます。

このとき、戻って「指名」を試してください。つまり、スケジューラーは、ノード上の他のポッドが現在占有しているリソースの一部をポッド用に事前に予約します。それでは、この記事の冒頭で提起された質問を見てみましょう。PodNominatorの役割は何ですか?著者は冗長な説明をする必要はないと思います。

最後に、別の質問があります。なぜPodNominatorがディスパッチキューに実装されているのですか?作成者もこれを行う場合、作成者の理由は非常に単純です。ポッドはノードを指定しますが、それでもスケジュールされていないため、スケジュールキューに入れるのが最も適切です。著者がこのように考えているかどうかはわかりません。

おすすめ

転載: blog.csdn.net/weixin_42663840/article/details/113941224