[kubernetes系列]Scheduler模塊深度講解

一,前言

調度器的職責是負責將Pod調度到最合適的Node上,可是要實現它並非易事,須要考慮不少方面。(1) 公平性:調度後集羣各個node應該保持均衡的狀態。(2) 性能:不能成爲集羣的性能瓶頸。 (3) 擴展性:用戶能根據自身需求定製調度器和調度算法。(4) 限制:須要考慮多種限制條件,例如親緣性,優先級,Qos等。(5) 代碼的優雅性,雖然不是必定要的^^。接下來帶着這些問題往下看。html

二,調度器源碼分析

接下來一邊說明調度的步驟,一邊看源碼(只分析主幹代碼),而後思考有沒有更好的方式。調度這裏,分紅幾個重要的步驟:1,初始化調度器;2,獲取未調度的Pod開始調度;3,預調度,優調度和擴展;4,調度失敗則發起搶佔。這裏只跟着流程走,具體有必要更詳細解讀的放在下面幾部分。本文代碼基於1.12.1版本node

(1) 初始化調度器

先生成configfatotry(可經過不一樣參數生成不一樣config),而後調度器可經過policy文件,policy configmap,或者指定provider,經過configfactory來建立config,再由config生成scheduler。咱們能夠在啓動時候選擇policy啓動或者provider啓動scheduler模塊。無論經過哪一種方式建立,最終都會進入到CreateFromKeys去建立scheduler。nginx

首先看如何獲取provider和policy算法

func NewSchedulerConfig(s schedulerserverconfig.CompletedConfig) (*scheduler.Config, error) {
   // 判斷是否開啓StorageClass
	var storageClassInformer storageinformers.StorageClassInformer
	if utilfeature.DefaultFeatureGate.Enabled(features.VolumeScheduling) {
		storageClassInformer = s.InformerFactory.Storage().V1().StorageClasses()
	}

	//  生成configfactory,包含全部須要的informer
	configurator := factory.NewConfigFactory(&factory.ConfigFactoryArgs{
		SchedulerName:                  s.ComponentConfig.SchedulerName,
		Client:                         s.Client,
		NodeInformer:                   s.InformerFactory.Core().V1().Nodes(),
		.....
	})

	source := s.ComponentConfig.AlgorithmSource
	var config *scheduler.Config
	switch {
	//根據準備好的provider生成config,
	case source.Provider != nil:
		sc, err := configurator.CreateFromProvider(*source.Provider)
		config = sc
	// 根據policy生成config
	case source.Policy != nil:
		policy := &schedulerapi.Policy{}
		switch {
		// 根據policy文件生成
		case source.Policy.File != nil:
			......
		// 根據policy configmap生成
		case source.Policy.ConfigMap != nil:
		        ......
		}
		sc, err := configurator.CreateFromConfig(*policy)
		config = sc
	}
	config.DisablePreemption = s.ComponentConfig.DisablePreemption
	return config, nil
}
複製代碼

上面的CreateFromProvider和CreateFromConfig最終都會進入到CreateFromKeys,去初始化系統自帶的GenericScheduler。shell

// 根據已註冊的 predicate keys and priority keys生成配置
func (c *configFactory) CreateFromKeys(predicateKeys, priorityKeys sets.String, extenders []algorithm.SchedulerExtender) (*scheduler.Config, error) {
      // 獲取全部的predicate函數 
	predicateFuncs, err := c.GetPredicates(predicateKeys)
	// 獲取priority配置(爲何不是返回函數?由於包含了權重,並且使用的是map-reduce)
	priorityConfigs, err := c.GetPriorityFunctionConfigs(priorityKeys)
	// metaproducer都是用來獲取metadata信息,例如affinity,request,limit等
	priorityMetaProducer, err := c.GetPriorityMetadataProducer()
	predicateMetaProducer, err := c.GetPredicateMetadataProducer()
	algo := core.NewGenericScheduler(
		c.podQueue,   //調度隊列。默認使用優先級隊列
		predicateFuncs,   // predicate算法函數鏈
		predicateMetaProducer,    
		priorityConfigs,  // priority算法鏈
		priorityMetaProducer,
		extenders,    // 擴展過濾器
		......
	)

	podBackoff := util.CreateDefaultPodBackoff()
}
複製代碼

