网易伏羲 梵城 分布式实验室

网易伏羲私有云在资源调度及资源整合方面的实践_Java

网易伏羲成立于2017年,是国内专业从事游戏与泛娱乐AI研究和应用的顶尖机构。伏羲计算效能团队2018年开始基于Kubernetes/OpenStack,面向AI,游戏等业务,打造了伏羲私有云。本文主要介绍下网易伏羲云在资源调度及资源整合方面的实践。

背景与挑战

网易伏羲私有云在资源调度及资源整合方面的实践_Java_02


网易伏羲成立于2017年,是国内专业从事游戏与泛娱乐AI研究和应用的顶尖机构。伏羲云从2018年开始基于Kubernetes构建了一站式的集模型开发,训练,评估和在线预测于一体的协作性AI云平台——丹炉,后来又面向在线应用业务,在丹炉的基础上衍生出了容器云平台。2019年我们在OpenStack的基础上开发了云游戏平台。2020年我们则面向游戏测开场景基于OpenStack搭建了私有云平台——筋斗云。
伏羲云上运行着AI训练/推断,游戏测开环境,数据库中间件,区块链,部分大数据服务,以及2B,2C应用等诸多业务。不同业务对资源的需求与利用呈现多样性,如AI训练,云游戏渲染主要依托于异构GPU计算;强化学习大规模训练主要使用CPU计算,对CPU有近乎无限的需求;在线业务对资源的实际利用率具有时间周期性或普遍利用率很低;很多复杂的分布式任务的部署,则需要强大的调度编排系统的支撑。而由于不同的业务需求及历史原因,伏羲云搭建了多个Kubernetes及Openstack集群,面对一个个资源孤岛,如何提升跨平台/跨集群的资源整合,提高单集群资源利用与弹性,都给我们带来了不小的挑战。
本文将介绍下伏羲云在资源调度及资源整合方面的部分实践,包括伏羲云架构介绍,事件调度引擎,潮汐集群,Kubernetes调度优化等。


伏羲云架构介绍

网易伏羲私有云在资源调度及资源整合方面的实践_Java_02


伏羲云主要依托网易自有机房,在游戏出海的背景下,我们也接入了如AWS等公有云资源,服务于海外业务。伏羲云基于Kubernetes和OpenStack等平台,孵化出了许多面向AI,  游戏等场景的产品。伏羲云基本架构如下:

网易伏羲私有云在资源调度及资源整合方面的实践_Java_04


从上往下看,在用户接入层伏羲云提供了交互,团队协作式的页面功能,也提供了OpenAPI功能,供一些第三方平台如NLP机器人平台,强化学习编程平台,画像平台,云游戏平台等直接调用。平台功能层直接面向用户,主要是一些产品功能的封装。基础支撑层则注重向上对业务共性做统一的抽象,向下则对不同的平台(Kubernetes,OpenStack,公有云),做统一的封装,使得业务不用感知底层细节。在Kubernetes和OpenStack层面,我们则做了一些适应业务需求的扩展开发或定制化改造。


事件调度引擎



伏羲私有云根据业务特点,实现了许多产品应用,如AI开发环境,AI训练任务,模型服务,容器服务,云主机,打包机,云游戏实例,数据库实例等。这些产品,最终会对应为Kubernetes/OpenStack里面一个或多个资源类型,如容器服务基于Kubernetes的Deployment进行封装,云游戏实例基于OpenStack的虚拟机进行封装,数据库实例则基于Kubernetes的自定义CRD进行封装。有些比较复杂的调度任务,则同时需要用多种资源来描述,这些资源甚至是跨Kubernetes和Openstack平台的。例如下图是我们基于Kubernetes开发的强化学习训练框架的一种使用场景:

网易伏羲私有云在资源调度及资源整合方面的实践_Java_05


和常见的深度学习训练不同,强化学习训练需要和环境(Environment)实时交互。网易伏羲开发的强化学习机器人主要服务于游戏。而基于Unity的游戏环境通常需要在Windows上才能运行,如此一个强化学习训练任务则可能要分布在OpenStack,Kubernetes两个不同的平台上。而且客户端和服务端之间,服务端内部的不同组件之间往往又存在着前后依赖关系。为了解决诸如此类的问题,我们自研了一套事件调度引擎,向下实现了对Kubernetes/OpenStack,容器/虚机等不同平台/不同资源类型的统一封装,向上则实现了对不同业务类型的统一抽象,提供统一的Compose YAML模板供业务接入。事件调度引擎基本架构如下:

网易伏羲私有云在资源调度及资源整合方面的实践_Java_06


