機(jī)器學(xué)習(xí)平臺在Kubernetes上的實踐
背景
過去音樂算法的模型訓(xùn)練任務(wù),是在物理機(jī)上進(jìn)行開發(fā)、調(diào)試以及定時調(diào)度。每個算法團(tuán)隊使用屬于自己的獨立物理機(jī),這種現(xiàn)狀會造成一些問題。比如物理機(jī)的分布零散,缺乏統(tǒng)一的管理,主要依賴于doc文檔的表格記錄機(jī)器的使用與歸屬;各業(yè)務(wù)間機(jī)器資源的調(diào)配,有時需要機(jī)器在不同機(jī)房的搬遷,耗時耗力。另外,由于存在多人共用、開發(fā)與調(diào)度任務(wù)共用等情況,會造成環(huán)境的相互影響,以及資源的爭奪。針對當(dāng)前的情況,總結(jié)問題如下:
- 資源利用率低:部分機(jī)器資源利用率偏低;無法根據(jù)各個業(yè)務(wù)的不同階段,在全局內(nèi)快速、動態(tài)的實現(xiàn)擴(kuò)縮容,以達(dá)到資源的合理配置,提升資源整體利用率;
- 環(huán)境相互影響:存在多人共用、測試與調(diào)度混用同一開發(fā)機(jī),未做任何的隔離,造成可能的環(huán)境、共享資源的相互影響與爭奪;
- 監(jiān)控報警缺失:物理機(jī)模式,任務(wù)監(jiān)控報警功能缺失,導(dǎo)致任務(wù)無法運維,或者效率低。
資源沒有全局統(tǒng)一的合理調(diào)配,會出現(xiàn)負(fù)載不均衡,資源不能最大化的利用。
Kubernetes的嘗試
在快速的擴(kuò)縮容、環(huán)境隔離、資源監(jiān)控等方面,Kubernetes及其相關(guān)擴(kuò)展,可以很好的解決問題。現(xiàn)將物理機(jī)集中起來,并構(gòu)建成一個Kubernetes集群。通過分析算法同事以往的工作方式,機(jī)器學(xué)習(xí)平臺(GoblinLab)決定嘗試基于Kubernetes提供在線的開發(fā)調(diào)試容器環(huán)境以及任務(wù)的容器化調(diào)度兩種方案,其分別針對任務(wù)開發(fā)和任務(wù)調(diào)度兩種場景。
任務(wù)開發(fā)
為最大化的減少算法同事由物理機(jī)遷移到容器化環(huán)境的學(xué)習(xí)成本,GoblinLab系統(tǒng)中基本將Kubernetes的容器當(dāng)做云主機(jī)使用。容器的鏡像以各版本Tensoflow鏡像為基礎(chǔ)(底層是Ubuntu),集成了大數(shù)據(jù)開發(fā)環(huán)境(Hadoop、Hive、Spark等Client),安裝了常用的軟件。另外,為了方便使用,容器環(huán)境提供了Jupyter Lab、SSH登錄、Code Server(VSCode)三種使用方式。
在GoblinLab中新建容器化開發(fā)環(huán)境比較簡單,只需選擇鏡像,填寫所需的資源,以及需要掛載的外部存儲即可(任務(wù)開發(fā)的環(huán)境下文簡稱開發(fā)實例)。
新建環(huán)境配置后,點擊啟動實例,容器初始化時,會自動啟動Jupyter lab、SSH以及CodeServer。
Jupyter Lab:
Code Server:
SSH登錄:
算法可以選擇以上任意一種方式進(jìn)行任務(wù)的開發(fā),或者調(diào)試。由于提供了Code Server(VSCode),所以可以獲得更好的體驗。
任務(wù)開發(fā)所用的容器化環(huán)境,在底層Kubernetes上是通過StatefulSet類型實現(xiàn),對應(yīng)資源編排文件如下(已精簡細(xì)節(jié)):
- kind: StatefulSet
- apiVersion: apps/v1
- metadata:
- name: ${name}
- namespace: "${namespace}"
- spec:
- replicas: 1
- selector:
- matchLabels:
- statefulset: ${name}
- system/app: ${name}
- template:
- spec:
- <#if (gpu > 0)>
- tolerations:
- - effect: NoSchedule
- key: nvidia.com/gpu
- value: "true"
- </#if>
- <#if usePrivateRepository == "true">
- imagePullSecrets:
- - name: registrykey-myhub
- </#if>
- volumes:
- - name: localtime
- hostPath:
- path: /etc/localtime
- <#if MountPVCs?? && (MountPVCs?size > 0)>
- <#list MountPVCs?keys as key>
- - name: "${key}"
- persistentVolumeClaim:
- claimName: "${key}"
- </#list>
- containers:
- - name: notebook
- image: ${image}
- imagePullPolicy: IfNotPresent
- volumeMounts:
- - name: localtime
- mountPath: /etc/localtime
- <#if readMountPVCs?? && (readMountPVCs?size > 0)>
- <#list readMountPVCs?keys as key>
- - name: "${key}"
- mountPath: "${readMountPVCs[key]}"
- readOnly: true
- </#list>
- </#if>
- <#if writeMountPVCs?? && (writeMountPVCs?size > 0)>
- <#list writeMountPVCs?keys as key>
- - name: "${key}"
- mountPath: "${writeMountPVCs[key]}"
- </#list>
- </#if>
- env:
- - name: NOTEBOOK_TAG
- value: "${name}"
- - name: HADOOP_USER
- value: "${hadoopUser}"
- - name: PASSWORD
- value: "${password}"
- resources:
- requests:
- cpu: ${cpu}
- memory: ${memory}Gi
- <#if (gpu > 0)>
- nvidia.com/gpu: ${gpu}
- </#if>
- limits:
- cpu: ${cpu}
- memory: ${memory}Gi
- <#if (gpu > 0)>
- nvidia.com/gpu: ${gpu}
- </#if>
目前GolbinLab已提供基于Tensoflow各版本的CPU與GPU通用鏡像11個,以及多個定制化鏡像。
任務(wù)調(diào)度
算法同事在使用容器化環(huán)境之前,任務(wù)的開發(fā)調(diào)度都是在GPU物理機(jī)器上完成,調(diào)度一般都是通過定時器或crontab命令調(diào)度任務(wù),任務(wù)無失敗、超時等報警,以及也沒有重試等機(jī)制,基本無相關(guān)的任務(wù)運維工具。
在介紹容器中開發(fā)的任務(wù)如何上線調(diào)度之前,先簡要介紹一下GoblinLab的系統(tǒng)架構(gòu)。
上圖為GoblinLab簡化的系統(tǒng)架構(gòu),其中主要分為四層,由上到下分別為:
- Application-應(yīng)用層:提供直接面向用戶的機(jī)器學(xué)習(xí)開發(fā)平臺(GoblinLab)
- Middle-中間層: 中間層,主要是接入了統(tǒng)一的調(diào)度、報警、以及配置等服務(wù)
- Wizard-執(zhí)行服務(wù): 其提供統(tǒng)一的執(zhí)行服務(wù),提供包含Kubernetes、Spark、Jar等各類任務(wù)的提交執(zhí)行。插件式,支持快速擴(kuò)展
- Infrastructure-基礎(chǔ)設(shè)施: 底層的基礎(chǔ)設(shè)施,主要包含Kubernetes集群、Spark集群以及普通服務(wù)器等
GolbinLab為了保障調(diào)度任務(wù)的穩(wěn)定性,將任務(wù)的開發(fā)與調(diào)度拆分,改變之前算法直接在物理機(jī)上開發(fā)完任務(wù)后,通過定時器或者crontab調(diào)度任務(wù)的方式。如上圖所示,在開發(fā)完成后,任務(wù)的調(diào)度是通過任務(wù)流中的容器化任務(wù)調(diào)度組件實現(xiàn),用戶需填組件的相關(guān)參數(shù)(代碼所在PVC及路徑,配置鏡像等),再通過任務(wù)流的調(diào)度功能實現(xiàn)任務(wù)調(diào)度。與任務(wù)開發(fā)不同,每個調(diào)度任務(wù)執(zhí)行在獨立的容器中,保證任務(wù)間相互隔離,同時通過后續(xù)介紹的資源隔離方案,可以優(yōu)先保障線上調(diào)度任務(wù)所需資源。
任務(wù)調(diào)度執(zhí)行的一般流程如下:
任務(wù)調(diào)度執(zhí)行時在Kubernetes上資源編排文件(已精簡細(xì)節(jié)):
- apiVersion: batch/v1
- kind: Job
- metadata:
- name: ${name}
- namespace: ${namespace}
- spec:
- template:
- spec:
- containers:
- - name: jupyter-job
- image: ${image}
- env:
- - name: ENV_TEST
- value: ${envTest}
- command: ["/bin/bash", "-ic", "cd ${workDir} && \
- ${execCommand} /root/${entryPath} ${runArgs}"]
- volumeMounts:
- - mountPath: "/root"
- name: "root-dir"
- resources:
- requests:
- cpu: ${cpu}
- memory: ${memory}Gi
- <#if (gpu > 0)>
- nvidia.com/gpu: ${gpu}
- </#if>
- limits:
- cpu: ${cpu}
- memory: ${memory}Gi
- <#if (gpu > 0)>
- nvidia.com/gpu: ${gpu}
- </#if>
- volumes:
- - name: "root-dir"
- persistentVolumeClaim:
- claimName: "${pvc}"
- backoffLimit: 0
權(quán)限控制
容器化開發(fā)環(huán)境配置啟動后,用戶可以通過SSH登錄、CodeServer或JupyterLab等其中一種方式使用。為了避免容器化開發(fā)環(huán)境被其他人使用,GoblinLab給每種方式都設(shè)置了統(tǒng)一的密鑰,而密鑰在每次啟動時隨機(jī)生成。
1、隨機(jī)生成密碼
2、設(shè)置賬號密碼(SSH登錄密碼)
- echo "root:${password}" | chpasswd
3、設(shè)置Code Server密碼 (VSCode)
- #設(shè)置環(huán)境變量PASSWORD即可
- env:
- - name: PASSWORD
- value: "${password}"
4、設(shè)置Jupyter Lab密碼
- jupyter notebook --generate-config,~/.jupyter 目錄下生成jupyter_notebook_config.py,并添加代碼
- import os
- from IPython.lib import passwd
- c = c # pylint:disable=undefined-variable
- c.NotebookApp.ip = '0.0.0.0' # https://github.com/jupyter/notebook/issues/3946 c.NotebookApp.port = int(os.getenv('PORT', 8888)) c.NotebookApp.open_browser = False
- sets a password if PASSWORD is set in the environment
- if 'PASSWORD' in os.environ:
- password = os.environ['PASSWORD']
- if password:
- c.NotebookApp.password = passwd(password)
- else:
- c.NotebookApp.password = ''
- c.NotebookApp.token = ''
- del os.environ['PASSWORD']
數(shù)據(jù)持久化
在Kubernetes容器中,如無特殊配置,容器中的數(shù)據(jù)是沒有進(jìn)行持久化,這意味著隨著容器的刪除或者重啟,數(shù)據(jù)就會丟失。對應(yīng)的解決方法比較簡單,只需給需要持久化的目錄,掛載外部存儲即可。在GoblinLab中,會給每個用戶自動創(chuàng)建一個默認(rèn)的外部存儲PVC,并掛載到容器的/root目錄。另外,用戶也可以自定義外部存儲的掛載。
除了自動創(chuàng)建的PVC外,用戶也可以自己創(chuàng)建PVC,并支持將創(chuàng)建的PVC只讀或者讀寫分享給其他人。
另外,在Goblinlab上也可以對PVC里的數(shù)據(jù)進(jìn)行管理。
服務(wù)暴露
Kubernetes集群中創(chuàng)建的服務(wù),在集群外無法直接訪問,GoblinLab使用Nginx Ingress + Gateway訪問,將集群內(nèi)的服務(wù)暴露到外部。
容器化開發(fā)環(huán)境的Service資源編排文件如下(已精簡細(xì)節(jié)):
- apiVersion: v1
- kind: Service
- metadata:
- name: ${name}
- namespace: ${namespace}
- spec:
- clusterIP: None
- ports:
- - name: port-notebook
- port: 8888
- protocol: TCP
- targetPort: 8888
- - name: port-sshd
- port: 22
- protocol: TCP
- targetPort: 22
- - name: port-vscode
- port: 8080
- protocol: TCP
- targetPort: 8080
- - name: port-tensofboard
- port: 6006
- protocol: TCP
- targetPort: 6006
- <#if ports?? && (ports?size > 0)>
- <#list ports as port>
- - name: port-${port}
- port: ${port}
- targetPort: ${port}
- </#list>
- </#if>
- selector:
- statefulset: ${name}
- type: ClusterIP
每當(dāng)用戶啟動一個容器化開發(fā)環(huán)境,GoblinLab將通過接口自動修改Nginx Ingress配置,將服務(wù)暴露出來,以供用戶使用,Ingress轉(zhuǎn)發(fā)配置如下:
- apiVersion: v1
- kind: ConfigMap
- metadata:
- name: tcp-services
- namespace: kube-system
- data:
- "20000": ns/notebook-test:8888
- "20001": ns/notebook-test:8080
- "20002": ns/notebook-test:22
資源管控
為提高資源的利用率,GoblinLab底層Kubernetes中的資源,基本都是以共享的方式使用,并進(jìn)行一定比例的超售。但是當(dāng)多個團(tuán)隊共享一個資源總量固定的集群時,為了確保每個團(tuán)隊公平的共享資源,此時需要對資源進(jìn)行管理和控制。在Kubernetes中,資源配額就是解決此問題的工具。目前GoblinLab需要管控的資源主要為CPU、內(nèi)存、GPU以及存儲等。平臺在考慮各個團(tuán)隊的實際需求后,將資源劃分為多個隊列(Kubernetes中的概念為namespace),提供給各個團(tuán)隊使用。
- apiVersion: v1
- kind: ResourceQuota
- metadata:
- name: skiff-quota
- namespace: test
- spec:
- hard:
- limits.cpu: "2"
- limits.memory: 5Gi
- requests.cpu: "2"
- requests.memory: 5Gi
- requests.nvidia.com/gpu: "1"
- requests.storage: 10Gi
在集群中,最常見的資源為CPU與內(nèi)存,由于可以超售(overcommit),所以存在limits與requests兩個配額限制。除此以外,其他資源為擴(kuò)展類型,由于不允許overcommit,所以只有requests配額限制。參數(shù)說明:
- limits.cpu:Across all pods in a non-terminal state, the sum of CPU limits cannot exceed this value.
- limits.memory: Across all pods in a non-terminal state, the sum of memory limits cannot exceed this value.
- requests.cpu:Across all pods in a non-terminal state, the sum of CPU requests cannot exceed this value.
- requests.memory:Across all pods in a non-terminal state, the sum of memory requests cannot exceed this value.
- http://requests.nvidia.com/gpu:Across all pods in a non-terminal state, the sum of gpu requests cannot exceed this value.
- requests.storage:Across all persistent volume claims, the sum of storage requests cannot exceed this value.
可以進(jìn)行配額控制的資源不僅有CPU、內(nèi)存、存儲、GPU,其他類型參見官方文檔:https://kubernetes.io/docs/con ... otas/
資源隔離
GoblinLab的資源隔離,指的是在同一Kubernetes集群中,資源在調(diào)度層面的相對隔離,其中包含GPU機(jī)器資源的隔離、線上與測試任務(wù)的隔離。
GPU機(jī)器資源的隔離
在Kubernetes集群中,相對于CPU機(jī)器,GPU機(jī)器資源較為珍貴,因此為了提供GPU的利用率,禁止CPU任務(wù)調(diào)度在GPU機(jī)器上。
GPU節(jié)點設(shè)置污點(Taint):禁止一般任務(wù)調(diào)度在GPU節(jié)點
- key: nvidia.com/gpu
- value: true
- effect: NoSchedule
Taint的effect可選配置:
- NoSchedule:Pod不會被調(diào)度到標(biāo)記為taints節(jié)點。
- PreferNoSchedule:NoSchedule的軟策略版本。盡量避免將Pod調(diào)度到存在其不能容忍taint的節(jié)點上。
- NoExecute:該選項意味著一旦Taint生效,如該節(jié)點內(nèi)正在運行的Pod沒有對應(yīng)Tolerate設(shè)置,會直接被逐出。
GPU任務(wù)設(shè)置容忍(Toleration):讓GPU任務(wù)可以調(diào)度在GPU節(jié)點
- <#if (gpu > 0)>
- tolerations:
- - effect: NoSchedule
- key: nvidia.com/gpu
- value: "true"
- </#if>
線上與測試任務(wù)隔離
線上與測試任務(wù)(GolbinLab中線上任務(wù)與測試任務(wù)為業(yè)務(wù)層面的定義,指的是周期調(diào)度任務(wù)和開發(fā)測試任務(wù))使用同一Kubernetes集群,但為了保障線上任務(wù)的資源,會特殊設(shè)置一些機(jī)器節(jié)點為線上任務(wù)的專有資源池。線上任務(wù)執(zhí)行時優(yōu)先調(diào)度在線上節(jié)點上,線上資源池?zé)o資源時,也可調(diào)度于非線上節(jié)點。
線上資源池節(jié)點設(shè)置污點(Taint):禁止一般任務(wù)調(diào)度在線上資源池
- key: node.netease.com/node-pool
- value: online
- effect: NoSchedule
線上任務(wù)添加容忍(Toleration):允許線上任務(wù)調(diào)度于線上資源池,但不是必須調(diào)度于線上資源池中
- tolerations:
- - effect: NoSchedule
- key: node.netease.com/node-pool
- value: "online"
- operator: Equal
線上資源池中機(jī)器節(jié)點設(shè)置標(biāo)簽 + 線上任務(wù)設(shè)置節(jié)點親和性(nodeAffinity):優(yōu)先將線上任務(wù)調(diào)度在線上資源池中,但如果線上資源池中無已資源,此時也可以調(diào)度在其他節(jié)點上
線上資源池中節(jié)點設(shè)置標(biāo)簽:為了方便,標(biāo)簽與污點同名:
- node.netease.com/node-pool: online
線上任務(wù)設(shè)置節(jié)點親和性(nodeAffinity):線上任務(wù)優(yōu)先調(diào)度在線上資源池中
- affinity:
- nodeAffinity:
- preferredDuringSchedulingIgnoredDuringExecution:
- nodeSelectorTerms:
- - matchExpressions:
- - key: node.netease.com/node-pool
- operator: In
- values:
- - online
目前Node affinity有以下幾種策略,官方文檔affinity-and-anti-affinity:
- requiredDuringSchedulingIgnoredDuringExecution表示Pod必須部署到滿足條件的節(jié)點上,如果沒有滿足條件的節(jié)點,就不停重試。其中IgnoreDuringExecution表示Pod部署之后運行的時候,如果節(jié)點標(biāo)簽發(fā)生了變化,不再滿足Pod指定的條件,Pod也會繼續(xù)運行。
- requiredDuringSchedulingRequiredDuringExecution表示Pod必須部署到滿足條件的節(jié)點上,如果沒有滿足條件的節(jié)點,就不停重試。其中RequiredDuringExecution表示Pod部署之后運行的時候,如果節(jié)點標(biāo)簽發(fā)生了變化,不再滿足Pod指定的條件,則重新選擇符合要求的節(jié)點。
- preferredDuringSchedulingIgnoredDuringExecution表示優(yōu)先部署到滿足條件的節(jié)點上,如果沒有滿足條件的節(jié)點,就忽略這些條件,按照正常邏輯部署。
- preferredDuringSchedulingRequiredDuringExecution表示優(yōu)先部署到滿足條件的節(jié)點上,如果沒有滿足條件的節(jié)點,就忽略這些條件,按照正常邏輯部署。其中RequiredDuringExecution表示如果后面節(jié)點標(biāo)簽發(fā)生了變化,滿足了條件,則重新調(diào)度到滿足條件的節(jié)點。
策略生效后效果如下圖所示, 線上任務(wù)優(yōu)先執(zhí)行與線上資源池節(jié)點中,但當(dāng)線上資源池沒有空閑資源后,線上任務(wù)Job5也可以去使用普通節(jié)點的資源。
階段性結(jié)果
截止今日,音樂機(jī)器學(xué)習(xí)平臺(GoblinLab)在容器化方面的嘗試,已開展了一段時間,并且已經(jīng)有了階段性的成果。
集群建設(shè)
經(jīng)過近一段時間的嘗試,目前音樂數(shù)據(jù)平臺的Kubernetes,隨著承載的業(yè)務(wù)越來越多,以及基于Kubernetes的大數(shù)據(jù)計算平臺(Flink等)的落地,后續(xù)將有大量的CPU資源加入, 其穩(wěn)定性將會成為比較大的挑戰(zhàn)。
用戶使用
- 任務(wù)遷移:目前協(xié)助算法同事已完成80%任務(wù)遷移
任務(wù)開發(fā)
- 用戶情況 : 已有60%算法同學(xué)使用開發(fā)實例的容器化環(huán)境;其中用戶來源包含音樂推薦算法、社交視頻推薦算法、搜索算法、音視頻、數(shù)據(jù)應(yīng)用、實時計算算法等團(tuán)隊
- 開發(fā)實例:平臺鼓勵組內(nèi)共用開發(fā)實例,限制了每人最多創(chuàng)建3個開發(fā)實例
- 任務(wù)調(diào)度: 覆蓋云音樂音樂推薦、社交視頻推薦算法、搜索算法、音視頻、數(shù)據(jù)應(yīng)用、實時計算算法等多個團(tuán)隊
容器化的好處
對于算法同事,由物理機(jī)遷移到機(jī)器學(xué)習(xí)平臺提供容器化的環(huán)境,能夠帶來的好處是:
- 更多資源:由于整理資源利用率的提高,將有機(jī)會獲取到比之前單獨使用物理機(jī)更多的資源;另外資源擴(kuò)縮容的周期由之前的以天為單位減少到秒級別即可完成
- 更好體驗:通過打通大數(shù)據(jù)、Git環(huán)境,提供多樣化的使用方式(SSH和在線IDE),由機(jī)器學(xué)習(xí)平臺統(tǒng)一維護(hù)環(huán)境鏡像,避免了每個團(tuán)隊需自己搭建、運維環(huán)境的苦惱
- 更完善的任務(wù)調(diào)度:GoblinLab的調(diào)度,提供了更完善的報警、重試、依賴檢查等功能,并且能夠與之前已有PS、Ironbaby任務(wù)的整合,實現(xiàn)在一個任務(wù)流的統(tǒng)一調(diào)度
- 更好的隔離:環(huán)境隔離是容器化的天然優(yōu)勢。另外資源在調(diào)度層面的隔離,可以更好的保障線上任務(wù)
- 統(tǒng)一的入口:統(tǒng)一了開發(fā)的入口后,可以有更大的操作空間。例如將通用的服務(wù)抽象后由平臺直接提供(依賴檢查、調(diào)度、報警監(jiān)控等等)、數(shù)據(jù)的共享、鏡像的更新以及后面持續(xù)的支持服務(wù)等
后續(xù)規(guī)劃
目前音樂機(jī)器學(xué)習(xí)平臺已能提供完整的容器化開發(fā)基礎(chǔ)能力,為進(jìn)一步提高集群的資源利用率、提升運維效率,后續(xù)計劃從資源調(diào)度策略優(yōu)化(搶占等)、更豐富的資源監(jiān)控等方面入手,進(jìn)一步優(yōu)化。
作者:網(wǎng)易云音樂數(shù)據(jù)智能部數(shù)據(jù)平臺組王軍正