[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,如果是我來設計,會怎麼做 我可能會給使用人員更多的靈活性,可以支援自定義演算法的動態載入,而不是需要重新編譯