到這裏scheduler.config就初始化了,若是要接着日後面看,咱們能夠看一下scheduler.config的定義。將會大大幫助咱們進行理解。編程

type Config struct {
       // 調度中的pod信息,保證不衝突
       SchedulerCache schedulercache.Cache
      //  上面定義的GenericScheduler就實現了該接口,因此會賦值進來,這是最重要的字段
	Algorithm  algorithm.ScheduleAlgorithm
	//  驅逐者,產生搶佔時候出場
	PodPreemptor PodPreemptor
      //  獲取下個未調度的pod
	NextPod func() *v1.Pod
      // 容錯機制,若是調用pod出錯,使用該函數進行處理(從新加入到調度隊列)
	Error func(*v1.Pod, error)
}
複製代碼

(2) 調度邏輯

調度邏輯包括了篩選合適node,優先級隊列,調度,搶佔等邏輯,比較複雜,接下來慢慢理順。json

2.1 調度

首先看一小段主要代碼,這代碼已經把調度邏輯的大致交代了,再基於這主要的代碼展開分析。後端

func (sched *Scheduler) scheduleOne() {
      // 獲取下一個等待調度的pod
	pod := sched.config.NextPod()
	// 嘗試將pod綁定到node上
	suggestedHost, err := sched.schedule(pod)
	if err != nil {
		if fitError, ok := err.(*core.FitError); ok {
			// 綁定出錯則發起搶佔
			sched.preempt(pod, fitError)
			metrics.PreemptionAttempts.Inc()
		}
		return
	}
	allBound, err := sched.assumeVolumes(assumedPod, suggestedHost)
}
複製代碼
2.1.1 獲取下個等待調度的pod

從初始化調度器的源碼分析中,咱們知道,使用的隊列是優先級隊列,那麼此時則是從優先級隊列中獲取優先級最高的pod。api

func (c *configFactory) getNextPod() *v1.Pod {
	pod, err := c.podQueue.Pop()
}
複製代碼
2.1.2 選擇合適的node

經過predicate和prioritize算法,而後選擇出一個節點,把給定的pod調度到節點上。最後若是還有extender,還須要經過extender緩存

func (g *genericScheduler) Schedule(pod *v1.Pod, nodeLister algorithm.NodeLister) (string, error) {
      // 獲取因此node	
	nodes, err := nodeLister.List()
	// cache中保存調度中須要的pod和node數據,須要更新到最新
	err = g.cache.UpdateNodeNameToInfoMap(g.cachedNodeInfoMap)
	// 過濾出合適調度的node集合
	filteredNodes, failedPredicateMap, err := g.findNodesThatFit(pod, nodes)
	//  返回合適調度的node的優先級排序
	priorityList, err := PrioritizeNodes(pod, g.cachedNodeInfoMap, metaPrioritiesInterface, g.prioritizers, filteredNodes, g.extenders)
	//  選擇處一個節點返回
	return g.selectHost(priorityList)
}
複製代碼

上面包括了node是如何被選擇出來的大致邏輯,接下來粗略看看每一個步驟。 過濾出合適調度的node集合最後會調用到下面這個函數

func podFitsOnNode(...) (bool, []algorithm.PredicateFailureReason, error) {
        // 循環遍歷全部predicate函數,而後調用
	for _, predicateKey := range predicates.Ordering() {
		if predicate, exist := predicateFuncs[predicateKey]; exist {
		       //調用函數
        	       if eCacheAvailable {
        			fit, reasons, err = nodeCache.RunPredicate(predicate, predicateKey, pod, metaToUse, nodeInfoToUse, equivClass, cache)
        		} else {
        			fit, reasons, err = predicate(pod, metaToUse, nodeInfoToUse)
        		}
        		// 不合適則記錄
			if !fit {
				failedPredicates = append(failedPredicates, reasons...)
			}
		}
	}
	return len(failedPredicates) == 0, failedPredicates, nil
}
複製代碼

