CFS任务的负载均衡

负载均衡的系列文章共分为三篇,第一篇为框架篇,描述负载均衡的相关原理、场景和框架。本篇作为该系列文章第二篇,主要通过对任务放置场景(task placement)的均衡分布进行分析,以便加深读者对内核调度器实现任务均衡分布的理解。

本文基于linux-5.4.24分析,由于涉及较多代码的讲解,建议结合源码阅读。另外,浏览本文前,建议先阅读公众号的负载均衡系列文章第一篇:CFS任务的负载均衡(框架篇)。当然,部分已经提及的基本概念,在本文中也会进行简单回顾。

一、任务放置场景

1. 什么是任务放置(task placement)

linux内核为每个CPU都配置一个cpu runqueue,用以维护当前CPU需要运行的所有线程,调度器会按一定的规则从runqueue中获取某个线程来执行。如果一个线程正挂在某个CPU的runqueue上,此时它处于就绪状态,尚未得到cpu资源,调度器会适时地通过负载均衡(load balance)来调整任务的分布;当它从runqueue中取出并开始执行时,便处于运行状态,若该状态下的任务负载不是当前CPU所能承受的,那么调度器会将其标记为misfit task,周期性地触发主动迁移(active upmigration),将misfit task布置到更高算力的CPU。

上面提到的场景,都是线程已经被分配到某个具体的CPU并且具备有效的负载。如果一个任务线程还未被放置到任何一个CPU上,即处于阻塞状态,又或者它是刚创建、刚开始执行的,此时调度器又是何如做均衡分布的呢?这便是今天我们要花点篇幅来介绍的任务放置场景。

内核中,task placement场景发生在以下三种情况:

(1)进程通过fork创建子进程;

(2)进程通过sched_exec开始执行;

(3)阻塞的进程被唤醒。

2. 调度域(sched domain)及其标志位(sd flag)

如果你正在使用智能手机阅读本文,那你或许知道,目前的手机设备往往具备架构不同的8个CPU core。我们仍然以4小核+4大核的处理器结构为例进行说明。4个小核(cpu0-3)组成一个little cluster,另外4个大核(cpu4-7)组成big cluster,每个cluster的CPU架构相同,它们之间使用同一个调频策略,并且频率调节保持一致。大核相对小核而言,具备更高的算力,但也会带来更多的能量损耗。

对于多处理器均衡(multiprocessor balancing)而言,sched domain是极为重要的概念。内核中以结构体struct sched_domain对其进行定义,将CPU core从下往上按层级划分,对系统所有CPU core进行管理,本系列文章第一篇已进行过较为详细的描述。little cluster和big cluster各自组成底层的MC domain,包含各自cluster的4个CPU core,顶层的DIE domian则覆盖系统中所有的CPU core。

内核调度器依赖sched domain进行均衡,为了方便地对各种均衡状态进行识别,内核定义了一组sched domain flag,用来标识当前sched domain具备的均衡属性。表中,我们可以看到task placement场景常见的三种情况对应的flag。

在构建CPU拓扑结构时,会为各个sched domain配置初始的标识位,如果是异构系统,会设置SD_BALANCE_WAKE:

3. task placement均衡代码框架

linux内核的调度框架是高度抽象、模块化的,所有的线程都拥有各自所属的调度类(sched class),比如大家所熟知的实时线程属于rt_sched_class,CFS线程属于fair_sched_class,不同的调度类采用不同的调度策略。上面提到的task placement的三种场景,最终的函数入口都是core.c中定义的select_task_rq()方法,之后会跳转至调度类自己的具体实现。本文以CFS调度类为分析对象,因为该调度类的线程在整个系统中占据较大的比重。有兴趣的朋友可以了解下其它调度类的select_task_rq()实现。

4. select_task_rq_fair()方法

CFS调度类的线程进行task placement时,会通过core.c的select_task_rq()方法跳转至select_task_rq_fair(),该方法声明如下:

static int select_task_rq_fair(struct task_struct *p, int prev_cpu, int sd_flag, int wake_flags)

sd_flag参数:传入sched domain标识位,目前一共有三种:SD_BALANCE_WAKE、SD_BALANCE_FORK、SD_BALANCE_EXEC,分别对应task placement的三种情形。调度器只会在设置有相应标识位的sched domain中进行CPU的选择。

wake_flags参数:特地为SD_BALANCE_WAKE提供的唤醒标识位,一共有三种类型:

select_task_rq_fair()内仅对WF_SYNC进行处理,若传入该标识位,说明唤醒线程waker在被唤醒线程wakee唤醒后,将进入阻塞状态,调度器会倾向于将wakee放置到waker所在的CPU。这种场景使用相当频繁,比如用户空间两个进程进行非异步binder通信,Server端唤醒一个binder线程处理事务时,调用的接口如下:

select_task_rq_fair()中涉及到三个重要的选核函数:

    find_energy_efficient_cpu()

    find_idlest_cpu()

    select_idle_sibling()

