1. 程式人生 > >[kubernetes系列]Scheduler模組深度講解

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

一,前言

排程器的職責是負責將Pod排程到最合適的Node上,但是要實現它並不是易事,需要考慮很多方面。(1) 公平性:排程後集群各個node應該保持均衡的狀態。(2) 效能:不能成為叢集的效能瓶頸。 (3) 擴充套件性:使用者能根據自身需求定製排程器和排程演算法。(4) 限制:需要考慮多種限制條件,例如親緣性,優先順序,Qos等。(5) 程式碼的優雅性,雖然不是一定要的^^。接下來帶著這些問題往下看。

二,排程器原始碼分析

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

(1) 初始化排程器

先生成configfatotry(可通過不同引數生成不同config),然後排程器可通過policy檔案,policy configmap,或者指定provider,通過configfactory來建立config,再由config生成scheduler。我們可以在啟動時候選擇policy啟動或者provider啟動scheduler模組。不管通過哪種方式建立,最終都會進入到CreateFromKeys去建立scheduler。

首先看如何獲取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。

// 根據已註冊的 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,優先順序佇列,排程,搶佔等邏輯,比較複雜,接下來慢慢理順。

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。

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