|
作者:腾讯星辰算力团队1.背景1.1.问题源起近年来,随着腾讯内部自研上云项目的不断发展,越来越多的业务开始使用云原生方式托管自己的工作负载,容器平台的规模因此不断增大。以Kubernetes为底座的云原生技术极大推动了云原生领域的发展,已然成为各大容器平台事实上的技术标准。在云原生场景下,为了最大化实现资源共享,单台宿主机往往会运行多个不同用户的计算任务。如果在宿主机内没有进行精细化的资源隔离,在业务负载高峰时间段,多个容器往往会对资源产生激烈的竞争,可能导致程序性能的急剧下降,主要体现为:资源调度时频繁的上下文切换时间频繁的进程切换导致的CPU高速缓存失效因此,在云原生场景下需要针对容器资源分配加以精细化的限制,确保在CPU利用率较高时,各容器之间不会产生激烈竞争从而引起性能下降。1.2.调度场景腾讯星辰算力平台承载了全公司的CPU和GPU算力服务,拥有着海量多类型的计算资源。当前,平台承载的多数重点服务偏离线场景,在业务日益增长的算力需求下,提供源源不断的低成本资源,持续提升可用性、服务质量、调度能力,覆盖更多的业务场景。然而,Kubernetes原生的调度与资源绑定功能已经无法满足复杂的算力场景,亟需对资源进行更加精细化的调度,主要体现为:Kubernetes原生调度器无法感知节点资源拓扑信息导致Pod生产失败kube-scheduler在调度过程中并不感知节点的资源拓扑,当kube-scheduler将Pod调度到某个节点后,kubelet如果发现节点的资源拓扑亲和性要求无法满足时,会拒绝生产该Pod,当通过外部控制环(如deployment)来部署Pod时,则会导致Pod被反复创建-->调度-->生产失败的死循环。基于离线虚拟机的混部方案导致的节点实际可用CPU核心数变化面对运行在线业务的云主机平均利用率较低的现实,为充分利用空闲资源,可将离线虚拟机和在线虚拟机混合部署,解决公司离线计算需求,提升自研上云资源平均利用率。在保证离线不干扰在线业务的情况下,腾讯星辰算力基于自研内核调度器VMF的支持,可以将一台机器上的闲时资源充分利用起来,生产低优先级的离线虚拟机。由于VMF的不公平调度策略,离线虚拟机的实际可用核心数受到在线虚拟机的影响,随着在线业务的繁忙程度不断变化。因此,kubelet通过cadvisor在离线宿主机内部采集到的CPU核心数并不准确,导致了调度信息出现偏差。cvm-architecture资源的高效利用需要更加精细化的调度粒度kube-scheduler的职责是为Pod选择一个合适的Node完成一次调度。然而,想对资源进行更高效的利用,原生调度器的功能还远远不够。在调度时,我们希望调度器能够进行更细粒度的调度,比如能够感知到CPU核心、GPU拓扑、网络拓扑等等,使得资源利用方式更加合理。2.预备知识2.1.cgroups之cpuset子系统cgroups是Linux内核提供的一种可以限制单个进程或者多个进程所使用资源的机制,可以对CPU、内存等资源实现精细化的控制。Linux下的容器技术主要通过cgroups来实现资源控制。在cgroups中,cpuset子系统可以为cgroups中的进程分配独立的CPU和内存节点。通过将CPU核心编号写入cpuset子系统中的cpuset.cpus文件中或将内存NUMA编号写入cpuset.mems文件中,可以限制一个或一组进程只使用特定的CPU或者内存。幸运的是,在容器的资源限制中,我们不需要手动操作cpuset子系统。通过连接容器运行时(CRI)提供的接口,可以直接更新容器的资源限制。// ContainerManager contains methods to manipulate containers managed by a// container runtime. The methods are thread-safe.type ContainerManager interface { // ...... // UpdateContainerResources updates the cgroup resources for the container. UpdateContainerResources(containerID string, resources *runtimeapi.LinuxContainerResources) error // ......}2.2.NUMA架构非统一内存访问架构(英语:Non-uniformmemoryaccess,简称NUMA)是一种为多处理器的电脑设计的内存架构,内存访问时间取决于内存相对于处理器的位置。在NUMA下,处理器访问它自己的本地内存的速度比非本地内存(内存位于另一个处理器,或者是处理器之间共享的内存)快一些。现代多核服务器大多采用NUMA架构来提高硬件的可伸缩性。numa-architecture从图中可以看出,每个NUMANode有独立的CPU核心、L3cache和内存,NUMANode之间相互连接。相同NUMANode上的CPU可以共享L3cache,同时访问本NUMANode上的内存速度更快,跨NUMANode访问内存会更慢。因此,我们应当为CPU密集型应用分配同一个NUMANode的CPU核心,确保程序的局部性能得到充分满足。2.3.Kubernetes调度框架Kubernetes自v1.19开始正式稳定支持调度框架,调度框架是面向Kubernetes调度器的一种插件架构,它为现有的调度器添加了一组新的“插件”API,插件会被编译到调度器之中。这为我们自定义调度器带来了福音。我们可以无需修改kube-scheduler的源代码,通过实现不同的调度插件,将插件代码与kube-scheduler编译为同一个可执行文件中,从而开发出自定义的扩展调度器。这样的灵活性扩展方便我们开发与配置各类调度器插件,同时无需修改kube-scheduler的源代码的方式使得扩展调度器可以快速更改依赖,更新到最新的社区版本。scheduling-framework-extensions调度器的主要扩展点如上图所示。我们扩展的调度器主要关心以下几个步骤:PreFilter和Filter这两个插件用于过滤出不能运行该Pod的节点,如果任何Filter插件将节点标记为不可行,该节点都不会进入候选集合,继续后面的调度流程。PreScore、Score和NormalizeScore这三个插件用于对通过过滤阶段的节点进行排序,调度器将为每个节点调用每个评分插件,最终评分最高的节点将会作为最终调度结果被选中。Reserve和Unreserve这个插件用于在Pod真正被绑定到节点之前,对资源做一些预留工作,保证调度的一致性。如果绑定失败则通过Unreserve来释放预留的资源。Bind这个插件用于将Pod绑定到节点上。默认的绑定插件只是为节点指定spec.nodeName来完成调度,如果我们需要扩展调度器,加上其他的调度结果信息,就需要禁用默认Bind插件,替换为自定义的Bind插件。3.国内外技术研究现状目前Kubernetes社区、Volcano开源社区均有关于拓扑感知相关的解决方案,各方案有部分相同之处,但各自都有局限性,无法满足星辰算力的复杂场景。3.1.Kubernetes社区Kubernetes社区scheduling兴趣小组针对拓扑感知调度也有过一套解决方案,这个方案主要是由RedHat来主导,通过scheduler-plugins和node-feature-discovery配合实现了考虑节点拓扑的调度方法。社区的方法仅仅考虑节点是否能够在满足kubelet配置要求的情况下,完成调度节点筛选和打分,并不会执行绑核,绑核操作仍然交给kubelet来完成,相关提案在这里。具体实现方案如下:节点上的nfd-topology-updater通过gRPC上报节点拓扑到nfd-master中(周期60s)。nfd-master更新节点拓扑与分配情况到CR中(NodeResourceTopology)。扩展kube-scheduler,进行调度时考虑NodeTopology。节点kubelet完成绑核工作。该方案存在较多的问题,不能解决生产实践中的需求:具体核心分配依赖kubelet完成,因此调度器只会考虑资源拓扑信息,并不会选择拓扑,调度器没有资源预留。这导致了节点调度与拓扑调度不在同一个环节,会引起数据不一致问题。由于具体核心分配依赖kubelet完成,所以已调度Pod的拓扑信息需要依靠nfd-worker每隔60s汇报一次,导致拓扑发现过慢因此使得数据不一致问题更加严重,参见这里。没有区分需要拓扑亲和的pod和普通的pod,容易造成开启拓扑功能的节点高优资源浪费。3.2.Volcano社区Volcano是在Kubernetes上运行高性能工作负载的容器批量计算引擎,隶属于CNCF孵化项目。在v1.4.0-Beta版本中进行了增强,发布了有关NUMA感知的特性。与Kubernetes社区scheduling兴趣小组的实现方式类似,真正的绑核并未单独实现,直接采用的是kubelet自带的功能。具体实现方案如下:resource-exporter是部署在每个节点上的DaemonSet,负责节点的拓扑信息采集,并将节点信息写入CR中(Numatopology)。Volcano根据节点的Numatopology,在调度Pod时进行NUMA调度感知。节点kubelet完成绑核工作。该方案存在的问题基本与Kubernetes社区scheduling兴趣小组的实现方式类似,具体核心分配依赖kubelet完成。虽然调度器尽力保持与kubelet一致,但因为无法做资源预留,仍然会出现不一致的问题,在高并发场景下尤其明显。3.3.小结基于国内外研究现状的结果进行分析,开源社区在节点资源绑定方面还是希望交给kubelet,调度器尽量保证与kubelet的一致,可以理解这比较符合社区的方向。因此,目前各个方案的典型实现都不完美,无法满足腾讯星辰算力的要求,在复杂的生产环境中我们需要一套更加稳健、扩展性更好的方案。因此,我们决定从各个方案的架构优点出发,探索出一套更加强大的、贴合腾讯星辰算力实际场景的资源精细化调度增强方案。4.问题分析4.1.离线虚拟机节点实际可用CPU核心数变化从1.2节中我们可以知道,腾讯星辰算力使用了基于离线虚拟机的混部方案,节点实际的CPU可用核心数会收到在线业务的峰值影响从而变化。因此,kubelet通过cadvisor在离线宿主机内部采集到的CPU核心数并不准确,这个数值是一个固定值。因此,针对离线资源我们需要调度器通过其他的方式来获取节点的实际算力。cvm目前调度和绑核都不能到离线虚拟机的实际算力,导致任务绑定到在线干扰比较严重的NUMAnode,资源竞争非常严重使得任务的性能下降。cvm-2幸运的是,我们在物理机上可以采集到离线虚拟机每个NUMAnode上实际可用的CPU资源比例,通过折损公式计算出离线虚拟机的实际算力。接下来就只需要让调度器在调度时能够感知到CPU拓扑以及实际算力,从而进行分配。4.2.精细化调度需要更强的灵活性通过kubelet自带的cpumanager进行绑核总是会对该节点上的所有Pod均生效。只要Pod满足Guaranteed的QoS条件,且CPU请求值为整数,都会进行绑核。然而,有些Pod并不是高负载类型却独占CPU,这种方式的方式很容易造成开启拓扑功能的节点高优资源浪费。同时,对于不同资源类型的节点,其拓扑感知的要求也是不一样的。例如,星辰算力的资源池中也含有较多碎片虚拟机,这部分节点不是混部方式生产出来的,相比而言资源稳定,但是规格很小(如8核CVM,每个NUMANode有4核)。由于大多数任务规格都会超过4核,这类资源就在使用过程中就可以跨NUMANode进行分配,否则很难匹配。因此,拓扑感知调度需要更强的灵活性,适应各种核心分配与拓扑感知场景。4.3.调度方案需要更强的扩展性调度器在抽象拓扑资源时,需要考虑扩展性。对于今后可能会需要调度的扩展资源,如各类异构资源的调度,也能够在这套方案中轻松使用,而不仅仅是cgroups子系统中含有的资源。4.4.避免超线程带来的CPU竞争问题在CPU核心竞争较为激烈时,超线程可能会带来更差的性能。更加理想的分配方式是将一个逻辑核分配给高负载应用,另一个逻辑核分配给不繁忙的应用,或者是将两个峰谷时刻相反的应用分配到同一个物理核心上。同时,我们避免将同一个应用分配到同一个物理核心的两个逻辑核心上,这样很可能造成CPU竞争问题。5.解决方案为了充分解决上述问题,并考虑到未来的扩展性,我们设计了一套精细化调度的方案,命名为cassini。整套解决方案包含三个组件和一个CRD,共同配合完成资源精细化调度的工作。注:cassini这个名称来源于知名的土星探测器卡西尼-惠更斯号,对土星进行了精准无误的探测,借此名来象征更加精准的拓扑发现与调度。5.1.总体架构solution各模块职责如下:cassini-worker:负责收集节点资源拓扑、执行资源绑定工作,作为DaemonSet在每个节点上运行。cassini-master:从外部系统负责收集节点特性(如节点的offline_capacity,节点电力情况),作为controller采用Deployment方式运行。scheduler-plugins:新增调度插件的扩展调度器替换原生调度器,在节点绑定的同时还会分配拓扑调度结果,作为静态Pod在每个master节点上运行。调度整体流程如下:cassini-worker启动,收集节点上的拓扑资源信息。创建或更新NodeResourceTopology(NRT)类型的CR资源,用于记录节点拓扑信息。读取kubelet的cpu_manager_state文件,将已有容器的kubelet绑核结果patch到Podannotations中。cassini-master根据外部系统获取到的信息来更新对应节点的NRT对象。扩展调度器scheduler-plugins执行Pod调度,根据NRT对象感知到节点的拓扑信息,调度Pod时将拓扑调度结构写到Podannotations中。节点kubelet监听并准备启动Pod。节点kubelet调用容器运行时接口启动容器。cassini-worker周期性地访问kubelet的10250端口来List节点上的Pod并从Podannotations中获取调度器的拓扑调度结果。cassini-worker调用容器运行时接口来更改容器的绑核结果。整体可以看出,cassini-worker在节点上收集更详细的资源拓扑信息,cassini-master从外部系统集中获取节点资源的附加信息。scheduler-plugins扩展了原生调度器,以这些附加信息作为决策依据来进行更加精细化的调度,并将结果写到Podannotations中。最终,cassini-worker又承担了执行者的职责,负责落实调度器的资源拓扑调度结果。5.2.API设计NodeResourceTopology(NRT)是用于抽象化描述节点资源拓扑信息的KubernetesCRD,这里主要参考了Kubernetes社区scheduling兴趣小组的设计。每一个Zone用于描述一个抽象的拓扑区域,ZoneType来描述其类型,ResourceInfo来描述Zone内的资源总量。// Zone represents a resource topology zone, e.g. socket, node, die or core.type Zone struct { // Name represents the zone name. // +required Name string `json:"name" protobuf:"bytes,1,opt,name=name"` // Type represents the zone type. // +kubebuilder:validation:Enum=Node;Socket;Core // +required Type ZoneType `json:"type" protobuf:"bytes,2,opt,name=type"` // arent represents the name of parent zone. // +optional arent string `json:"parent,omitempty" protobuf:"bytes,3,opt,name=parent"` // Costs represents the cost between different zones. // +optional Costs CostList `json:"costs,omitempty" protobuf:"bytes,4,rep,name=costs"` // Attributes represents zone attributes if any. // +optional Attributes map[string]string `json:"attributes,omitempty" protobuf:"bytes,5,rep,name=attributes"` // Resources represents the resource info of the zone. // +optional Resources *ResourceInfo `json:"resources,omitempty" protobuf:"bytes,6,rep,name=resources"`}注意到,为了更强的扩展性,每个Zone内加上了一个Attributes来描述Zone上的自定义属性。如4.1节中所示,我们将采集到的离线虚拟机实际算力写入到Attributes字段中,来描述每个NUMANode实际可用算力。5.3.调度器设计plugin扩展调度器在原生调度器基础上扩展了新的插件,大体如下所示:Filter:读取NRT资源,根据每个拓扑内的实际可用算力以及Pod拓扑感知要求来筛选节点并选择拓扑。Score:根据Zone个数打分来打分,Zone越多分越低(跨Zone会带来性能损失)。Reserve:在真正绑定前做资源预留,避免数据不一致,kube-scheduler的cache中也有类似的assume功能。Bind:禁用默认的Bind插件,在Bind时加入Zone的选择结果,附加在annotations中。通过TopologyMatch插件使得调度器在调度时能够考虑节点拓扑信息并进行拓扑分配,并通过Bind插件将结果附加在annotations中。值得一提的是,这里还额外实现了关于节点电力调度等更多维度调度的调度器插件,本篇中不作展开讨论,感兴趣的同学可以私戳我了解。5.4.master设计cassini-master是中控组件,从外部来采集一些节点上无法采集的资源信息。我们从物理上采集到离线虚拟机的实际可用算力,由cassini-master负责将这类结果附加到对应节点的NRT资源中。该组件将统一资源收集的功能进行了剥离,方便更新与扩展。5.5.worker设计cassini-worker是一个较为复杂的组件,作为DaemonSet在每个节点上运行。它的职责分两部分:采集节点上的拓扑资源。执行调度器的拓扑调度结果。5.5.1.资源采集资源拓扑采集主要是通过从/sys/devices下采集系统相关的硬件信息,并创建或更新到NRT资源中。该组件会watch节点kubelet的配置信息并上报,让调度器感知到节点的kubelet的绑核策略、预留资源等信息。由于硬件信息几乎不变化,默认会较长时间采集一次并更新。但watch配置的事件是实时的,一旦kubelet配置后会立刻感知到,方便调度器根据节点的配置进行不同的决策。5.5.2.拓扑调度结果执行拓扑调度结果执行是通过周期性地reconcile来完成制定容器的拓扑分配。获取Pod信息为了防止每个节点的cassini-worker都watchkube-apiserver造成kube-apiserver的压力,cassini-worker改用周期性访问kubelet的10250端口的方式,来List节点上的Pod并从Podannotations中获取调度器的拓扑调度结果。同时,从status中还可以获取到每个容器的ID与状态,为拓扑资源的分配创建了条件。记录kubelet的CPU绑定信息在kubelet开启CPU核心绑定时,扩展调度器将会跳过所有的TopologyMatch插件。此时Podannotations中不会包含拓扑调度结果。在kubelet为Pod完成CPU核心绑定后,会将结果记录在cpu_manager_state文件中,cassini-worker读取该文件,并将kubelet的绑定结果patch到Podannotations中,供后续调度做判断。记录CPU绑定信息根据cpu_manager_state文件,以及从annotations中获取的Pod的拓扑调度结果,生成自己的cassini_cpu_manager_state文件,该文件记录了节点上所有Pod的核心绑定结果。执行拓扑分配根据cassini_cpu_manager_state文件,调用容器运行时接口,完成最终的容器核心绑定工作。6.优化结果根据上述精细化调度方案,我们对一些线上的任务进行了测试。此前,用户反馈任务调度到一些节点后计算性能较差,且由于steal_time升高被频繁驱逐。在替换为拓扑感知调度的解决方案后,由于拓扑感知调度可以细粒度地感知到每个NUMA节点的离线实际算力(offline_capacity),任务会被调度到合适的NUMA节点上,测试任务的训练速度可提升至原来的3倍,与业务在高优CVM的耗时相当,且训练速度较为稳定,资源得到更合理地利用。同时,在使用原生调度器的情况下,调度器无法感知离线虚拟机的实际算力。当任务调度到某个节点上后,该节点steal_time会因此升高,任务无法忍受这样的繁忙节点就会由驱逐器发起Pod的驱逐。在此情况下,如果采用原生调度器,将会引起反复驱逐然后反复调度的情况,导致SLA收到较大影响。经过本文所述的解决方案后,可将CPU抢占的驱逐率大大下降至物理机水平。7.总结与展望本文从实际业务痛点出发,首先简单介绍了腾讯星辰算力的业务场景与精细化调度相关的各类背景知识,然后充分调研国内外研究现状,发现目前已有的各种解决方案都存在局限性。最后通过痛点问题分析后给出了相应的解决方案。经过优化后,资源得到更合理地利用,原有测试任务的训练速度能提升至原来的3倍,CPU抢占的驱逐率大大降低至物理机水平。未来,精细化调度将会覆盖更多的场景,包括在GPU虚拟化技术下对GPU整卡、碎卡的调度,支持高性能网络架构的调度,电力资源的调度,支持超售场景,配合内核调度共同完成等等。
|
|