過濾出node後,咱們還須要給這些node排序,越適合調度的優先級越高。這裏不分析了,思路跟過濾那裏差很少,不過使用的map reduce來計算。

2.3 搶佔

若是正常調度沒法調度到node,那麼就會發起搶佔邏輯,選擇一個node,驅逐低優先級的pod。這個節點須要知足各類需求(把低優先級pod驅逐後資源必須能知足該pod,親和性檢查等)

func (g *genericScheduler) Preempt(pod *v1.Pod, nodeLister algorithm.NodeLister, scheduleErr error) (*v1.Node, []*v1.Pod, []*v1.Pod, error) {
	allNodes, err := nodeLister.List()
	potentialNodes := nodesWherePreemptionMightHelp(allNodes, fitError.FailedPredicates)
	// 獲取PDB(會盡力保證PDB)
	pdbs, err := g.cache.ListPDBs(labels.Everything())
	// 選擇出能夠搶佔的node集合
	nodeToVictims, err := selectNodesForPreemption(pod, g.cachedNodeInfoMap, potentialNodes, g.predicates,
		g.predicateMetaProducer, g.schedulingQueue, pdbs)
	nodeToVictims, err = g.processPreemptionWithExtenders(pod, nodeToVictims)
	// 選擇出一個節點發生搶佔
	candidateNode := pickOneNodeForPreemption(nodeToVictims)
	// 更新低優先級的nomination
	nominatedPods := g.getLowerPriorityNominatedPods(pod, candidateNode.Name)
	if nodeInfo, ok := g.cachedNodeInfoMap[candidateNode.Name]; ok {
		return nodeInfo.Node(), nodeToVictims[candidateNode].Pods, nominatedPods, err
	}
}
複製代碼
2.3.1,搶佔邏輯分析

調度器會選擇一個pod P嘗試進行調度,若是沒有node知足條件,那麼會觸發搶佔邏輯

1,尋找合適的node N,若是有一組node都符合,那麼會選擇擁有最低優先級的一組pod的node,若是這些pod有PDB保護或者驅逐後仍是沒法知足P的要求,那麼會去尋找高點優先級的。 1,當找到適合P進行調度的node N時候,會從該node刪除一個或者多個pod(優先級低於P,且刪除後能讓P進行調度) 2,pod刪除時候,須要一個優雅關閉的時間,P會從新進入隊列,等待下次調度。 3,會在P中的status字段設置nominatedNodeName爲N的name(該字段爲了在P搶佔資源後等待下次調度的過程當中,讓調度器知道該node已經發生了搶佔,P指望落在該node上)。 4,若是在N資源釋放完後,有個比P優先級更高的pod調度到N上,那麼P可能沒法調度到N上了,此時會清楚P的nominatedNodeName字段。若是在N上的pod優雅關閉的過程當中,出現了另外一個可供P調度的node,那麼P將會調度到該node,則會形成nominatedNodeName和實際的node名稱不符合,同時,N上的pod仍是會被驅逐。

三,調度算法分析

1,predicate

在predicates.go中說明了目前提供的各個算法,多達20多種,下面列出幾種