它们分别代表任务放置过程中的三条路径。task placement的各个场景,根据不同条件,最终都会进入其中某一条路径,得到任务放置CPU并结束此次的task placement过程。现在让我们来理一理这三条路径的常见进入条件以及基本的CPU选择考量:

(1)find_energy_efficient_cpu(),即EAS选核路径。当传入参数sd_flag为SD_BALANCE_WAKE,并且系统配置key值sched_energy_present(即考虑性能和功耗的均衡),调度器就会进入EAS选核路径进行CPU的查找。这里涉及到内核中Energy Aware Scheduling(EAS)机制,我们稍后将在第三节中详细描述。总之,EAS路径在保证任务能正常运行的前提下,为任务选取使系统整体能耗最小的CPU。通常情况下,EAS总是能如愿找到符合要求的CPU,但如果当前平台不是异构系统,或者系统中存在超载(Over-utilization)的CPU,EAS就直接返回-1,不能在这次调度中大展拳脚。

当EAS不能在这次调度中发挥作用时,分支的走向取决于该任务是否为wake affine类型的任务,这里让我们先来简单了解下该类型的任务。

用户场景有时会出现一个主任务(waker)唤醒多个子任务(wakee)的情况,如果我们将其作为wake affine类型处理,将wakee打包在临近的CPU上(如唤醒CPU、上次执行的CPU、共享cache的CPU),即可以提高cache命中率,改善性能,又能避免唤醒其它可能正处于idle状态的CPU,节省功耗。看起来这样的处理似乎非常完美,可惜的是,往往有些wakee对调度延迟非常敏感,如果将它们打包在一块,CPU上的任务就变得“拥挤”,调度延迟就会急剧上升,这样的场景下,所谓的cache命中率、功耗,一切的诱惑都变得索然无味。

对于wake affine类型的判断,内核主要通过wake_wide()和wake_cap()的实现,从wakee的数量以及临近CPU算力是否满足任务需求这两个维度进行考量。

(2)find_idlest_cpu(),即慢速路径。有两种常见的情况会进入慢速路径:传入参数sd_flag为SD_BALANCE_WAKE,且EAS没有使能或者返回-1时,如果该任务不是wake affine类型,就会进入慢速路径;传入参数sd_flag为SD_BALANCE_FORK、SD_BALANCE_EXEC时,由于此时的任务负载是不可信任的,无法预测其对系统能耗的影响,也会进入慢速路径。慢速路径使用find_idlest_cpu()方法找到系统中最空闲的CPU,作为放置任务的CPU并返回。基本的搜索流程是:

首先确定放置的target domain(从waker的base domain向上,找到最底层配置相应sd_flag的domain),然后从target domain中找到负载最小的调度组,进而在调度组中找到负载最小的CPU。

这种选核方式对于刚创建的任务来说,算是一种相对稳妥的做法,开发者也指出,或许可以将新创建的任务放置到特殊类型的CPU上,或者通过它的父进程来推断它的负载走向,但这些启发式的方法也有可能在一些使用场景下造成其他问题。

(3)select_idle_sibling(),即快速路径。传入参数sd_flag为SD_BALANCE_WAKE,但EAS又无法发挥作用时,若该任务为wake affine类型任务,调度器就会进入快速路径来选取放置的CPU,该路径在CPU的选择上,主要考虑共享cache且idle的CPU。在满足条件的情况下,优先选择任务上一次运行的CPU(prev cpu),hot cache的CPU是wake affine类型任务所青睐的。其次是唤醒任务的CPU(wake cpu),即waker所在的CPU。当该次唤醒为sync唤醒时(传入参数wake_flags为WF_SYNC),对wake cpu的idle状态判定将会放宽,比如waker为wake cpu唯一的任务,由于sync唤醒下的waker很快就进入阻塞状态,也可当做idle处理。

如果prev cpu或者wake cpu无法满足条件,那么调度器会尝试从它们的LLC domain中去搜索idle的CPU。

二、Energy Aware Scheduling(EAS)

系统中的Energy Aware Scheduling(EAS)机制被使能时,调度器就会在CFS任务由阻塞状态唤醒的时候,使用find_energy_efficient_cpu()为任务选择合适的放置CPU。

1. 什么是Energy Model(EM)

在了解什么是EAS之前,我们先学习下EM。EM的设计使用比较简单,因为我们要避免在task placement时,由于算法过于复杂导致调度延迟变高。理解EM的一个重点是理解性能域(performance domain)。与sched domain相同,内核也有相应的结构体struct perf_domain来定义性能域。相同微架构的CPU会归属到同一个perf domain,4大核+4小核的CPU拓扑信息如下:

little cluster的4个CPU core组成一个perf domain,big cluster则组成另外一个。相同perf domian内的所有CPU core一起进行调频,保持着相同的频率。CPU使用的频点是分级的,各级别的频点与capacity值、power值是一一映射的关系,例如:小核的4个cpu,最大capacity都是512,与之对应的最高频点为1G Hz,那么500M Hz的频点对应的capacity就是256。为了将这些信息有效的组织起来,内核又为EM增加两个新的结构体,用于存储这些信息,它们都能够从perf domain中获取。

