kernel在多核机器上的负载均衡机制

32
Linux Kernel 在在在在在在在在在在 在在在 在在 在在在 () 在在在在 2.6.18.8 在在 1

Upload: robin-dong

Post on 02-Jun-2015

2.827 views

Category:

Technology


1 download

TRANSCRIPT

Linux Kernel 在多核机器上的负载均衡机制

董昊 (三百)源代码以 2.6.18.8 为准

1

现在的服务器 CPU 架构

2

每个 CPU 含多个核每个核有自己的一级 cache同一 CPU 内的多个核共享同一个二级 cache(下图是最常见的体系结构,但不代表所有的 CPU )

Kernel 的调度数据结构

• 每个 CPU (核)上有一个运行队列 (struct rq)• 每个运行队列上有两个优先级队列 (struct

prio_array)– active– expire (已经用完时间片的进程)

• 每个优先级队列里有分级队列,分别挂载不同优先级的进程

3

struct rq

4

prio_array

5

内核从 active 队列里按优先级挑进程出来运行,如果进程用完了时间片,就将其放入 expire 队列

当 active 队列的进程都用完了时间片,则把指向active 和 expire 的指针对换,开始新一轮的优先级调度

任务负载均衡

• 问题:把任务(进程)分配到多个 core 上去,保证各个 core 的负载均衡,且要考虑有些 core 之间共享 cache 而有些 core 之间没有关系。

• 所有负载均衡需要解决的问题:– 怎样衡量负载?– 什么时间检查负载是否均衡?– 怎么调整为均衡?

6

先想些笨办法

• 怎样衡量负载?– 在运行队列上的 running 的进程数

• 什么时候检查任务是否均衡?– 每 1 秒检查一次

• 怎么调整为均衡?– 让各个核上的任务数相等——把任务数最多的核上的

任务挪一个(或几个)到任务数最少的核上

7

开始细化这些笨办法

• 怎样衡量负载?• 用进程数衡量 CPU 负载的缺点——进程的优先级

不一样• 会造成有的核跑着 10 个低优先级的任务,而另

一个核上跑着 10 个高优先级的任务,任务响应偏慢

• 可以用核上正在跑的进程的优先级来衡量负载。高优先级的我们当他负载高,低优先级的我们当他负载低

8

Kernel 的做法

• 进程本身有“静态优先级” (current->static_prio),– 值范围是 100~139– 进程的 static_prio 在运行时不变– 用 nice 系统调用可以更改

• 进程的负载计算方法– 如果 static_prio 小于 120 (高优先级)

• p->load_weight = (140-p->static_prio) * 128 / 5– 如果 static_prio 等于或大于 120 (低优先级)

• p->load_weight = (140-p->static_prio) * 128 / 20

各种静态优先级进程的负载

静态优先级 负载100 1024

110 768

120 128

130 64

10

可以看出,低优先级进程的负载衰减的很厉害

Kernel 的做法

• 核( CPU )的负载:正在其上运行的进程的load_weight 相加

• rq->raw_weighted_load

• 问:为什么用“静态优先级”来衡量进程的负载,而不是动态优先级?– 动态优先级包含了进城运行时的特性,比如

sleep_avg (平均睡眠时间)– 而衡量负载主要考虑:进程一旦开始使用 CPU 会造成

什么影响。进程之前睡得多少并不重要。11

继续细化这些笨办法

• 什么时候检查任务是否均衡?• 每秒调整一次负载慢吗?• 可以在 CPU 负载变化的时候调整• 进程睡眠或醒来时是负载变化的时候

12

Kernel 的做法

• 进程睡去……

13

