本文回顾总结近一段时间网易云音乐机器学习平台(GoblinLab)在容器化实践的一些尝试。 过去音乐算法的模型训练任务是在物理机上进行开发、调试以及定时调度。每个算法团队使用属于自己的独立物理机,这种现状会造成一些问题。比如物理机的分布零散,缺乏统一的管理,主要依赖于doc文档的表格记录机器的使用与归属;各业务间机器资源的调配,有时需要机器在不同机房的搬迁,耗时耗力。另外,由于存在多人共用、开发与调度任务共用等情况,会造成环境的相互影响,以及资源的争夺。针对当前的情况,总结问题如下: 资源利用率低:部分机器资源利用率偏低;无法根据各个业务的不同阶段,在全局内快速、动态的实现扩缩容,以达到资源的合理配置,提升资源整体利用率; 环境相互影响:存在多人共用、测试与调度混用同一开发机,未做任何的隔离,造成可能的环境、共享资源的相互影响与争夺; 监控报警缺失:物理机模式,任务监控报警功能缺失,导致任务无法运维,或者效率低。 资源没有全局统一的合理调配,会出现负载不均衡,资源不能最大化的利用。 在快速的扩缩容、环境隔离、资源监控等方面,KubeRnetes及其相关扩展,可以很好的解决问题。现将物理机集中起来,并构建成一个KubeRnetes集群。通过分析算法同事以往的工作方式,机器学习平台(GoblinLab)决定尝试基于KubeRnetes提供在线的开发调试容器环境以及任务的容器化调度两种方案,其分别针对任务开发和任务调度两种场景。 任务开发为最大化的减少算法同事由物理机迁移到容器化环境的学习成本,GoblinLab系统中基本将KubeRnetes的容器当做云主机使用。容器的镜像以各版本Tensoflow镜像为基础(底层是Ubuntu),集成了大数据开发环境(Hadoop、Hive、SpaRk等client),安装了常用的软件。另外,为了方便使用,容器环境提供了JupyteR Lab、SSH登录、code SeRveR(VScode)三种使用方式。 在GoblinLab中新建容器化开发环境比较简单,只需选择镜像,填写所需的资源,以及需要挂载的外部存储即可(任务开发的环境下文简称开发实例)。
算法可以选择以上任意一种方式进行任务的开发,或者调试。由于提供了code SeRveR(VScode),所以可以获得更好的体验。 任务开发所用的容器化环境,在底层KubeRnetes上是通过StatefulSet类型实现,对应资源编排文件如下(已精简细节): 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 USePRivateReposiTory == “tRue”> imagePullSecRets: – naMe: Registrykey-Myhub voluMes: – naMe: localtiMe hostPath: path: /etc/localtiMe <if MountPVCs?? && && (MountPVCs?size > 0)> <list MountPVCs?keys as key> – naMe: “${key}” peRsistentVoluMeClAIM: clAIMNaMe: “${key}” 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 <if wRITeMountPVCs?? && && (wRITeMountPVCs?size > 0)> <list wRITeMountPVCs?keYs as key> – naMe: “${key}” MountPath: “${wRITeMountPVCs[key]}” 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} liMITs: CPu: ${CPu} MeMoRy: ${MeMoRy}Gi <if (gpu > 0)> NVIDIA.coM/gpu: ${gpu} 目前GolBInLab已提供基于Tensoflow各版本的CPU与GPU通用镜像11个,以及多个定制化镜像。 任务调度 算法同事在使用容器化环境之前,任务的开发调度都是在GPU物理机器上完成,调度一般都是通过定时器或cRontab命令调度任务,任务无失败、超时等报警,以及也没有重试等机制,基本无相关的任务运维工具。
在介绍容器中开发的任务如何上线调度之前,先简要介绍一下GoblinLab的系统架构。
上图为GoblinLab简化的系统架构,其中主要分为四层,由上到下分别为: application-应用层:提供直接面向用户的机器学习开发平台(GoblinLab) Middle-中间层: 中间层,主要是接入了统一的调度、报警、以及配置等服务 WizaRd-执行服务: 其提供统一的执行服务,提供包含KubeRnetes、SpaRk、JaR等各类任务的提交执行。插件式,支持快速扩展 InfRastRUCtuRe-基础设施: 底层的基础设施,主要包含KubeRnetes集群、SpaRk集群以及普通服务器等
上图为GoblinLab简化的系统架构,包括四层,从上到下依次为应用层、中间层、执行服务和基础设施。
GolBInLab为了保障调度任务的稳定性,将任务的开发与调度拆分,改变之前算法直接在物理机上开发完任务后,通过定时器或者cRontab调度任务的方式。如上图所示,在开发完成后,任务的调度是通过任务流中的容器化任务调度组件实现,用户需填组件的相关参数(代码所在PVC及路径,配置镜像等),再通过任务流的调度功能实现任务调度。与任务开发不同,每个调度任务执行在独立的容器中,保证任务间相互隔离,同时通过后续介绍的资源隔离方案,可以优先保障线上调度任务所需资源。
任务调度执行时在KubeRnetes上资源编排文件(已精简细节): 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} &aMp;&aMp; ${execcommand} /Root/${entRyPath} ${RunARgs}”] voluMeMounts: – MountPath: “/Root” naMe: “Root-diR” ResouRces: requests.CPu: “${CPu}” requests.MeMoRy: “${MeMoRy}Gi” requests.NVIDIA.coM/gpu: “${gpu}” requests.sTorage: 10Gi
权限控制 容器化开发环境配置启动后,用户可以通过SSH登录、codeSeRveR或JupyteRLab等其中一种方式使用。为了避免容器化开发环境被其他人使用,GoblinLab给每种方式都设置了统一的密钥,而密钥在每次启动时随机生成。
随机生成密码 设置账号密码(SSH登录密码) 设置code SeRveR密码 (VScode) 设置JupyteR Lab密码
数据持久化 在KubeRnetes容器中,如无特殊配置,容器中的数据是没有进行持久化,这意味着随着容器的删除或者重启,数据就会丢失。对应的解决方法比较简单,只需给需要持久化的目录,挂载外部存储即可。在GoblinLab中,会给每个用户自动创建一个默认的外部存储PVC,并挂载到容器的/Root目录。另外,用户也可以自定义外部存储的挂载。
除了自动创建的PVC外,用户也可以自己创建PVC,并支持将创建的PVC只读或者读写分享给其他人。另外,在Goblinlab上也可以对PVC里的数据进行管理。
服务暴露 KubeRnetes集群中创建的服务,在集群外无法直接访问,GoblinLab使用Nginx IngReSS + Gateway访问,将集群内的服务暴露到外部。
容器化开发环境的SeRvice资源编排文件如下(已精简细节): 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
每当用户启动一个容器化开发环境,GoblinLab将通过接口自动修改Nginx IngReSS配置,将服务暴露出来,以供用户使用,IngReSS转发配置如下: 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中的资源基本都是以共享的方式使用,并进行一定比例的超售。但是当多个团队共享一个资源总量固定的集群时,为了确保每个团队公平的共享资源,此时需要对资源进行管理和控制。在KubeRnetes中,资源配额就是解决此问题的工具。目前GoblinLab需要管控的资源主要为CPU、内存、GPU以及存储等。平台在考虑各个团队的实际需求后,将资源划分为多个队列(KubeRnetes中的概念为naMespace),提供给各个团队使用。
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与内存,由于可以超售(OVeRcoMMIT),所以存在liMITs与requests两个配额限制。除此以外,其他资源为扩展类型,由于不允许OVeRcoMMIT,所以只有requests配额限制。参数说明: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.
可以进行配额控制的资源不仅有CPU、内存、存储、GPU,其他类型参见官方文档。资源隔离 GoblinLab的资源隔离指的是在同一KubeRnetes集群中,资源在调度层面的相对隔离,其中包含GPU机器资源的隔离、线上与测试任务的隔离。
GPU机器资源的隔离在KubeRnetes集群中,相对于CPU机器,GPU机器资源较为珍贵,因此为了提供GPU的利用率,禁止CPU任务调度在GPU节点。
GPU节点设置污点(TAInt):禁止一般任务调度在GPU节点
key: NVIDIA.coM/gpu value: tRue Effect: NoSchedule
TAInt的Effect可选配置:NoSchedule:Pod不会被调度到标记为tAInts节点。 PRefeRNoSchedule: