生產環境中的kubernetes 優先順序與搶佔

  • 2019 年 10 月 21 日
  • 筆記

kubernetes 中的搶佔功能是調度器比較重要的feature,但是真正使用起來還是比較危險,否則很容易把低優先順序的pod給無辜kill。為了提高GPU集群的資源利用率,決定勇於嘗試一番該featrue。當然使用之前還是得閱讀一下相關的程式碼做到心裡有數,出了問題也方便定位修復。

基本原理

優先順序與搶佔是為了確保一個高優先順序的pod在調度失敗後,可以通過"擠走" 低優先順序的pod,騰出空間後保證它可以調度成功。 我們首先需要在集群中聲明PriorityClass來定義優先等級數值和搶佔策略,

apiVersion: scheduling.k8s.io/v1  kind: PriorityClass  metadata:    name: high  value: 10000  preemptionPolicy: Never  globalDefault: false  description: "This priority class should be used for high priority service pods."  ---  apiVersion: scheduling.k8s.io/v1  kind: PriorityClass  metadata:    name: low  value: -999  globalDefault: false  description: "This priority class should be used for log priority service pods."

如上所示定義了兩個PriorityClass對象。然後就可以在pod中聲明使用它了:

apiVersion: apps/v1  kind: Deployment  metadata:    labels:      run: nginx    name: high-nginx  spec:    replicas: 1    selector:      matchLabels:        run: nginx    template:      metadata:        labels:          run: nginx      spec:        containers:        - image: nginx          imagePullPolicy: Always          name: nginx          resources:            limits:              cpu: "500m"        priorityClassName: high

這個 Pod 通過 priorityClassName 欄位。聲明了要使用名叫 high-priority 的 PriorityClass。當這個 Pod 被提交給 Kubernetes 之後,Kubernetes 的 Priority AdmissionController 就會自動將這個 Pod 的spec.priority 欄位設置為10000。
如下:

  preemptionPolicy: Never    priority: 10000    priorityClassName: high

Pod創建好之後,調度器就會根據創建的priority進行調度的決策,首先會在等待隊列中優先調度,如果調度失敗就會進行搶佔: 依次遍歷所有的node找出最適合的node,將該nodename填充在spec.nominatedNodeName欄位上,然後等待被搶佔的pod全都退出後再次嘗試調度到該node之上。具體的邏輯請自行閱讀相關程式碼,此處不在贅述。

生產環境使用方式

  • v1.14版本的kubernetes該feature已經GA,默認開啟,但此時我們往往沒有做好準備,如果直接給pod設置優先順序會導致很多意料之外的搶佔,造成故障。 (參見How a Production Outage Was Caused Using Kubernetes Pod Priorities)。所以建議在初次使用的時候還是先顯式關閉搶佔,只設置優先順序,等集群中所有的pod都有了各自的優先順序之後再開啟,此時所有的搶佔都是可預期的。可以通過kube-scheduler 配置文件中的disablePreemption: true進行關閉
  • 調度器是根據優先順序pod.spec.priority數值來決定優先順序的,而用戶是通過指定pod.sepc.priorityclass的名字來為pod選擇優先順序的,此時就需要Priority AdmissionController根據priorityclass name為pod自動轉換並設置對應的priority數值。我們需要確保該admissionController開啟,如果你的kube-apiserver中還是通過--admission-control flag來指定admissionoController的話需要手動添加Priority admissonController,如果是通過--enable-admission-plugins來指定的話,無需操作,該admissionController默認開啟。
  • 按照集群規劃創建對應的PriorityClass及其對應的搶佔策略,目前支援兩種策略: Never, PreemptLowerPriority。 Never可以指定不搶佔其他pod, 即使該pod優先順序特別高,這對於一些離線任務較為友好。 非搶佔調度在v1.15中為alpha, 需要通過--feature-gates=NonPreemptingPriority=true 進行開啟。
  • 在創建好了PriorityClass之後,需要防止高優先順序的pod過分佔用太多資源,使用resourceQuota機制來限制其使用量,避免低優先順序的pod總是被高優先順序的pod壓制,造成資源飢餓。resoueceQuote可以通過指定scope為PriorityClass來限定某個優先順序能使用的資源量:
apiVersion: v1  kind: ResourceQuota  metadata:    name: high-priority  spec:    hard:      pods: "10"    scopeSelector:      matchExpressions:      - operator : In        scopeName: PriorityClass        values: ["high"]

如上即為限制高優先順序的pod最多能創建10個。operator指定了作用的對象,operator: In可以顯式指定作用於的哪些priorityClass,operator: Exists則指定作用於該namespace下的所有priorityClass。

  • 有時候我們想要只有priorityClass對應的resourceQuota存在之後才能創建pod,確保所有的priorityClass的pod資源都是受控的。 如果那個namespacew沒有該resourceQuota則拒絕該pod的創建,該操作可以通過指定--admission-control-config-file文件來設置,內容如下:
apiVersion: apiserver.k8s.io/v1alpha1  kind: AdmissionConfiguration  plugins:  - name: "ResourceQuota"    configuration:      apiVersion: resourcequota.admission.k8s.io/v1beta1      kind: Configuration      limitedResources:      - resource: pods        matchScopes:        - scopeName: PriorityClass          operator: In          values: ["high"]

該配置定義了: "high"優先順序的pod只能在指定的namespaces下創建,該namespaces有作用於"high"優先順序的resouceQuota,上面第四步中的resouceQuota即可滿足要求。
scopeName定義了作用的對象為priorityClass, operator指定了作用的範圍,可以是In操作符,指定某幾個value, 也可以是Exits操作符,指定所有的PriorityClass必須有對應的quota存在, 否則該namespace就無法創建該優先順序的pod,這些操作符與上面resouceQuota中定義的一一對應。通過這樣就限制了一些優先順序只能在有資源約束的namespace下創建。

  • 如果沒有顯式指定優先順序,則默認的優先順序值為0,需要結合業務規劃決定是否有必要調整默認優先順序。
  • 對於一些daemonset需要顯式設置較高的優先順序來防止被搶佔,在部署一個新的daemonset的時候需要考慮是否會造成大規模pod的搶佔。
  • 等到所有的優先順序設置完畢之後就可以開啟搶佔功能了,此時集群中所有pending 的高優先順序pod就會瞬間搶佔,還是需要額外小心,確保集群中高優先順序的pod不會導致低優先順序的pod大規模被kill,如果我們提前設置了對應的resource quota值,則會有一定的資源約束。
  • 優先順序和搶佔對於資源的精細化運營考驗很大,對於resource quota的設置需要十分精細,需要考慮兩個維度來設置: namespace層面和priority層面,我們既希望限制namespace使用的資源,有希望某個priority使用的資源,防止低優先順序的pod資源飢餓。 可以在初期只考慮namespace層面的限制,priority層面通過上層業務來保證,例如創建任務的時候保證集群中高優先順序的資源使用量不超過50%等。

其他思考

筆者的線上環境中, 有些再跑的模型訓練任務業務對於自動failove實現不是很好,如果中途被搶佔了只能從頭開始計算,需要佔用額外的GPU資源,這種工作類型不允許被搶佔,但是如果把他設置為高優先順序又不太合適,因為它確實不是最高的優先順序,優先順序最高的還是在線業務,不能讓它搶佔在線業務。 它屬於中間優先順序,可以搶佔低優先順序的pod。 經過探索發現目前kubernetes並不支援該中類型,當前支援的搶佔策略為: Never, PreemptLowerPriority都無法滿足需求。所以在此基礎上開發了NonPreemptible類型的搶佔策略,該優先順序的pod是不允許被其他人搶佔的,調度還是按照優先順序在隊列里排隊,但是一旦調度上去就無法被搶佔。
這種搶佔策略略顯"霸道",所以需要謹慎使用,設置resouceQuota,並且只能由特定的任務使用。 並且為了不影響deamonset等優先順序最高的任務,允許被某個指定Priority數值之上的pod搶佔。並且隨著業務的發展,這部分邏輯需要逐步去掉,之所有存在這部分邏輯是因為pod不能被中斷,不能被搶佔,所以還是需要使這些任務支援重啟與掛起,具體來說就是: pod掛載遠程磁碟並自動checkpoint,重啟之後從以前的恢復點繼續執行。等這些業務改造完成之後,逐步去掉這種工作搶佔策略

reference

Pod Priority and Preemption
Priority in ResourceQuota
Allow PriorityClasses To Be Non-Preempting