MatchInterPodAffinity:檢查pod和其餘pod是否符合親和性規則
CheckNodeCondition: 檢查Node的情況
MatchNodeSelector:檢查Node節點的label定義是否知足Pod的NodeSelector屬性需求
PodFitsResources:檢查主機的資源是否知足Pod的需求,根據實際已經分配的資源(request)作調度,而不是使用已實際使用的資源量作調度
PodFitsHostPorts:檢查Pod內每個容器所需的HostPort是否已被其它容器佔用,若是有所需的HostPort不知足需求,那麼Pod不能調度到這個主機上
HostName:檢查主機名稱是否是Pod指定的NodeName
NoDiskConflict:檢查在此主機上是否存在卷衝突。若是這個主機已經掛載了卷,其它一樣使用這個卷的Pod不能調度到這個主機上,不一樣的存儲後端具體規則不一樣
NoVolumeZoneConflict:檢查給定的zone限制前提下,檢查若是在此主機上部署Pod是否存在卷衝突
PodToleratesNodeTaints:確保pod定義的tolerates能接納node定義的taints
CheckNodeMemoryPressure:檢查pod是否能夠調度到已經報告了主機內存壓力過大的節點
CheckNodeDiskPressure:檢查pod是否能夠調度到已經報告了主機的存儲壓力過大的節點
MaxEBSVolumeCount:確保已掛載的EBS存儲卷不超過設置的最大值,默認39
MaxGCEPDVolumeCount:確保已掛載的GCE存儲卷不超過設置的最大值,默認16
MaxAzureDiskVolumeCount:確保已掛載的Azure存儲卷不超過設置的最大值,默認16
GeneralPredicates:檢查pod與主機上kubernetes相關組件是否匹配
NoVolumeNodeConflict:檢查給定的Node限制前提下,檢查若是在此主機上部署Pod是否存在卷衝突
複製代碼

因爲每一個predicate都不復雜,就不分析了

2,priority

優選的算法也不少,這裏列出幾個

EqualPriority:全部節點一樣優先級,無實際效果
ImageLocalityPriority:根據主機上是否已具有Pod運行的環境來打分,得分計算:不存在所需鏡像,返回0分,存在鏡像,鏡像越大得分越高
LeastRequestedPriority:計算Pods須要的CPU和內存在當前節點可用資源的百分比,具備最小百分比的節點就是最優,得分計算公式:cpu((capacity – sum(requested)) * 10 / capacity) + memory((capacity – sum(requested)) * 10 / capacity) / 2
BalancedResourceAllocation:節點上各項資源(CPU、內存)使用率最均衡的爲最優,得分計算公式:10 – abs(totalCpu/cpuNodeCapacity-totalMemory/memoryNodeCapacity)*10
SelectorSpreadPriority:按Service和Replicaset歸屬計算Node上分佈最少的同類Pod數量,得分計算:數量越少得分越高
NodeAffinityPriority:節點親和性選擇策略,提供兩種選擇器支持:requiredDuringSchedulingIgnoredDuringExecution(保證所選的主機必須知足全部Pod對主機的規則要求)、preferresDuringSchedulingIgnoredDuringExecution(調度器會盡可能但不保證知足NodeSelector的全部要求)
TaintTolerationPriority:相似於Predicates策略中的PodToleratesNodeTaints,優先調度到標記了Taint的節點
InterPodAffinityPriority:pod親和性選擇策略,相似NodeAffinityPriority,提供兩種選擇器支持:requiredDuringSchedulingIgnoredDuringExecution(保證所選的主機必須知足全部Pod對主機的規則要求)、preferresDuringSchedulingIgnoredDuringExecution(調度器會盡可能但不保證知足NodeSelector的全部要求),兩個子策略:podAffinity和podAntiAffinity,後邊會專門詳解該策略
MostRequestedPriority:動態伸縮集羣環境比較適用,會優先調度pod到使用率最高的主機節點,這樣在伸縮集羣時,就會騰出空閒機器,從而進行停機處理。
複製代碼

四,調度優先級隊列

在1.11版本之前是alpha,在1.11版本開始爲beta,而且默認開啓。在1.9及之後的版本,優先級不只影響調度的前後順序,同時影響在node資源不足時候的驅逐順序。

1,源碼分析

看結構體定義便可,其餘的代碼都是很容易看懂

type PriorityQueue struct {
	// 有序堆,按照優先級存放等待調度的pod
	activeQ *Heap
	// 嘗試調度而且調度失敗的pod
	unschedulableQ *UnschedulablePodsMap
	// 存儲高優先級pod(發生了搶佔)指望調度的node信息,即有NominatedNodeName Annotation的pod
	nominatedPods map[string][]*v1.Pod
	receivedMoveRequest bool
}
複製代碼

2,使用

若是在1.11版本之前,須要先開啓該特性。

2.1 PriorityClasses

