调度器45—wake_affine

基于 Linux-5.10

一、wake_affine 简介

1. 背景

在进程唤醒选核路径中, wake_affine 倾向于将被唤醒进程(wakee)尽可能安排在 waker所在 CPU 上, 这样考虑的原因是: 有唤醒关系的进程是相互关联的, 尽可能地运行在具有 cache 共享的调度域中,
这样可以获得一些 chache-hit 带来的性能提升。

将 wakee 主要唤醒在 waker CPU 上, 必然造成 waker 和 wakee 的资源竞争. 特别是对于 1:N 场景下(一个waker唤醒多个wakee), wake_affine 会导致 waker 进程饥饿。
为了解决这一情况,COMMIT 62470419e993 "sched: Implement smarter wake-affine logic" 实现了一种智能 wake-affine 的优化机制. 用于 wake_flips 的巧妙方式, 识别出 1:N 等复杂唤醒模型, 只有在认
为 wake_affine 能提升性能时(want_affine)才进行 wake_affine.


2. COMMIT 62470419e993 翻译

kernel-5.10$ git show 62470419e993
commit 62470419e993f8d9d93db0effd3af4296ecb79a5
Author: Michael Wang <wangyun@linux.vnet.ibm.com>
Date: Thu Jul 4 12:55:51 2013 +0800

sched: Implement smarter wake-affine logic


sched:实现更智能的 wake-affine 逻辑

wake-affine调度器功能目前总是试图将 wakee(被唤醒者) 拉近 waker(唤醒者)。 从理论上讲,如果唤醒者的 CPU 为被唤醒者缓存热数据,这应该是有益的,并且在极端乒乓的高上下文切换率的情况下也是有益的。

测试表明它可以使 hackbench 受益高达 15%。

但是,该功能有些盲目,某些工作负载(例如 pgbench)会受到影响。 它在算法上也很耗时。

测试表明,它可以将 pgbench 损坏高达 50%——远远超过它在最佳情况下带来的好处。

所以 wake-affine 应该更聪明,它应该意识到什么时候应该停止它在寻找合适的 CPU 来唤醒时的吃力不讨好的努力。

此补丁引入了“wakee_flips”,每次任务翻转(切换)其唤醒目标时都会增加(之前唤醒的是A这次唤醒的不是A,则加1)。

所以一个高的'wakee_flips'值意味着任务有多个 wakee(1:N),数字越大,唤醒频率越高。###############

现在在决定是否拉取时,要注意'wakee_flips'高的 wakee,拉取这样的任务可能对 wakee 有利。 也暗示 waker 之后将面临残酷的竞争,可能非常残酷或非常快因此遭受损失,取决于'wakee_flips'背后的故事。

此外,如果 waker 也有很高的 'wakee_flips',这意味着多个任务依赖它(也有很多任务唤醒 waker),那么 waker 的更高延迟会损坏所有这些任务,因此拉动 wakee 似乎是一个糟糕的交易。

因此,当'waker->wakee_flips / wakee->wakee_flips'变得越来越高时,拉动的代价似乎越来越高。

因此,该补丁有助于 wake-affine 功能在以下情况下停止其拉动工作:

wakee->wakee_flips > factor && waker->wakee_flips > (factor * wakee->wakee_flips)

这里的'factor'是当前CPU的NUMA节点中的CPU数量,因此更大的节点将导致更多的拉动,因为试验变得更加严格。

应用补丁后,pgbench 显示出高达 40% 的改进并且没有退化。

使用 12 个 cpu x86 服务器和 tip 3.10.0-rc7 进行测试。

最后一列中的百分比突出显示了获胜最多的领域,所有其他领域也得到了改善:

       pgbench             base        smart
    
        | db_size | clients |  tps  |   |  tps  |
        +---------+---------+-------+   +-------+
        | 22 MB   |       1 | 10598 |   | 10796 |
        | 22 MB   |       2 | 21257 |   | 21336 |
        | 22 MB   |       4 | 41386 |   | 41622 |
        | 22 MB   |       8 | 51253 |   | 57932 |
        | 22 MB   |      12 | 48570 |   | 54000 |
        | 22 MB   |      16 | 46748 |   | 55982 | +19.75%
        | 22 MB   |      24 | 44346 |   | 55847 | +25.93%
        | 22 MB   |      32 | 43460 |   | 54614 | +25.66%
        | 7484 MB |       1 |  8951 |   |  9193 |
        | 7484 MB |       2 | 19233 |   | 19240 |
        | 7484 MB |       4 | 37239 |   | 37302 |
        | 7484 MB |       8 | 46087 |   | 50018 |
        | 7484 MB |      12 | 42054 |   | 48763 |
        | 7484 MB |      16 | 40765 |   | 51633 | +26.66%
        | 7484 MB |      24 | 37651 |   | 52377 | +39.11%
        | 7484 MB |      32 | 37056 |   | 51108 | +37.92%
        | 15 GB   |       1 |  8845 |   |  9104 |
        | 15 GB   |       2 | 19094 |   | 19162 |
        | 15 GB   |       4 | 36979 |   | 36983 |
        | 15 GB   |       8 | 46087 |   | 49977 |
        | 15 GB   |      12 | 41901 |   | 48591 |
        | 15 GB   |      16 | 40147 |   | 50651 | +26.16%
        | 15 GB   |      24 | 37250 |   | 52365 | +40.58%
        | 15 GB   |      32 | 36470 |   | 50015 | +37.14%

 

