調度器的職責是負責將Pod調度到最合適的Node上,可是要實現它並非易事,須要考慮不少方面。(1) 公平性:調度後集羣各個node應該保持均衡的狀態。(2) 性能:不能成爲集羣的性能瓶頸。 (3) 擴展性:用戶能根據自身需求定製調度器和調度算法。(4) 限制:須要考慮多種限制條件,例如親緣性,優先級,Qos等。(5) 代碼的優雅性,雖然不是必定要的^^。接下來帶着這些問題往下看。html
接下來一邊說明調度的步驟,一邊看源碼(只分析主幹代碼),而後思考有沒有更好的方式。調度這裏,分紅幾個重要的步驟:1,初始化調度器;2,獲取未調度的Pod開始調度;3,預調度,優調度和擴展;4,調度失敗則發起搶佔。這裏只跟着流程走,具體有必要更詳細解讀的放在下面幾部分。本文代碼基於1.12.1版本node
先生成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)
}
複製代碼
調度邏輯包括了篩選合適node,優先級隊列,調度,搶佔等邏輯,比較複雜,接下來慢慢理順。json
首先看一小段主要代碼,這代碼已經把調度邏輯的大致交代了,再基於這主要的代碼展開分析。後端
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)
}
複製代碼
從初始化調度器的源碼分析中,咱們知道,使用的隊列是優先級隊列,那麼此時則是從優先級隊列中獲取優先級最高的pod。api
func (c *configFactory) getNextPod() *v1.Pod {
pod, err := c.podQueue.Pop()
}
複製代碼
經過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來計算。
若是正常調度沒法調度到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
}
}
複製代碼
調度器會選擇一個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仍是會被驅逐。
在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都不復雜,就不分析了
優選的算法也不少,這裏列出幾個
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資源不足時候的驅逐順序。
看結構體定義便可,其餘的代碼都是很容易看懂
type PriorityQueue struct {
// 有序堆,按照優先級存放等待調度的pod
activeQ *Heap
// 嘗試調度而且調度失敗的pod
unschedulableQ *UnschedulablePodsMap
// 存儲高優先級pod(發生了搶佔)指望調度的node信息,即有NominatedNodeName Annotation的pod
nominatedPods map[string][]*v1.Pod
receivedMoveRequest bool
}
複製代碼
若是在1.11版本之前,須要先開啓該特性。
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."
複製代碼
例如指定上面的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
複製代碼
因爲在驅逐pod時候,優雅關閉須要等待必定的時間,那麼致使pod真正被調度時候會存在一個時間差,咱們能夠優化低優先級的pod的優雅關閉時間或者調低優雅關閉時間
調度器會嘗試在不違反PDB狀況下去驅逐pod,可是隻是嘗試,若是找不到或者仍是不知足狀況下,仍然爲刪除低優先級的pod
若是在node上的pod存在inter-pod affinity,那麼因爲inter-pod affinity規則,pod P是沒法調度到該pod的(若是須要驅逐這些inter-pod affinity 的pod)。因此若是咱們有這塊的需求,須要保證後調度的pod的優先級不高於前面的。
若是pod P要調度到N,pod Q此時已經在經過zone下的不一樣node運行,P和Q若是存在zone-wide的anti-affinity,那麼P將沒法調度到N上,由於沒法跨node去驅逐Q。
經過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
複製代碼
這裏徹底複製第四個參考文獻。 利用咱們上面分析源碼知道的,可使用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"`
}
複製代碼
咱們能夠自定義本身的預選和優選算法,而後加載到算法工廠中,不過這樣須要修改代碼和從新編譯調度器
若是有特殊的調度需求的,而後確實沒法經過默認調度器解決的。能夠本身實現一個scheduler controller,在本身的scheduler controller中,可使用已經有的算法和本身的調度算法。這塊等後面本身有作了相關事項再補充分享。
1,編程和設計思想的收穫 (1) 工廠模式的使用教程
2,若是是我來設計,會怎麼作 我可能會給使用人員更多的靈活性,能夠支持自定義算法的動態加載,而不是須要從新編譯