2. 什么是EAS

异构CPU拓扑架构(比如Arm big.LITTLE架构),存在高性能的cluster和低功耗的cluster,它们的算力(capacity)之间存在差异,这让调度器在唤醒场景下进行task placement变得更加复杂,我们希望在不影响整体系统吞吐量的同时,尽可能地节省能量,因此,EAS应运而生。它的设计令调度器在做CPU选择时,增加能量评估的维度,它的运作依赖于Energy Model(EM)。

EAS对能量(energy)和功率(power)的定义与传统意义并无差别,energy是类似电源设备上的电池这样的资源,单位是焦耳,power则是每秒的能量损耗值,单位是瓦特。

EAS在非异构系统下,或者系统中存在超载CPU时不会使能,调度器对于CPU超载的判定是比较严格的,当root domain中存在CPU负载达到该CPU算力的80%以上时,就认为是超载。

3. EM是如何估算energy的

由于EM将系统中所有CPU的各级capacity、frequence、power以便捷高效的方式组织起来,计算energy的工作就变得很简单了。内核中某个perf domian的energy可以通过em_pd_energy()获得,它实际上是通过假定将任务放置到某个CPU上,引起perf domain各个CPU负载变化,来估算整体energy数值。令人值得庆幸的是,该方法的实现代码中,有一半以上都是注释语句。

static inline unsigned long em_pd_energy(struct em_perf_domain *pd, unsigned long max_util, unsigned long sum_util)

max_util参数:perf domain各个CPU中的最高负载。

sum_util参数:perf domain中所有CPU的总负载。

前面提到过,同个perf domian下的所有CPU使用相同的频点,因此,cluster选择哪个频点,取决于拥有最大负载的CPU。EM首先会获取当前perf domain的最高频点和最大算力,并将max_util映射到对应的频率上,找到超过该频率的最低频点及相应的算力cs_capacity,毕竟我们要确保任务能够正常执行。

尽管我们知道EA可以很轻易的获得该频点的功率cs_power值,并且无论是cs_capacity还是cs_power,domain下所有CPU都是相同的,但是要获得各个CPU的energy,我们还需要一个跟各个CPU运行时间相关的信息。由于CPU不是超载的(超载情况下EAS不会使能),它不会一直运行任务,我们需要忽略掉idle的部分,这一点可以通过CPU负载与算力的比值进行估算。这是由于,负载体现了CPU执行任务的窗口时间,当整个窗口时间都在运行任务时,CPU的负载就达到其算力上限。

好了,现在需要的信息都齐全,只要将所有CPU的energy累加起来,就能得到整个perf domain的估计能量值。

4. EAS task placement

EAS在任务唤醒时,通过函数find_energy_efficient_cpu()为任务选择合适的放置CPU,它的实现逻辑大致如下:

(1)通过em_pd_energy()计算取得各个perf domian未放置任务的基础能量值;

(2)遍历各个perf domain,找到该domain下拥有最大空余算力的CPU以及prev cpu,作为备选放置CPU;

(3)通过em_pd_energy()计算取得将任务放置到备选CPU引起的perf domain的energy变化值;

(4)通过比较得到令energy变化最小的备选CPU,即将任务放置到该CPU上,能得到最小的domain energy,如果相对于将任务放置到prev cpu,此次的选择能节省6%以上的能量,则该CPU为目标CPU。

选择perf domain中拥有最大空余算力的CPU作为备选CPU,是因为这样可以避免某个CPU负载特别高,导致整个cluster的频点往上提。并且顾及到hot cache的prev cpu有利于提高任务运行效率,EAS对于prev cpu还是难以割舍的,除非节能可以达到6%以上。

另外,从上面的逻辑中也可以看出为何超载情况下EAS是不使能的。我们假定little cluster中cpu3存在超载的情况,那么无论你将任务放置到哪个CPU上,little cluster总是维持最高频点,对于同个perf domain下拥有最大空余算力的CPU来说,这样预估的energy是不公平的,与EAS的设计相违背,EAS希望能通过放置任务改变cluster的频点来降低功耗。

三、总结

本文作为负载均衡系列文章的第二篇,主要对CFS任务的task placement做场景分析,描述调度器在此过程中的选择实现和考量,由于篇幅和精力有限,很多具体的细节还没能呈现清晰,特别是对快速路径和慢速路径这一块的描述,希望有兴趣的朋友可以自行阅读源码实现,共同学习交流。

我们可以看到,目前task placement过程中的一些启发式算法还存在缺陷,也能看到开发者对此的不断思考和创新,随着内核版本的不断更新迭代,未来的调度算法一定会出现更多有意思的特性。

参考资料

[1] linux-5.4.24 source code

[2] linux-5.4.24/Documentation/power/ energy-model.rst

[3] linux-5.4.24/Documentation/scheduler/ sched-energy.rst

[4] https://lwn.net/Articles/728942/

扫码关注
“内核工匠”微信公众号
Linux 内核黑科技 | 技术文章 | 精选教程
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页