PriorityClasses在建立時候無需指定namespace,由於它是屬於全局的。只容許全局存在一個globalDefault爲true的PriorityClasses,來做爲未指定priorityClassName的pod的優先級。對PriorityClasses的改動(例如改變globalDefault爲true,刪除PriorityClasses)不會影響已經建立的pod,pod的優先級只初始化一次。

建立以下:

apiVersion: scheduling.k8s.io/v1beta1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000
globalDefault: false
description: "This priority class should be used for XYZ service pods only."
複製代碼

2.2 在pod中指定priorityClassName

例如指定上面的high-priority,未指定和沒有PriorityClasses指定globalDefault爲true的狀況下,優先級爲0。在1.9及之後的版本,高優先級的pod相比低優先級pod,處於調度隊列的前頭,可是若是高優先級隊列沒法被調度,也不會阻塞,調度器會調度低優先級的pod。

建立以下:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
  priorityClassName: high-priority
複製代碼

4,須要注意的地方

4.1,驅逐pod到調度pod存在時間差

因爲在驅逐pod時候,優雅關閉須要等待必定的時間,那麼致使pod真正被調度時候會存在一個時間差,咱們能夠優化低優先級的pod的優雅關閉時間或者調低優雅關閉時間

4.2,支持PDB,可是不能保證

調度器會嘗試在不違反PDB狀況下去驅逐pod,可是隻是嘗試,若是找不到或者仍是不知足狀況下,仍然爲刪除低優先級的pod

4.3,若是開始刪除pod,那麼說明該node必定能知足需求

4.4,低優先級pod有inter-pod affinity

若是在node上的pod存在inter-pod affinity,那麼因爲inter-pod affinity規則,pod P是沒法調度到該pod的(若是須要驅逐這些inter-pod affinity 的pod)。因此若是咱們有這塊的需求,須要保證後調度的pod的優先級不高於前面的。

4.5,不支持跨node的驅逐

若是pod P要調度到N,pod Q此時已經在經過zone下的不一樣node運行,P和Q若是存在zone-wide的anti-affinity,那麼P將沒法調度到N上,由於沒法跨node去驅逐Q。

4.6,須要防止用戶設置大優先級的pod

五,調度器實戰

1,自定義調度器

1.1 官方例子

經過shell腳步輪詢獲取指定調度器名稱爲my-scheduler的pod。

#!/bin/bash
SERVER='localhost:8001'
while true;
do
   for PODNAME in $(kubectl --server $SERVER get pods -o json | jq '.items[] | select(.spec.schedulerName == "my-scheduler") | select(.spec.nodeName == null) | .metadata.name' | tr -d '"')