事件调度引擎目前已经被广泛用于伏羲内部丹炉,筋斗云,云游戏等平台或跨平台,跨集群的资源调度中。事件调度引擎主要分为任务构建,事件系统,调度系统三大块。
任务构建
以DAG来描述任务,业务方只需要编写相应的YAML用来描述任务对资源的需求,以及定义资源之间的依赖关系即可,如下图在一个任务中描述了对Kubernetes Deployment和OpenStack虚拟机的需求及依赖关系。

网易伏羲私有云在资源调度及资源整合方面的实践_Java_07


事件系统
事件调度引擎中描述的资源任务,往往是需要异步执行的,如对Deployment,Pod,虚拟机诸如此类资源的创建。如何获取资源状态则通常有轮询和事件侦听等方式,轮训往往会产生大量空耗的请求及可能漏掉部分中间状态,事件调度引擎主要通过事件侦听的方式来获取Kubernetes,OpenStack等平台的资源变更事件,并用于资源状态追踪,资源生命周期管理,调度编排,监控告警等方面。
Kubernetes本身提供了比较完善的事件watch机制,我们也主要利用Kubernetes已有的事件机制,在Kubernetes原始事件的基础上聚合/封装成我们所需要的业务事件。而OpenStack在API层面没有实现类似Kubernetes的事件机制,我们了则利用OpenStack本身的资源信息都是基于MySQL存储,资源的创建及生命周期的变更最终都会被更新至MySQL中这一特性,采用侦听MySQL Binlog的方式,把Mysql对应资源表中,资源状态字段的变更事件化,并发布至我们的事件系统中。
调度系统
调度系统在任务构建,事件系统的基础上,实现了事件驱动,任务依赖处理及并行,gang schedule,事务回滚,超时管理,GC,优先级队列,公平队列等功能。


潮汐集群

网易伏羲私有云在资源调度及资源整合方面的实践_Java_02


伏羲私有云AI训练任务是基于Kubernetes/容器构建的,而部分业务(如云游戏,筋斗云等)则是基于OpenStack构建的。AI训练任务往往需要占用大量的资源,尤其是强化学习大规模训练需要大量的CPU资源,我们已有的Kubernetes AI训练集群经常会出现资源不足的问题。而云游戏,筋斗云等OpenStack集群的资源利用则具有时间周期性,如白天利用率高,晚上利用率低。根据我们的资源使用特点,我们在云游戏/筋斗云等Openstack集群上,开发了Kubernetes潮汐集群。当凌晨1点且Openstack资源空闲时,创建VM作为Kubernetes worker节点(如同涨潮),并调度一定量的AI训练任务上去,当早上8点,则删除Kubernetes worker VM(如同退潮),以优先保障云游戏/筋斗云等业务运行。潮汐集群设计如下:

网易伏羲私有云在资源调度及资源整合方面的实践_Java_09


AI训练任务往往具备离线性,非实时性等特点,一些对完成时间不是很敏感的AI训练任务,我们会把这些训练任务调度至潮汐集群中。在潮汐集群中,我们以Deployment去封装AI训练任务,当集群加入节点且资源充足时,Deployment的副本会被拉起运行,AI训练过程中会每隔若干step保存checkpoint至Ceph。当潮汐节点被删除后,Deployment的副本也会被删除掉,AI训练任务被挂起。等到下一个潮汐周期,Deployment副本会被再次拉起,并从Ceph上读取相应的checkpoint并继续执行训练。
Kubernetes调度优化

网易伏羲私有云在资源调度及资源整合方面的实践_Java_02


自研均衡调度算法
Kubernetes优选阶段目前主要有LeastRequestedPriority,MostRequestedPriority,BalancedResourceAllocation等三个算法,使得集群中节点的资源占用不至于过度饥饿,也不至于过度饱和,以及减少资源碎片化的问题。Kubernetes默认算法,我们实际实践下来,发现资源碎片化率还是很高,如节点CPU分出去96%,内存只分出去了85%。
为了进一步减少资源碎片化问题,我们分析,节点均衡度在Pod调度前后的变化而排定节点优先级,以及超过设定均衡度最低容忍阈值则进行二次打分的方式, 对Kubernetes BalancedResourceAllocation算法做了改造。
我们分析发现BalancedResourceAllocation每次仅取Pod调度后各项资源使用率最均衡的节点,但没有考虑到Pod调度后对于节点的资源使用均衡度的破坏程度,例如:假设调度节点为Node1,可能存在的情况是,Pod调度前Node1均衡度比Pod调度后Node1均衡度更好,但由于调度后Node1仍为集群中均衡度最好的,故被算法选中为调度节点。但对于Node而言,实际上均衡度变差了,即Pod起到了削弱Node1均衡度的作用。
为此我们对预选通过的每个节点Ni分别计算两个值。
一,计算节点当前的均衡度(基于Kubernetes 1.13)。
网易伏羲私有云在资源调度及资源整合方面的实践_Java_11
注:cpuCapacity为节点CPU可申请总量,curUsedCpu为节点已分配CPU量。memCapacity,curUsedMem类比。
二,假设将待调度Pod调度至节点上,计算节点新的均衡度。
网易伏羲私有云在资源调度及资源整合方面的实践_Java_12
注:reqCpu为Pod申请CPU量。reqMem类比。
若newBalanceScore>=BalanceScore表示均衡度变好,则标记节点Ni为增强节点,反之标记为削弱节点。