二、wake_affine 实现

只在fair.c中的选核路径中使用。

1. 相关函数

static void record_wakee(struct task_struct *p)
{
    /* 对 wakee_flips 每秒衰减50% */
    if (time_after(jiffies, current->wakee_flip_decay_ts + HZ)) {
        current->wakee_flips >>= 1;
        current->wakee_flip_decay_ts = jiffies;
    }

    /* 唤醒的任务变了,则 wakee_flips 加1 */
    if (current->last_wakee != p) {
        current->last_wakee = p;
        current->wakee_flips++;
    }
}

static int wake_wide(struct task_struct *p)
{
    unsigned int master = current->wakee_flips; //waker的wakee翻转次数
    unsigned int slave = p->wakee_flips; //wakee的wakee翻转次数
    int factor = __this_cpu_read(sd_llc_size); //本CPU所在Cluster的CPU个数

    if (master < slave)
        swap(master, slave);

    /* 此次的waker和wakee的唤醒翻转有一个较小则返回0。或用的是否恰当?#### */
    if (slave < factor || master < slave * factor)
        return 0;

    return 1;
}

static int wake_affine(struct sched_domain *sd, struct task_struct *p, int this_cpu, int prev_cpu, int sync)
{
    int target = nr_cpumask_bits;

    /* WA_IDLE 默认为真 */
    if (sched_feat(WA_IDLE))
        /* 根据idle和sync情况看能否选this_cpu或prev_cpu */
        target = wake_affine_idle(this_cpu, prev_cpu, sync);

    /* WA_WEIGHT 默认为真 */
    if (sched_feat(WA_WEIGHT) && target == nr_cpumask_bits)
        /* 若上面没有选到,根据sync和负载看能否选this_cpu */
        target = wake_affine_weight(sd, p, this_cpu, prev_cpu, sync);

    /* 走到这里会有统计 */
    schedstat_inc(p->se.statistics.nr_wakeups_affine_attempts);
    /* 若上面都没选到,则选prev_cpu */
    if (target == nr_cpumask_bits)
        return prev_cpu;

    schedstat_inc(sd->ttwu_move_affine);
    /* 走到这里会有统计 */
    schedstat_inc(p->se.statistics.nr_wakeups_affine);

    return target;
}

 

2. 调用路径:

static int select_task_rq_fair(struct task_struct *p, int prev_cpu, int sd_flag, int wake_flags)
{
    struct sched_domain *tmp, *sd = NULL;
    int cpu = smp_processor_id();
    int new_cpu = prev_cpu;
    int want_affine = 0;
    int sync = (wake_flags & WF_SYNC) && !(current->flags & PF_EXITING);

    ...

    /* 必须是唤醒选核路径 */
    if (sd_flag & SD_BALANCE_WAKE) {
        record_wakee(p);
        ...
        /*
         * 此次的waker和wakee的唤醒翻转有一个较小,且任务p允许运行在此cpu上,
         * 则表示需要 wake_affine。
         * want_affine为真下面才会遍历到DIE层级.
         */
        want_affine = !wake_wide(p) && cpumask_test_cpu(cpu, p->cpus_ptr);
    }
    ...

    /* 遍历当前cpu所在调度域,从MC到DIE遍历 */
    for_each_domain(cpu, tmp) {
        /*
         * SD_WAKE_AFFINE: MC和DIE层级都有此标志。
         * MC层级sd->span包含此cluster的所有CPU,DIE层级的sd->span包含系统中的所有CPU。
         * 需要 wake_affine 且 任务之前运行的CPU和当前CPU属于同一个cluster.
        */
        if (want_affine && (tmp->flags & SD_WAKE_AFFINE) && cpumask_test_cpu(prev_cpu, sched_domain_span(tmp))) {
            if (cpu != prev_cpu)
                new_cpu = wake_affine(tmp, p, cpu, prev_cpu, sync);
            break;
        }

        ...

        if (!want_affine)
            break;
    }

    ...

    return new_cpu;
}

可见原生内核下 wake_affine 对选核影响还挺大的,主要选与当前CPU同Cluster的CPU作为备选CPU。


三、相关DEBUG接口

1. 查看走入wake_affine选核的次数占比

# cat /proc/1265/sched
se.statistics.nr_wakeups                     :                   28
se.statistics.nr_wakeups_sync                :                    1
se.statistics.nr_wakeups_migrate             :                    1
se.statistics.nr_wakeups_local               :                   16
se.statistics.nr_wakeups_remote              :                   12
se.statistics.nr_wakeups_affine              :                    0
se.statistics.nr_wakeups_affine_attempts     :                   10
...
nr_switches                                  :                    2
nr_voluntary_switches                        :                    2
nr_involuntary_switches                      :                    0

 

四、总结

1. wake_affine 主要是想将wakee拉到waker所在的CPU上以便于cache-hit,但是对于1:N的唤醒模型会导致wake饥饿,于是映入 wake_flip 的概念。

2. wake_wide() 用于表示是否需要 wake-affine,使用的却是或,是否合理可能值得再商榷。

 

posted on 2023-02-18 22:57  Hello-World3  阅读(293)  评论(0编辑  收藏  举报

导航