作者 | 王慶璨(阿里雲技術專家)、張凱(阿里雲高級技術專家)
導讀:阿里雲容器服務團隊結合多年 Kubernetes 產品與客戶支持經驗,對 Kube-scheduler 進行了大量優化和擴展,逐步使其在不同場景下依然能穩定、高效地調度各種類型的複雜工作負載。《進擊的 Kubernetes 調度系統》系列文章將把我們的經驗、技術思考和實現細節全面地展現給 Kubernetes 用戶和開發者,期望幫助大家更好地瞭解 Kubernetes 調度系統的強大能力和未來發展方向。本文為該系列文章的第二篇。
前言
什麼是 Coscheduling 和 Gang scheduling。Wikipedia 對 Coscheduling 的定義是“在併發系統中將多個相關聯的進程調度到不同處理器上同時運行的策略”。在 Coscheduling 的場景中,最主要的原則是保證所有相關聯的進程能夠同時啟動。防止部分進程的異常,導致整個關聯進程組的阻塞。這種導致阻塞的部分異常進程,稱之為“碎片(fragement)”。
在 Coscheduling 的具體實現過程中,根據是否允許“碎片”存在,可以細分為 Explicit Coscheduling,Local Coscheduling 和 Implicit Coscheduling。 其中 Explicit Coscheduling 就是大家常聽到的 Gang Scheduling。Gang Scheduling 要求完全不允許有“碎片”存在, 也就是“All or Nothing”。
我們將上述定義的概念對應到 Kubernetes 中,就可以理解 Kubernetes 調度系統支持批任務 Coscheduling 的含義了。 一個批任務(關聯進程組)包括了 N 個 Pod(進程),Kubernetes 調度器負責將這 N 個 Pod 調度到 M 個節點(處理器)上同時運行。如果這個批任務需要部分 Pod 同時啟動即可運行,我們稱需啟動 Pod 的最小數量為 min-available。特別地,當 min-available=N 時,批任務要求滿足 Gang Scheduling。
為什麼 Kubernetes 調度系統需要 Coscheduling?
Kubernetes 目前已經廣泛的應用於在線服務編排,為了提升集群的的利用率和運行效率,我們希望將 Kubernetes 作為一個統一的管理平臺來管理在線服務和離線作業。默認的調度器是以 Pod 為調度單元進行依次調度,不會考慮 Pod 之間的相互關係。但是很多數據計算類的離線作業具有組合調度的特點,即要求所有的子任務都能夠成功創建後,整個作業才能正常運行。如果只有部分子任務啟動的話,啟動的子任務將持續等待剩餘的子任務被調度。這正是 Gang Scheduling 的場景。
如下圖所示,JobA 需要 4 個 Pod 同時啟動,才能正常運行。Kube-scheduler 依次調度 3 個 Pod 並創建成功。到第 4 個 Pod 時,集群資源不足,則 JobA 的 3 個 Pod 處於空等的狀態。但是它們已經佔用了部分資源,如果第 4 個 Pod 不能及時啟動的話,整個 JobA 無法成功運行,更糟糕的是導致集群資源浪費。
如果出現更壞的情況的話,如下圖所示,集群其他的資源剛好被 JobB 的 3 個 Pod 所佔用,同時在等待 JobB 的第 4 個 Pod 創建,此時整個集群就出現了死鎖。
社區相關的方案
社區目前有 Kube-batch 以及基於 Kube-batch 衍生的 Volcano 2 個項目來解決上文中提到的痛點。實現的方式是通過開發新的調度器將 Scheduler 中的調度單元從 Pod 修改為 PodGroup,以組的形式進行調度。使用方式是如果需要 Coscheduling 功能的 Pod 走新的調度器,其他的例如在線服務的 Pod 走 Kube-scheduler 進行調度。
這些方案雖然能夠解決 Coscheduling 的問題,但是同樣引入了新的問題。如大家所知,對於同一集群資源,調度器需要中心化。但如果同時存在兩個調度器的話,有可能會出現決策衝突,例如分別將同一塊資源分配給兩個不同的 Pod,導致某個 Pod 調度到節點後因為資源不足,導致無法創建的問題。解決的方式只能是通過標籤的形式將節點強行的劃分開來,或者部署多個集群。這種方式通過同一個 Kubernetes 集群來同時運行在線服務和離線作業,勢必會導致整體集群資源的浪費以及運維成本的增加。再者,Volcano 運行需要啟動定製的 MutatingAdmissionWebhook 和 ValidatingAdmissionWebhook。這些 Webhooks 本身存在單點風險,一旦出現故障,將影響集群內所有 pod 的創建。另外,多運行一套調度器,本身也會帶來維護上的複雜性,以及與上游 Kube-scheduler 接口兼容上的不確定性。
基於 Scheduling Framework 的方案
《進擊的 Kubernetes 調度系統 (一):Scheduling Framework 》介紹了 Kubernetes Scheduling Framework 的架構原理和開發方法。在此基礎上,我們擴展實現了 Coscheduling 調度插件,幫助 Kubernetes 原生調度器支持批作業調度,同時避免上述方案存在的問題。Scheduling framework 的內容在前一篇文章詳細介紹,歡迎大家翻閱。
Kubernetes 負責 Kube-scheduler 的小組 sig-scheduler 為了更好的管理調度相關的 Plugin,新建了項目 scheduler-plugins 管理不同場景的 Plugin。我們基於 scheduling framework 實現的 Coscheduling Plugin 成為該項目的第一個官方插件,下面我將詳細的介紹 Coscheduling Plugin 的實現和使用方式。
技術方案
總體架構
PodGroup
我們通過 label 的形式來定義 PodGroup 的概念,擁有同樣 label 的 Pod 同屬於一個 PodGroup。min-available 是用來標識該 PodGroup 的作業能夠正式運行時所需要的最小副本數。
labels:
pod-group.scheduling.sigs.k8s.io/name: tf-smoke-gpu
pod-group.scheduling.sigs.k8s.io/min-available: "2"
備註: 要求屬於同一個 PodGroup 的 Pod 必須保持相同的優先級
Permit
Framework 的 Permit 插件提供了延遲綁定的功能,即 Pod 進入到 Permit 階段時,用戶可以自定義條件來允許 Pod 通過、拒絕 Pod 通過以及讓 Pod 等待狀態 (可設置超時時間)。Permit 的延遲綁定的功能,剛好可以讓屬於同一個 PodGruop 的 Pod 調度到這個節點時,進行等待,等待積累的 Pod 數目滿足足夠的數目時,再統一運行同一個 PodGruop 的所有 Pod 進行綁定並創建。
舉個實際的例子,當 JobA 調度時,需要 4 個 Pod 同時啟動,才能正常運行。但此時集群僅能滿足 3 個 Pod 創建,此時與 Default Scheduler 不同的是,並不是直接將 3 個 Pod 調度並創建。而是通過 Framework 的 Permit 機制進行等待。
此時當集群中有空閒資源被釋放後,JobA 的中 Pod 所需要的資源均可以滿足。
則 JobA 的 4 個 Pod 被一起調度創建出來,正常運行任務。
QueueSort
由於 Default Scheduler 的隊列並不能感知 PodGroup 的信息,所以 Pod 在出隊時處於無序性 (針對 PodGroup 而言)。如下圖所示,a 和 b 表示兩個不同的 PodGroup,兩個 PodGroup 的 Pod 在進入隊列時,由於創建的時間交錯導致在隊列中以交錯的順序排列。
當一個新的 Pod 創建後,入隊後,無法跟與其相同的 PodGroup 的 Pod 排列在一起,只能繼續以混亂的形式交錯排列。
這種無序性就會導致如果 PodGroupA 在 Permit 階段處於等待狀態,此時 PodGroupB 的 Pod 調度完成後也處於等待狀態,相互佔有資源使得 PodGroupA 和 PodGroupB 均無法正常調度。這種情況即是把死鎖現象出現的位置從 Node 節點移動到 Permit 階段,無法解決前文提到的問題。
針對如上所示的問題,我們通過實現 QueueSort 插件, 保證在隊列中屬於同一個 PodGroup 的 Pod 能夠排列在一起。我們通過定義 QueueSort 所用的 Less 方法,作用於 Pod 在入隊後排隊的順序:
func Less(podA *PodInfo, podB *PodInfo) bool
首先,繼承了默認的基於優先級的比較方式,高優先級的 Pod 會排在低優先級的 Pod 之前。
然後,如果兩個 Pod 的優先級相同,我們定義了新的排隊邏輯來支持 PodGroup 的排序。
- 如果兩個 Pod 都是 regularPod(普通的 Pod),則誰先創建誰在隊列裡邊排在前邊;
- 如果兩個 Pod 一個是 regularPod,另一個是 pgPod(屬於某個 PodGroup 的 Pod), 我們比較的是 regularPod 的創建時間和 pgPod 所屬 PodGroup 的創建時間,則誰先創建誰在隊列裡邊排在前邊;
- 如果兩個 Pod 都是 pgPod,我們比較兩個 PodGroup 的創建時間,則誰先創建誰在隊列裡邊排在前邊。同時有可能兩個 PodGroup 的創建時間相同,我們引入了自增 Id,使得 PodGroup 的 Id 誰小誰排在前邊 (此處的目的是為了區分不同的 PodGroup)。
通過如上的排隊策略,我們實現屬於同一個 PodGroup 的 Pod 能夠同一個 PodGroup 的 Pod 能夠排列在一起。
當一個新的 Pod 創建後,入隊後,會跟與其相同的 PodGroup 的 Pod 排列在一起。
Prefilter
為了減少無效的調度操作,提升調度的性能,我們在 Prefilter 階段增加一個過濾條件,當一個 Pod 調度時,會計算該 Pod 所屬 PodGroup 的 Pod 的 Sum(包括 Running 狀態的),如果 Sum 小於 min-available 時,則肯定無法滿足 min-available 的要求,則直接在 Prefilter 階段拒絕掉,不再進入調度的主流程。
UnReserve
如果某個 Pod 在 Permit 階段等待超時了,則會進入到 UnReserve 階段,我們會直接拒絕掉所有跟 Pod 屬於同一個 PodGroup 的 Pod,避免剩餘的 Pod 進行長時間的無效等待。
Coscheduling 試用
安裝部署
用戶既可以在自己搭建的 Kubernetes 集群中,也可以在任一個公有云提供的標準 Kubernetes 服務中來試用 Coscheduling。需要注意的是集群版本 1.16+, 以及擁有更新集群 master 的權限。
本文將使用 阿里雲容器服務 ACK 提供的 Kubernetes 集群來進行測試。
前提條件
- 支持 Kubernetes 1.16 以上版本
- 選擇創建 ACK 提供的標準專有集群(Dedicated cluster)
- 保證集群節點可以訪問公網
- helm v3:ACK 在 master 節點上默認安裝 helm,請確認版本是否為 helm v3,如果是 helm v2 請升級值 helm v3,安裝 helm v3 請參考 helm v3 安裝。
部署 Coscheduling
我們已經將 Coscheduling 插件和原生調度器代碼統一構建成新的容器鏡像。並提供了一個 helm chart 包 ack-coscheduling 來自動安裝。它會啟動一個任務,自動用 Coscheduling scheduler 替換集群默認安裝的原生 scheduler,並且會修改 scheduler 的相關 Config 文件,使 scheduling framework 正確地加載 Coscheduling 插件。完成試用後,用戶可通過下文提示的卸載功能恢復集群默認 scheduler 及相關配置。
下載 helm chart 包,執行命令安裝:
$ wget http://kubeflow.oss-cn-beijing.aliyuncs.com/ack-coscheduling.tar.gz
$ tar zxvf ack-coscheduling.tar.gz
$ helm install ack-coscheduling -n kube-system ./ack-coscheduling
NAME: ack-coscheduling
LAST DEPLOYED: Mon Apr 13 16:03:57 2020
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
驗證 Coscheduling
在 Master 節點上,使用 helm 命令驗證是否安裝成功。
$ helm get manifest ack-coscheduling -n kube-system | kubectl get -n kube-system -f -
NAME COMPLETIONS DURATION AGE
scheduler-update-clusterrole 1/1 8s 35s
scheduler-update 3/1 of 3 8s 35s
卸載 Coscheduling
通過 helm 卸載,將 kube-scheduler 的版本及配置回滾到集群默認的狀態。
$ helm uninstall ack-coscheduling -n kube-system
使用方式
使用 Coscheduling 時,只需要在創建任務的 yaml 描述中配置 pod-group.scheduling.sigs.k8s.io/name 和 pod-group.scheduling.sigs.k8s.io/min-available 這兩個 label 即可。
labels:
pod-group.scheduling.sigs.k8s.io/name: tf-smoke-gpu
pod-group.scheduling.sigs.k8s.io/min-available: "3"
- pod-group.scheduling.sigs.k8s.io/name:用於表示 PodGroup 的 Name;
- pod-group.scheduling.sigs.k8s.io/min-available: 用於表示當前集群資源至少滿足 min-available 個 pod 啟動時,才能整體調度該任務。
備註: 屬於同一個 PodGroup 的 Pod 必須保持相同的優先級。
Demo 展示
接下來我們通過運行 Tensorflow 的分佈式訓練作業來演示 Coscheduling 的效果。當前測試集群有 4 個 GPU 卡
- 通過 Kubeflow 的 Arena 工具在已有 Kubernetes 集群中部署 Tensorflow 作業運行環境。
Arena 是基於 Kubernetes 的機器學習系統開源社區 Kubeflow 中的子項目之一。Arena 用命令行和 SDK 的形式支持了機器學習任務的主要生命週期管理(包括環境安裝,數據準備,到模型開發,模型訓練,模型預測等),有效提升了數據科學家工作效率。
git clone https://github.com/kubeflow/arena.git
kubectl create ns arena-system
kubectl create -f arena/kubernetes-artifacts/jobmon/jobmon-role.yaml
kubectl create -f arena/kubernetes-artifacts/tf-operator/tf-crd.yaml
kubectl create -f arena/kubernetes-artifacts/tf-operator/tf-operator.yaml
檢查是否部署成功:
$ kubectl get pods -n arena-system
NAME READY STATUS RESTARTS AGE
tf-job-dashboard-56cf48874f-gwlhv 1/1 Running 0 54s
tf-job-operator-66494d88fd-snm9m 1/1 Running 0 54s
- 用戶向集群中提交 Tensorflow 作業 (TFJob),如下示例,包含 1 個 Parameter Server pod 和 4 個 Worker pod,每個 Worker 需要 2 個 GPU。配置了 pod group,需要至少 5 個 pod 啟動後才能運行。
apiVersion: "kubeflow.org/v1"
kind: "TFJob"
metadata:
name: "tf-smoke-gpu"
spec:
tfReplicaSpecs:
PS:
replicas: 1
template:
metadata:
creationTimestamp: null
labels:
pod-group.scheduling.sigs.k8s.io/name: tf-smoke-gpu
pod-group.scheduling.sigs.k8s.io/min-available: "5"
spec:
containers:
- args:
- python
- tf_cnn_benchmarks.py
- --batch_size=32
- --model=resnet50
- --variable_update=parameter_server
- --flush_stdout=true
- --num_gpus=1
- --local_parameter_device=cpu
- --device=cpu
- --data_format=NHWC
image: registry.cn-hangzhou.aliyuncs.com/kubeflow-images-public/tf-benchmarks-cpu:v20171202-bdab599-dirty-284af3
name: tensorflow
ports:
- containerPort: 2222
name: tfjob-port
resources:
limits:
cpu: '1'
workingDir: /opt/tf-benchmarks/scripts/tf_cnn_benchmarks
restartPolicy: OnFailure
Worker:
replicas: 4
template:
metadata:
creationTimestamp: null
labels:
pod-group.scheduling.sigs.k8s.io/name: tf-smoke-gpu
pod-group.scheduling.sigs.k8s.io/min-available: "5"
spec:
containers:
- args:
- python
- tf_cnn_benchmarks.py
- --batch_size=32
- --model=resnet50
- --variable_update=parameter_server
- --flush_stdout=true
- --num_gpus=1
- --local_parameter_device=cpu
- --device=gpu
- --data_format=NHWC
image: registry.cn-hangzhou.aliyuncs.com/kubeflow-images-public/tf-benchmarks-gpu:v20171202-bdab599-dirty-284af3
name: tensorflow
ports:
- containerPort: 2222
name: tfjob-port
resources:
limits:
nvidia.com/gpu: 2
workingDir: /opt/tf-benchmarks/scripts/tf_cnn_benchmarks
restartPolicy: OnFailure
- 當用戶不使用 Coscheduling 功能時
刪除上述 TFJob yaml 中的 pod-group.scheduling.sigs.k8s.io/name 和 pod-group.scheduling.sigs.k8s.io/min-available 標籤,表示該任務不使用 Coscheduling。創建任務後,集群資源只能滿足 2 個 Worker 啟動,剩餘兩個處於 Pending 狀態。
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
tf-smoke-gpu-ps-0 1/1 Running 0 6m43s
tf-smoke-gpu-worker-0 1/1 Running 0 6m43s
tf-smoke-gpu-worker-1 1/1 Running 0 6m43s
tf-smoke-gpu-worker-2 0/1 Pending 0 6m43s
tf-smoke-gpu-worker-3 0/1 Pending 0 6m43s
查看其中正在運行的 Worker 的日誌,都處於等待剩餘那兩個 Worker 啟動的狀態。此時,4 個 GPU 都被佔用卻沒有實際執行任務。
$ kubectl logs -f tf-smoke-gpu-worker-0
INFO|2020-05-19T07:02:18|/opt/launcher.py|27| 2020-05-19 07:02:18.199696: I tensorflow/core/distributed_runtime/master.cc:221] CreateSession still waiting for response from worker: /job:worker/replica:0/task:3
INFO|2020-05-19T07:02:28|/opt/launcher.py|27| 2020-05-19 07:02:28.199798: I tensorflow/core/distributed_runtime/master.cc:221] CreateSession still waiting for response from worker: /job:worker/replica:0/task:2
- 當用戶使用 Coscheduling 功能時
添加 pod-group 相關標籤後創建任務,因為集群的資源無法滿足用戶設定的 min-available 要求,則 PodGroup 無法正常調度,所有的 Pod 一直處於 Pending 狀態。
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
tf-smoke-gpu-ps-0 0/1 Pending 0 43s
tf-smoke-gpu-worker-0 0/1 Pending 0 43s
tf-smoke-gpu-worker-1 0/1 Pending 0 43s
tf-smoke-gpu-worker-2 0/1 Pending 0 43s
tf-smoke-gpu-worker-3 0/1 Pending 0 43s
此時,如果通過集群擴容,新增 4 個 GPU 卡,資源能滿足用戶設定的 min-available 要求,則 PodGroup 正常調度,4 個 Worker 開始運行。
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
tf-smoke-gpu-ps-0 1/1 Running 0 3m16s
tf-smoke-gpu-worker-0 1/1 Running 0 3m16s
tf-smoke-gpu-worker-1 1/1 Running 0 3m16s
tf-smoke-gpu-worker-2 1/1 Running 0 3m16s
tf-smoke-gpu-worker-3 1/1 Running 0 3m16s
查看其中一個 Worker 的日誌,顯示訓練任務已經開始:
$ kubectl logs -f tf-smoke-gpu-worker-0
INFO|2020-05-19T07:15:24|/opt/launcher.py|27| Running warm up
INFO|2020-05-19T07:21:04|/opt/launcher.py|27| Done warm up
INFO|2020-05-19T07:21:04|/opt/launcher.py|27| Step Img/sec loss
INFO|2020-05-19T07:21:05|/opt/launcher.py|27| 1 images/sec: 31.6 +/- 0.0 (jitter = 0.0) 8.318
INFO|2020-05-19T07:21:15|/opt/launcher.py|27| 10 images/sec: 31.1 +/- 0.4 (jitter = 0.7) 8.343
INFO|2020-05-19T07:21:25|/opt/launcher.py|27| 20 images/sec: 31.5 +/- 0.3 (jitter = 0.7) 8.142
後續工作
利用 Kubernetes Scheduling Framework 的機制實現了 Coscheduling,解決了 AI、數據計算類的批任務需要組合調度,同時減少資源浪費的問題。從而提升集群整體資源利用率。
作者介紹:
王慶璨,阿里雲技術專家,專注於大規模集群資源管理和調度。Kubernetes 社區成員,主要參與 Kube-scheduler 社區開發。目前負責阿里雲容器服務 ACK 資源調度和雲原生 AI 相關工作。
張凱,阿里雲高級技術專家,從事容器服務 ACK 和雲原生 AI 解決方案的研發和客戶支持,以及相關領域的開源建設。擁有 10 餘年大規模深度學習平臺,雲計算,SOA 等領域經驗。
“阿里巴巴雲原生關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的公眾號。”