网易伏羲私有云在资源调度及资源整合方面的实践_Java_13


其次,对标记后的所有节点进行排序(调度优先级从高到低),排序规则为:1. 增强节点优先级大于削弱节点,故所有增强节点排在所有削弱节点前面;2. 增强节点内部排序为newBalanceScore大的优先级更高排在更前面,削弱节点内部排序同理。

网易伏羲私有云在资源调度及资源整合方面的实践_Java_14


二次打分与压缩调度:
为降低资源比例偏差较大的Pod对算法的影响(影响如下图,调度此Pod,通过均衡度计算,调度后节点N1的均衡度比其他的更好,故调度至节点N1,但也由此产生大量的不可调度资源)

网易伏羲私有云在资源调度及资源整合方面的实践_Java_15


为了解决这个问题,我们通过设置阈值,当触发阈值条件时,在原先对节点进行均衡度打分的基础上,重新对节点进行打分,在尽量保证均衡度较高的情况下,尽可能将Pod调度至剩余资源多的节点上,以此保证对于产生的资源使用不平衡情况,能够通过后续调度新的Pod上去运行来改善。(如图,若不将Pod调度至均衡度计算得分最高的节点N1,而是调度至剩余资源相对更多的节点N2,后续通过调度新的Pod使得节点N2的均衡度提升,资源碎片化程度大大下降)

网易伏羲私有云在资源调度及资源整合方面的实践_Java_16


具体步骤:
首先,划定阈值,一是CPU分配率阈值ξ1和内存分配率阈值ξ2,二是均衡度最低容忍阈值δ。
其次,根据步骤2排序结果,对调度优先级最高的节点进行条件判断,当该节点的CPU分配率大于ξ1或者内存使用率大于ξ2时,若newBalanceScore < δ,则通过以下计算公式重新对节点进行打分;若没有触发阈值条件,则将Pod调度至调度优先级最高的节点上运行。
网易伏羲私有云在资源调度及资源整合方面的实践_Java_17
注:cpuNodeCapacity 为节点CPU可申请总量,usedCpu 为节点CPU已申请量,surplusCpu = cpuNodeCapacity - usedCpu;MemmoryNodeCapacity,surplusMemory类比。
网易伏羲私有云在资源调度及资源整合方面的实践_Java_18
注:λ+ϖ=1,由于二次打分时更倾向于资源剩余更多的节点,故一般设置λ<ϖ。
最后通过二次打分得到的Score对节点进行从大到小排序,从而选择Score最大的节点作为最终调度节点。
整体流程如下:

网易伏羲私有云在资源调度及资源整合方面的实践_Java_19


通过改进后的均衡调度算法。我们实测下来后发现,资源碎片化率有7%~10%的减少。节点CPU, 内存的分配率基本能够达到95%以上。
GPU压缩调度
伏羲私有云Kubernetes GPU节点通常会安装8个GPU,而Kubernetes默认优选调度算法只考虑CPU,内存,volume等资源,未考虑GPU等扩展资源。实际场景中,往往会导致GPU分配碎片化严重,算法研究员经常反馈一次性申请如4卡申请不出来,但集群剩余GPU卡总和却很多。这里我们实现了GPU层面的MostRequestedPriority算法, 以优先填满一个GPU节点,空出更多的GPU节点供大任务调度。
担保区调度
弹性伸缩是Kubernetes的一大亮点功能。伏羲私有云在Kubernetes HPA的基础上实现了根据自定义指标的HPA功能。我们也根据游戏业务的实际场景(如逆水寒的论战江湖玩法只在周三及周日晚上的7:30-9:00开启),实现了CRON HPA功能。HPA存在缩容,扩容两种场景,缩容会释放副本占用的资源,扩容则会增加副本并向集群申请资源,由于自动扩容/定时扩容时没有运维人员的介入,存在扩容时集群资源不足时而Pod长时间Pending的隐患,进而会影响业务的服务质量。如何解决该隐患,我们提出了担保区的概念,就如同JVM的内存担保机制,当新生代无法分配内存时,则把新生代的对象转移至老年代。