;
   do
       NODES=($(kubectl --server $SERVER get nodes -o json | jq '.items[].metadata.name' | tr -d '"'))
       NUMNODES=${#NODES[@]}
       CHOSEN=${NODES[$[ $RANDOM % $NUMNODES ]]}
       curl --header "Content-Type:application/json" --request POST --data '{"apiVersion":"v1", "kind": "Binding", "metadata": {"name": "'$PODNAME'"}, "target": {"apiVersion": "v1", "kind" : "Node", "name": "'$CHOSEN'"}}' http://$SERVER/api/v1/namespaces/default/pods/$PODNAME/binding/
       echo "Assigned $PODNAME to $CHOSEN"
   done
   sleep 1
done
複製代碼

1.2 自定義擴展

這裏徹底複製第四個參考文獻。 利用咱們上面分析源碼知道的,可使用policy文件,本身組合須要的調度算法,而後能夠指定擴展(可多個)。

{
  "kind" : "Policy",
  "apiVersion" : "v1",
  "predicates" : [
    {"name" : "PodFitsHostPorts"},
    {"name" : "PodFitsResources"},
    {"name" : "NoDiskConflict"},
    {"name" : "MatchNodeSelector"},
    {"name" : "HostName"}
    ],
  "priorities" : [
    {"name" : "LeastRequestedPriority", "weight" : 1},
    {"name" : "BalancedResourceAllocation", "weight" : 1},
    {"name" : "ServiceSpreadingPriority", "weight" : 1},
    {"name" : "EqualPriority", "weight" : 1}
    ],
  "extenders" : [
    {
          "urlPrefix": "http://localhost/scheduler",
          "apiVersion": "v1beta1",
          "filterVerb": "predicates/always_true",
          "bindVerb": "",
          "prioritizeVerb": "priorities/zero_score",
          "weight": 1,
          "enableHttps": false,
          "nodeCacheCapable": false
          "httpTimeout": 10000000
    }
      ],
  "hardPodAffinitySymmetricWeight" : 10
  }
複製代碼

關於extender的配置的定義

type ExtenderConfig struct {
    // 訪問該extender的url前綴
    URLPrefix string `json:"urlPrefix"`
    //過濾器調用的動詞,若是不支持則爲空。當向擴展程序發出過濾器調用時,此謂詞將附加到URLPrefix
    FilterVerb string `json:"filterVerb,omitempty"`
    //prioritize調用的動詞,若是不支持則爲空。當向擴展程序發出優先級調用時,此謂詞被附加到URLPrefix。
    PrioritizeVerb string `json:"prioritizeVerb,omitempty"`
    //優先級調用生成的節點分數的數字乘數,權重應該是一個正整數
    Weight int `json:"weight,omitempty"`
    //綁定調用的動詞,若是不支持則爲空。在向擴展器發出綁定調用時,此謂詞會附加到URLPrefix。
    //若是此方法由擴展器實現,則將pod綁定動做將由擴展器返回給apiserver。只有一個擴展能夠實現這個功能
    BindVerb string
    // EnableHTTPS指定是否應使用https與擴展器進行通訊
    EnableHTTPS bool `json:"enableHttps,omitempty"`
    // TLSConfig指定傳輸層安全配置
    TLSConfig *restclient.TLSClientConfig `json:"tlsConfig,omitempty"`
    // HTTPTimeout指定對擴展器的調用的超時持續時間,過濾器超時沒法調度pod。Prioritize超時被忽略
    //k8s或其餘擴展器優先級被用來選擇節點
    HTTPTimeout time.Duration `json:"httpTimeout,omitempty"`
    //NodeCacheCapable指定擴展器可以緩存節點信息
    //因此調度器應該只發送關於合格節點的最少信息
    //假定擴展器已經緩存了羣集中全部節點的完整詳細信息
    NodeCacheCapable bool `json:"nodeCacheCapable,omitempty"`
    // ManagedResources是由擴展器管理的擴展資源列表.
    // - 若是pod請求此列表中的至少一個擴展資源,則將在Filter,Prioritize和Bind(若是擴展程序是活頁夾)
    //階段將一個窗格發送到擴展程序。若是空或未指定,全部pod將被髮送到這個擴展器。
    // 若是pod請求此列表中的至少一個擴展資源,則將在Filter,Prioritize和Bind(若是擴展程序是活頁夾)階段將一個pod發送到擴展程序。若是空或未指定,全部pod將被髮送到這個擴展器。
    ManagedResources []ExtenderManagedResource `json:"managedResources,omitempty"`
}
複製代碼

1.3 實現本身的調度算法

咱們能夠自定義本身的預選和優選算法,而後加載到算法工廠中,不過這樣須要修改代碼和從新編譯調度器

1.4 作一個符合業務需求的調度器

若是有特殊的調度需求的,而後確實沒法經過默認調度器解決的。能夠本身實現一個scheduler controller,在本身的scheduler controller中,可使用已經有的算法和本身的調度算法。這塊等後面本身有作了相關事項再補充分享。

六,收穫

1,編程和設計思想的收穫 (1) 工廠模式的使用教程

2,若是是我來設計,會怎麼作 我可能會給使用人員更多的靈活性,能夠支持自定義算法的動態加載,而不是須要從新編譯

七,參考文獻

1,Kubernetes scheduler V2草案

2,cizixs.com/2017/07/19/…

3,blog.leanote.com/post/criss_…

4,zhuanlan.zhihu.com/p/35429941

相關文章
相關標籤/搜索