優先級(Priority)和搶占(Preemption)機制
優先級和搶占機制,解決的是Pod調度失敗時該怎么辦的問題
正常情況下,當一個Pod調度失敗后,它就會被暫時“擱置”起來,直到Pod被更新,或者集群狀態發生變化,調度器才會對這個Pod進行重新調度
特殊要求的場景:
當一個高優先級的Pod調度失敗后,該Pod並不會被“擱置”,而是會“擠走”某個Node上的一些低優先級的Pod.這樣就保證這個高優先級Pod的調度成功
優先級的實現
需要在Kubernetes里提交一個PriorityClass資源的定義
Kubernetes規定優先級是一個32bit的整數,最大值不超過1000000000(10 億,1 billion),並且值越大代表優先級越高.而超出10億的值,其實是被Kubernetes保留下來分配給系統 Pod 使用的.這樣做的目的,就是保證系統Pod不會被用戶搶占掉
YAML文件里的globalDefault被設置為true的話,那就意味着這個PriorityClass的值會成為系統的默認值.而如果這個值是false就表示我們只希望聲明使用該PriorityClass的Pod擁有值為1000000的優先級,對於沒有聲明PriorityClass的Pod來說它們的優先級就是0
在創建了PriorityClass對象之后,Pod 就可以聲明使用它.Kubernetes的PriorityAdmissionController就會自動將這個Pod的spec.priority字段設置為PriorityClass的值
優先級的體現:
當Pod擁了優先級之后,高優先級的Pod就可能會比低優先級的Pod提前出隊,從而盡早完成調度過程.這個過程,就是“優先級”這個概念在 Kubernetes里的主要體現
搶占的體現:
當一個高優先級的Pod調度失敗的時候,調度器的搶占能力就會被觸發.這時,調度器就會試圖從當前集群里尋找一個節點,使得當這個節點上的一個或者多個低優先級 Pod 被刪除后,待調度的高優先級Pod 就可以被調度到這個節點上.這個過程,就是“搶占”這個概念在Kubernetes里的主要體現
當上述搶占過程發生時,搶占者並不會立刻被調度到被搶占的Node上.調度器只會將搶占者的spec.nominatedNodeName字段,設置為被搶占的Node的名字.然后,搶占者會重新進入下一個調度周期,然后在新的調度周期里來決定是不是要運行在被搶占的節點上.即使在下一個調度周期,調度器也不會保證搶占者一定會運行在被搶占的節點上.原因如下:
1.調度器只會通過標准的DELETE API來刪除被搶占的Pod,所以這些Pod必然是有一定的“優雅退出”時間(默認是 30s)的,而在這段時間里,其他的節點也是有可能變成可調度的或者直接有新的節點被添加到這個集群中來.所以,鑒於優雅退出期間,集群的可調度性可能會發生的變化
2.在搶占者等待被調度的過程中,如果有其他更高優先級的Pod也要搶占同一個節點,那么調度器就會清空原搶占者的spec.nominatedNodeName字段,從而允許更高優先級的搶占者執行搶占,並且這也就使得原搶占者也有機會去重新搶占其他節點
搶占機制的設計和實現
搶占發生的原因,一定是一個高優先級的Pod調度失敗
搶占算法的一個最重要的設計,就是在調度隊列的實現里,使用了兩個不同的隊列
第一個隊列activeQ:
凡是在activeQ里的Pod,都是下一個調度周期需要調度的對象.所以,當你在 Kubernetes 集群里新創建一個Pod的時候,調度器會將這個Pod入隊到activeQ里面.而我在前面提到過的,調度器不斷從隊列里出隊(Pop)一個Pod 進行調度,實際上都是從activeQ里出隊的
第二個隊列unschedulableQ:
專門用來存放調度失敗的Pod,而這里的一個關鍵點就在於,當一個unschedulableQ里的Pod被更新之后,調度器會自動把這個Pod移動到activeQ里
搶占者調度失敗后執行兩個操作:
1.搶占者就會被放進unschedulableQ里面
2.調度失敗事件會觸發調度器為搶占者尋找犧牲者的流程
搶占的具體流程
1.調度器會檢查這次失敗事件的原因,來確認搶占是不是可以幫助搶占者找到一個新節點.這是因為有很多Predicates的失敗是不能通過搶占來解決的.比如,PodFitsHost 算法(負責的是,檢查Pod的nodeSelector與Node的名字是否匹配)這種情況下,除非Node 的名字發生變化,否則你即使刪除再多的Pod,搶占者也不可能調度成功
2.如果確定搶占可以發生,那么調度器就會把自己緩存的所有節點信息復制一份,然后使用這個副本來模擬搶占過程.
調度器會檢查緩存副本里的每一個節點,然后從該節點上最低優先級的Pod開始,逐一“刪除”這些Pod.而每刪除一個低優先級Pod,調度器都會檢查一下搶占者是否能夠運行在該 Node 上.一旦可以運行,調度器就記錄下這個Node的名字和被刪除Pod的列表,這就是一次搶占過程的結果了.
當遍歷完所有的節點之后,調度器會在上述模擬產生的所有搶占結果里做一個選擇,找出最佳結果.而這一步的判斷原則,就是盡量減少搶占對整個系統的影響.比如,需要搶占的 Pod 越少越好,需要搶占的Pod的優先級越低越好
在得到了最佳的搶占結果之后,這個結果里的Node,就是即將被搶占的Node.被刪除的Pod列表,就是犧牲者
3.調度器開始執行真正的搶占操作
第一步: 調度器會檢查犧牲者列表,清理這些Pod所攜帶的nominatedNodeName字段
第二步: 調度器會把搶占者的nominatedNodeName,設置為被搶占的Node的名字
搶占者Pod的更新操作,就會觸發調度器自動把這個Pod從unschedulableQ移動到activeQ里,讓搶占者在下一個調度周期重新進入調度流程
接下來,調度器就會通過正常的調度流程來調度搶占者,但是在任何一次正常調度流程中任何一個Pod都有可能被其它新的搶占者搶占,這其中包括了剛unschedulableQ中轉成activeQ里的pod 這就是為什么調度器並不保證搶占者一定會運行在當初選定的被搶占的Node上
第三步: 調度器會開啟一個Goroutine同步地刪除犧牲者
4.對於任何一個待調度的Pod在調度的時候需要處理的特殊額外情況
為某一對Pod和Node執行Predicates算法的時候,如果待檢查的Node是一個即將被搶占的節點,即:調度隊列里有nominatedNodeName字段值是該Node名字的Pod存在(可以稱之為:“潛在的搶占者”)那么調度器就會對這個Node將同樣的Predicates算法運行兩遍
如果沒有潛在搶占者調度一個Pod就只需執行一遍Predicates算法
第一遍: 調度器會假設上述“潛在的搶占者”已經運行在這個節點上,然后執行Predicates算法
由於InterPodAntiAffinity規則的存在.由於InterPodAntiAffinity規則關心待考察節點上所有Pod之間的互斥關系,所以在執行調度算法時必須考慮,如果搶占者已經存在於待考察Node上時,待調度Pod還能不能調度成功
在這里只要考慮那些優先級等於或者大於待調度Pod的搶占者.對於其他較低優先級Pod來說,待調度Pod總是可以通過搶占運行在待考察Node上
第二遍: 調度器會正常執行Predicates算法,即不考慮任何“潛在的搶占者"
因為“潛在的搶占者”最后不一定會運行在待考察的Node上,Kubernetes調度器並不保證搶占者一定會運行在當初選定的被搶占的Node上
只有這兩遍Predicates算法都能通過時,這個Pod 和Node才會被認為是可以綁定(bind)的
5.當整個集群發生可能會影響調度結果的變化(添加或者更新Node添加和更新 PV、Service 等)時,調度器會執行MoveAllToActiveQueue的操作,把所調度失敗的Pod從unscheduelableQ移動到activeQ里面
6.當一個已經調度成功的Pod被更新時,調度器則會將unschedulableQ里所有跟這個Pod有 Affinity/Anti-affinity關系的Pod,移動到 activeQ 里面