网易伏羲私有云在资源调度及资源整合方面的实践_Java_20


我们则把Kubernetes生产集群总体分为在线区,担保区两个区域。在线区只会部署在线应用,当在线区的应用扩容且在线区资源不足时,新增副本才会被调度至担保区。为了避免担保区资源有可能长期闲置浪费的问题,我们会在担保区闲时调度部分AI训练任务上去运行。担保区在我们后续的离在线混部的开发中,也会逐渐演化为混部区。
扩展调度(scheduler-extender)
基于Kubernetes scheduler extender机制,实现了担保区调度的功能。主要实现了以下规则:
  • AI训练任务只会被调度至担保区

  • 在线区的在线应用在扩容的过程中,只有在线区资源不足时,扩容的Pod才会被调度到担保区

  • 首次部署的应用,即使在线区资源不足,也不应该被调度到担保区

  • 训练任务在担保区的申请的资源总量,不允许超过担保区资源总量的60%

  • 训练任务在担保区执行MostRequestedPriority调度策略,以空出更多的空闲节点给在线应用,减少大资源请求在线应用调度失败的概率


抢占式调度
基于Kubernetes已有的抢占式调度功能,设置在线应用为高优先级,AI训练任务为低优先级,当担保区资源不足时,训练任务将被抢占,以优先保障在线应用的调度。
重调度(rescheduler)
由于我们需要在担保区或在线区主动删除Pod,使之重新走一遍自定义调度器并调度至另一区域的需求。我们这里主要采用节点打Label,Pod配置nodeSelector的方式使得Pod调度至指定区域。由于Kubernetes不支持在调度阶段或Pod运行阶段动态修改Pod nodeSelector。由此,我们会在admission-webhook中动态修改Pod YAML对应的nodeSelector,使之调度至期望的区域。rescheduler和admission-webkook则通过Configmap同步,通过记录重调度前后Pod的ownerReferences来标识需要重调度的Pod。
重调度包含两个方向:
一,对在线区Pending的Pod发起重调度,使之调度至担保区。
这种场景主要是rescheduler会实时检测Pending的Pod并判断其是否是扩容出的副本,如果是则发起在线区->担保区的重调度。
二,对担保区长时间滞留的在线应用Pod发起重调度,使之调度至在线区。
由于Kubernetes Deployment扩容副本和缩容副本的顺序是逆序的。该场景主要考虑,在线应用scale up过程中, 调度到担保区的Pod有可能长时间滞留,无法回收的问题,进而导致担保区变成了在“线区”,失去了担保的作用。
例如:某应用执行扩缩容,一种需要重调度的场景如下:

网易伏羲私有云在资源调度及资源整合方面的实践_Java_21


以上场景下,会出现在线区资源充足,但仍有部分副本(Pod1)占用担保区资源的问题。大量这种情况发生的话,会导致担保区资源不足,进而失去担保的作用。针对上述问题,这里设计了重调度的功能,如在担保区运行时长超过一定时间且在线区有满足该Pod调度所需的资源时,则发起担保区->在线区的重调度。

未来计划

网易伏羲私有云在资源调度及资源整合方面的实践_Java_22


当下,我们也在从以下几个方面积极尝试:

  • 离在线混部,  伏羲云目前承载着AI开发、训练,推理,容器服务,部分大数据,游戏测试等业务。实际运行下来,我们发现在线应用的实际资源利用率普遍不高,我们也在着手开发离在线混部的技术,使得在线应用和离线任务能够混部到相同的物理资源上,在保障线上服务SLA的同时,最大化机器资源利用率。

  • AI调度,Kubernetes调度本质上是决定待调度的Pod调度至哪个节点上,可以理解为一类决策问题,我们也在尝试利用强化学习,进化算法等方式去优化Kubernetes的调度。我们也在尝试运用一些机器学习的方法从Prometheus监控及业务数据中发掘一些画像信息以优化调度。

  • Pod迁移,伏羲云用户开发环境基于Kubernetes Deployment实现,Pod重启后用户安装的软件会全部丢失,我们目前在尝试基于checkpoint/restore机制,实现Pod的持久化及恢复。