3294 asmlinkage void __sched schedule(void) 3295 {… 3354 switch_count = &prev->nivcsw; 3355 if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) { 3356 switch_count = &prev->nvcsw; 3357 if (unlikely((prev->state & TASK_INTERRUPTIBLE) && 3358 unlikely(signal_pending(prev)))) 3359 prev->state = TASK_RUNNING; 3360 else { 3361 if (prev->state == TASK_UNINTERRUPTIBLE) 3362 rq->nr_uninterruptible++; 3363 deactivate_task(prev, rq); // 将进程拿出 rq ,进程负载也会被减掉 3364 } 3365 }…

schedule()

14

… schedule() 函数继续

3367 cpu = smp_processor_id(); 3368 if (unlikely(!rq->nr_running)) { 3369 idle_balance(cpu, rq); 3370 if (!rq->nr_running) { 3371 next = rq->idle; 3372 rq->expired_timestamp = 0; 3373 wake_sleeping_dependent(cpu); 3374 goto switch_tasks; 3375 } 3376 }…

这种情况仅发生在 rq 上没有其它进程的时候

idle_balance()

15

idle_balance() 函数 2733 static void idle_balance(int this_cpu, struct rq *this_rq) 2734 { 2735 struct sched_domain *sd; 2736 2737 for_each_domain(this_cpu, sd) { 2738 if (sd->flags & SD_BALANCE_NEWIDLE) { 2739 /* If we've pulled tasks over stop searching: */ 2740 if (load_balance_newidle(this_cpu, this_rq, sd)) 2741 break; 2742 } 2743 } 2744 }

sched_domain 与 sched_group

16

sched_domain 和 sched_group 的知识可参考(本图来源):http://www.ibm.com/developerworks/cn/linux/l-cn-schldom/index.html

8 核机器

17

从子 sched_domain 到父 shced_domain 287 #define for_each_domain(cpu, __sd) \ 288 for (__sd = rcu_dereference(cpu_rq(cpu)->sd); __sd; __sd = __sd->parent)

8 核机器(父调度域)

18

通用的调度特性

19

119 #define SD_CPU_INIT (struct sched_domain) { \ 120 .span = CPU_MASK_NONE, \ 121 .parent = NULL, \ 122 .groups = NULL, \ 123 .min_interval = 1, \ 124 .max_interval = 4, \ 125 .busy_factor = 64, \ 126 .imbalance_pct = 125, \ 127 .cache_nice_tries = 1, \ 128 .per_cpu_gain = 100, \ 129 .busy_idx = 2, \ 130 .idle_idx = 1, \ 131 .newidle_idx = 2, \ 132 .wake_idx = 1, \ 133 .forkexec_idx = 1, \ 134 .flags = SD_LOAD_BALANCE \ 135 | SD_BALANCE_NEWIDLE \ 136 | SD_BALANCE_EXEC \ 137 | SD_WAKE_AFFINE \ 138 | BALANCE_FOR_POWER, \ 139 .last_balance = jiffies, \ 140 .balance_interval = 1, \ 141 .nr_balance_failed = 0, \ 142 }

在我们常用的 Intel 架构上,可以认为:SD_NODE_INIT 用来初始化每个 CPUSD_CPU_INIT 用来初始化CPU 上的每个核正因为同一 CPU 上的核共享L2 cache ,所以SD_CPU_INIT 有SD_WAKE_AFFINE (亲和)选项

不同体系结构的不同调度特性

20

include/asm-x86_64/topolopy.h

31 #define SD_NODE_INIT (struct sched_domain) { \ 32 .span = CPU_MASK_NONE, \ 33 .parent = NULL, \ 34 .groups = NULL, \ 35 .min_interval = 8, \ 36 .max_interval = 32, \ 37 .busy_factor = 32, \ 38 .imbalance_pct = 125, \ 39 .cache_nice_tries = 2, \ 40 .busy_idx = 3, \ 41 .idle_idx = 2, \ 42 .newidle_idx = 0, \ 43 .wake_idx = 1, \ 44 .forkexec_idx = 1, \ 45 .per_cpu_gain = 100, \ 46 .flags = SD_LOAD_BALANCE \ 47 | SD_BALANCE_FORK \ 48 | SD_BALANCE_EXEC \ 49 | SD_WAKE_BALANCE, \ 50 .last_balance = jiffies, \ 51 .balance_interval = 1, \ 52 .nr_balance_failed = 0, \ 53 }

include/asm-powerpc/topolopy.h 43 #define SD_NODE_INIT (struct sched_domain) { \ 44 .span = CPU_MASK_NONE, \ 45 .parent = NULL, \ 46 .groups = NULL, \ 47 .min_interval = 8, \ 48 .max_interval = 32, \ 49 .busy_factor = 32, \ 50 .imbalance_pct = 125, \ 51 .cache_nice_tries = 1, \ 52 .per_cpu_gain = 100, \ 53 .busy_idx = 3, \ 54 .idle_idx = 1, \ 55 .newidle_idx = 2, \ 56 .wake_idx = 1, \ 57 .flags = SD_LOAD_BALANCE \ 58 | SD_BALANCE_EXEC \ 59 | SD_BALANCE_NEWIDLE \ 60 | SD_WAKE_IDLE \ 61 | SD_WAKE_BALANCE, \ 62 .last_balance = jiffies, \ 63 .balance_interval = 1, \ 64 .nr_balance_failed = 0, \ 65 }

schedule()

21

3378 array = rq->active; 3379 if (unlikely(!array->nr_active)) { 3380 /* 3381 * Switch the active and expired arrays. 3382 */ 3383 schedstat_inc(rq, sched_switch); 3384 rq->active = rq->expired; 3385 rq->expired = array; 3386 array = rq->active; 3387 rq->expired_timestamp = 0; 3388 rq->best_expired_prio = MAX_PRIO; 3389 }

这里可以看到 active 和 expire 优先级队列互换的操作

Kernel 的做法

22

• 进程醒来……• try_to_wake_up

try_to_wake_up() 1422 int idx = this_sd->wake_idx; 1423 unsigned int imbalance; 1424 1425 imbalance = 100 + (this_sd->imbalance_pct - 100) / 2; 1426 1427 load = source_load(cpu, idx); 1428 this_load = target_load(this_cpu, idx); 1429 1430 new_cpu = this_cpu; /* Wake to this CPU if we can */ 1431 1432 if (this_sd->flags & SD_WAKE_AFFINE) { // 对于一个 CPU 内的两个核 1433 unsigned long tl = this_load; 1434 unsigned long tl_per_task = cpu_avg_load_per_task(this_cpu); 1435…… 1441 if (sync) 1442 tl -= current->load_weight; 1443 1444 if ((tl <= load && 1445 tl + target_load(cpu, idx) <= tl_per_task) || 1446 100*(tl + p->load_weight) <= imbalance*load) { // 如果是同一 CPU 里的两个核,只要我这个核的负载不大,就把本来该另一个核跑的进程揽过来, AFFINE ,亲兄弟嘛 1447 /* 1448 * This domain has SD_WAKE_AFFINE and 1449 * p is cache cold in this domain, and 1450 * there is no bad imbalance. 1451 */ 1452 schedstat_inc(this_sd, ttwu_move_affine); 1453 goto out_set_cpu; 1454 } 1455 }

try_to_wake_up() 1461 if (this_sd->flags & SD_WAKE_BALANCE) { 1462 if (imbalance*this_load <= 100*load) { // 如果是两个不同 CPU 上的核,则只有在我这个核的负载很小时,才揽进程 1463 schedstat_inc(this_sd, ttwu_move_balance); 1464 goto out_set_cpu; 1465 } 1466 } 1467 } 1468 1469 new_cpu = cpu; /* Could not wake to this_cpu. Wake to cpu instead */ 1470 out_set_cpu: 1471 new_cpu = wake_idle(new_cpu, p); // 有没有空闲的核? 1472 if (new_cpu != cpu) { 1473 set_task_cpu(p, new_cpu); 1474 task_rq_unlock(rq, &flags); 1475 /* might preempt at this point */ 1476 rq = task_rq_lock(p, &flags); 1477 old_state = p->state; 1478 if (!(old_state & state)) 1479 goto out; 1480 if (p->array) 1481 goto out_running; 1482 1483 this_cpu = smp_processor_id(); 1484 cpu = task_cpu(p); 1485 }

Kernel 的做法

• 只是进程睡去和醒来的时候负载均衡就够了吗?– 如果某个进程调用 nice修改了 static_prio

• 每个核每 10ms 调用一次 scheduler_tick• scheduler_tick 调用 rebalance_tick• rebalance_tick根据核所在的 sched_domain

的特性来决定多久调一次 load_balance– 根据本 CPU 是否繁忙

25

每个 core 都有一个 APIC

26每个核都有自己的 APIC ,有自己的时钟中断

rebalance_tick()

27

2811 static void 2812 rebalance_tick(int this_cpu, struct rq *this_rq, enum idle_type idle) 2813 { 2814 unsigned long this_load, interval, j = cpu_offset(this_cpu); 2815 struct sched_domain *sd; 2816 int i, scale; 2817 2818 this_load = this_rq->raw_weighted_load; 2819 2820 /* Update our load: */ 2821 for (i = 0, scale = 1; i < 3; i++, scale <<= 1) { 2822 unsigned long old_load, new_load; 2823 2824 old_load = this_rq->cpu_load[i]; 2825 new_load = this_load; 2826 /* 2827 * Round up the averaging division if load is increasing. This 2828 * prevents us from getting stuck on 9 if the load is 10, for 2829 * example. 2830 */ 2831 if (new_load > old_load) 2832 new_load += scale-1; 2833 this_rq->cpu_load[i] = (old_load*(scale-1) + new_load) / scale; 2834 }

rebalance_tick()

28

2836 for_each_domain(this_cpu, sd) { 2837 if (!(sd->flags & SD_LOAD_BALANCE)) 2838 continue; 2839 2840 interval = sd->balance_interval; 2841 if (idle != SCHED_IDLE) 2842 interval *= sd->busy_factor; 2843 2844 /* scale ms to jiffies */ 2845 interval = msecs_to_jiffies(interval); 2846 if (unlikely(!interval)) 2847 interval = 1; 2848 2849 if (j - sd->last_balance >= interval) { 2850 if (load_balance(this_cpu, this_rq, sd, idle)) { 2851 /* 2852 * We've pulled tasks over so either we're no 2853 * longer idle, or one of our SMT siblings is 2854 * not idle. 2855 */ 2856 idle = NOT_IDLE; 2857 } 2858 sd->last_balance += interval; 2859 } 2860 } 2861 }

load_balance()

29

2527 static int load_balance(int this_cpu, struct rq *this_rq, 2528 struct sched_domain *sd, enum idle_type idle) 2529 {…… 2542 redo: 2543 group = find_busiest_group(sd, this_cpu, &imbalance, idle, &sd_idle, 2544 &cpus); 2545 if (!group) { 2546 schedstat_inc(sd, lb_nobusyg[idle]); 2547 goto out_balanced; 2548 } 2549 2550 busiest = find_busiest_queue(group, idle, imbalance, &cpus); 2551 if (!busiest) { 2552 schedstat_inc(sd, lb_nobusyq[idle]); 2553 goto out_balanced; 2554 } 2555 2556 BUG_ON(busiest == this_rq); 2557 2558 schedstat_add(sd, lb_imbalance[idle], imbalance); 2559 2560 nr_moved = 0; 2561 if (busiest->nr_running > 1) {…… 2568 double_rq_lock(this_rq, busiest); 2569 nr_moved = move_tasks(this_rq, this_cpu, busiest, 2570 minus_1_or_zero(busiest->nr_running), 2571 imbalance, sd, idle, &all_pinned); 2572 double_rq_unlock(this_rq, busiest);

load_balance() 的考量

• find_busiest_group 从本调度域找出负载最高的sched_group , find_busiest_queue 从sched_group中找出最繁忙的 rq– 可能负载已经均衡,找不出,则返回 NULL– 要考虑 imbalance_pct

• move_task 把任务从最繁忙的 rq 里挪到本 CPU来– Linux 的负载均衡是“拉模式”:我(本 core )定时

从别处最繁忙的地方拉任务过来– 一边预计算负载一边挪动,如果负载已经均衡,就不

要再挪了30

Kernel 的任务负载均衡

31

• 怎样衡量负载?– 运行队列上的 running 进程的 load_weight 之和

• 什么时间检查负载是否均衡?– 进程睡去、醒来– 每个核每 10ms

• 怎么调整为均衡?– 根据 imbalance 程度从最忙的队列里抽出 running

进程放入其他核