博客 / 詳情

返回

Linux多核系統的負載均衡

前面的調度學習都是默認在單個 CPU 上的調度策略。我們知道為了 CPU 之間減少“干擾”,每個 CPU 上都有一個任務隊列。運行的過程種可能會出現有的 CPU 很忙,有的 CPU 很閒,如下圖所示:
image.png
為了避免這個問題的出現,Linux 內核實現了 CPU 可運行進程隊列之間的負載均衡。
因為負載均衡是在多個核上的均衡,所以在講解負載均衡之前,我們先看下多核的架構。
將 task 從負載較重的 CPU 上轉移到負載相對較輕的 CPU 上執行,這個過程就是負載均衡的過程。
多核架構
這裏以 Arm64 的 NUMA(Non Uniform Memory Access) 架構為例,看下多核架構的組成。
image.png
從圖中可以看出,這是非一致性內存訪問。每個 CPU 訪問 local memory,速度更快,延遲更小。因為 Interconnect 模塊的存在,整體的內存會構成一個內存池,所以 CPU 也能訪問 remote memory,但是相對 local memory 來説速度更慢,延遲更大。
我們知道一個多核心的 SOC 片上系統,內部結構是很複雜的。內核採用 CPU 拓撲結構來描述一個 SOC 的架構,使用調度域和調度組來描述 CPU 之間的層次關係。
LinuxC++後台服務器開發架構師免費學習地址
【文章福利】:小編整理了一些個人覺得比較好的學習書籍、視頻資料共享在羣文件裏面,有需要的可以自行添加哦!~點擊加入(需要自取)
image.png

CPU 拓撲
每一個 CPU 都會維護這麼一個結構體實例,用來描述 CPU 拓撲。

struct cpu_topology {
 int thread_id;
 int core_id;
 int cluster_id;
 cpumask_t thread_sibling;
 cpumask_t core_sibling;
};

thread_id: 從 mpidr_el1 寄存器中獲取
core_id:從 mpidr_el1 寄存器中獲取
cluster_id:從mpidr_el1寄存器中獲取
thread_sibling:當前 CPU 的兄弟 thread。
core_sibling:當前 CPU 的兄弟Core,即在同一個 Cluster 中的 CPU。
可以通過 /sys/devices/system/cpu/cpuX/topology 查看 cpu topology 的信息。
cpu_topology 結構體是通過函數 parse_dt_topology() 解析 DTS 中的信息建立的:
kernel_init() -> kernel_init_freeable() -> smp_prepare_cpus() -> init_cpu_topology() -> parse_dt_topology()
static int __init parse_dt_topology(void)
{
 struct device_node *cn, *map;
 int ret = 0;
 int cpu;

 cn = of_find_node_by_path("/cpus");          ------(1)
 if (!cn) {
  pr_err("No CPU information found in DT\n");
  return 0;
 }

 /*
  * When topology is provided cpu-map is essentially a root
  * cluster with restricted subnodes.
  */
 map = of_get_child_by_name(cn, "cpu-map");   ------(2)
 if (!map)
  goto out;

 ret = parse_cluster(map, 0);                 ------(3)
 if (ret != 0)
  goto out_map;

 topology_normalize_cpu_scale();

 /*
  * Check that all cores are in the topology; the SMP code will
  * only mark cores described in the DT as possible.
  */
 for_each_possible_cpu(cpu)
  if (cpu_topology[cpu].cluster_id == -1)
   ret = -EINVAL;

out_map:
 of_node_put(map);
out:
 of_node_put(cn);
 return ret;
}

找到 dts 中 cpu topology 的根節點 "/cpus"
找到 "cpu-map" 節點
解析 "cpu-map" 中的 cluster
以 i.mx8qm 為例,topology 為:”4A53 + 2A72”,dts中定義如下:

# imx8qm.dtsi

cpus: cpus {
        #address-cells = <2>;
        #size-cells = <0>;

        A53_0: cpu@0 {
                device_type = "cpu";
                compatible = "arm,cortex-a53", "arm,armv8";
                reg = <0x0 0x0>;
                clocks = <&clk IMX_SC_R_A53 IMX_SC_PM_CLK_CPU>;
                enable-method = "psci";
                next-level-cache = <&A53_L2>;
                operating-points-v2 = <&a53_opp_table>;
                #cooling-cells = <2>;
        };

        A53_1: cpu@1 {
                device_type = "cpu";
                compatible = "arm,cortex-a53", "arm,armv8";
                reg = <0x0 0x1>;
                clocks = <&clk IMX_SC_R_A53 IMX_SC_PM_CLK_CPU>;
                enable-method = "psci";
                next-level-cache = <&A53_L2>;
                operating-points-v2 = <&a53_opp_table>;
                #cooling-cells = <2>;
        };

        A53_2: cpu@2 {
                device_type = "cpu";
                compatible = "arm,cortex-a53", "arm,armv8";
                reg = <0x0 0x2>;
                clocks = <&clk IMX_SC_R_A53 IMX_SC_PM_CLK_CPU>;
                enable-method = "psci";
                next-level-cache = <&A53_L2>;
                operating-points-v2 = <&a53_opp_table>;
                #cooling-cells = <2>;
        };

        A53_3: cpu@3 {
                device_type = "cpu";
                compatible = "arm,cortex-a53", "arm,armv8";
                reg = <0x0 0x3>;
                clocks = <&clk IMX_SC_R_A53 IMX_SC_PM_CLK_CPU>;
                enable-method = "psci";
                next-level-cache = <&A53_L2>;
                operating-points-v2 = <&a53_opp_table>;
                #cooling-cells = <2>;
        };

        A72_0: cpu@100 {
                device_type = "cpu";
                compatible = "arm,cortex-a72", "arm,armv8";
                reg = <0x0 0x100>;
                clocks = <&clk IMX_SC_R_A72 IMX_SC_PM_CLK_CPU>;
                enable-method = "psci";
                next-level-cache = <&A72_L2>;
                operating-points-v2 = <&a72_opp_table>;
                #cooling-cells = <2>;
        };

        A72_1: cpu@101 {
                device_type = "cpu";
                compatible = "arm,cortex-a72", "arm,armv8";
                reg = <0x0 0x101>;
                clocks = <&clk IMX_SC_R_A72 IMX_SC_PM_CLK_CPU>;
                enable-method = "psci";
                next-level-cache = <&A72_L2>;
                operating-points-v2 = <&a72_opp_table>;
                #cooling-cells = <2>;
        };

        A53_L2: l2-cache0 {
                compatible = "cache";
        };

        A72_L2: l2-cache1 {
                compatible = "cache";
        };
  
        cpu-map {
                cluster0 {
                        core0 {
                                cpu = <&A53_0>;
                        };
                        core1 {
                                cpu = <&A53_1>;
                        };
                        core2 {
                                cpu = <&A53_2>;
                        };
                        core3 {
                                cpu = <&A53_3>;
                        };
                };

                cluster1 {
                        core0 {
                                cpu = <&A72_0>;
                        };
                        core1 {
                                cpu = <&A72_1>;
                        };
                };
        };
};

經過 parse_dt_topology(),update_siblings_masks() 解析後得到 cpu_topology 的值為:
CPU0: cluster_id = 0, core_id = 0
CPU1: cluster_id = 0, core_id = 1
CPU2: cluster_id = 0, core_id = 2
CPU3: cluster_id = 0, core_id = 3
CPU4: cluster_id = 1, core_id = 0
CPU5: cluster_id = 1, core_id = 1
調度域和調度組
在 Linux 內核中,調度域使用 sched_domain 結構表示,調度組使用 sched_group 結構表示。
調度域 sched_domain

struct sched_domain {
    struct sched_domain *parent;   
    struct sched_domain *child;     
    struct sched_group  *groups;     
    unsigned long min_interval;
    unsigned long max_interval; 
    ...
};

parent:由於調度域是分層的,上層調度域是下層的調度域的父親,所以這個字段指向的是當前調度域的上層調度域。
child:如上所述,這個字段用來指向當前調度域的下層調度域。
groups:每個調度域都擁有一批調度組,所以這個字段指向的是屬於當前調度域的調度組列表。
min_interval/max_interval:做均衡也是需要開銷的,不能時刻去檢查調度域的均衡狀態,這兩個參數定義了檢查該 sched domain 均衡狀態的時間間隔的範圍
sched_domain 是分成兩個 level,base domain 稱為 MC domain(multi core domain),頂層 domain 稱為 DIE domain。
調度組 sched_group
struct sched_group {

struct sched_group *next;
unsigned int group_weight;
...
struct sched_group_capacity *sgc;
unsigned long cpumask[0];

};

next:指向屬於同一個調度域的下一個調度組。
group_weight:該調度組中有多少個cpu。
sgc:該調度組的算力信息。
cpumask:用於標記屬於當前調度組的 CPU 列表(每個位表示一個 CPU)。
為了減少鎖的競爭,每一個 CPU 都有自己的 MC domain、DIE domain 以及 sched_group,並且形成了 sched_domain 之間的層級結構,sched_group 的環形鏈表結構。CPU 對應的調度域和調度組可通過在設備模型文件 /proc/sys/kernel/sched_domain 裏查看。
具體的 sched_domain 的初始化代碼分析如下:

kernel_init() -> kernel_init_freeable() -> sched_init_smp() -> init_sched_domains(cpu_active_mask) -> build_sched_domains(doms_cur[0], NULL)
static int
build_sched_domains(const struct cpumask *cpu_map, struct sched_domain_attr *attr)
{
 enum s_alloc alloc_state;
 struct sched_domain *sd;
 struct s_data d;
 int i, ret = -ENOMEM;

 alloc_state = __visit_domain_allocation_hell(&d, cpu_map);    ------(1)
 if (alloc_state != sa_rootdomain)
  goto error;

 /* Set up domains for CPUs specified by the cpu_map: */
 for_each_cpu(i, cpu_map) {
  struct sched_domain_topology_level *tl;

  sd = NULL;
  for_each_sd_topology(tl) {
   sd = build_sched_domain(tl, cpu_map, attr, sd, i);        ------(2)
   if (tl == sched_domain_topology)
    *per_cpu_ptr(d.sd, i) = sd;
   if (tl->flags & SDTL_OVERLAP)
    sd->flags |= SD_OVERLAP;
  }
 }

 /* Build the groups for the domains */
 for_each_cpu(i, cpu_map) {
  for (sd = *per_cpu_ptr(d.sd, i); sd; sd = sd->parent) {
   sd->span_weight = cpumask_weight(sched_domain_span(sd));
   if (sd->flags & SD_OVERLAP) {
    if (build_overlap_sched_groups(sd, i))
     goto error;
   } else {
    if (build_sched_groups(sd, i))                          ------(3)
     goto error;
   }
  }
 }
  ......
 /* Attach the domains */
 rcu_read_lock();
 for_each_cpu(i, cpu_map) {
  int max_cpu = READ_ONCE(d.rd->max_cap_orig_cpu);
  int min_cpu = READ_ONCE(d.rd->min_cap_orig_cpu);

  sd = *per_cpu_ptr(d.sd, i);

  if ((max_cpu < 0) || (cpu_rq(i)->cpu_capacity_orig >
      cpu_rq(max_cpu)->cpu_capacity_orig))
   WRITE_ONCE(d.rd->max_cap_orig_cpu, i);

  if ((min_cpu < 0) || (cpu_rq(i)->cpu_capacity_orig <
      cpu_rq(min_cpu)->cpu_capacity_orig))
   WRITE_ONCE(d.rd->min_cap_orig_cpu, i);

  cpu_attach_domain(sd, d.rd, i);                            ------(4)
 }
 rcu_read_unlock();

 if (!cpumask_empty(cpu_map))
  update_asym_cpucapacity(cpumask_first(cpu_map));

 ret = 0;
error:
 __free_domain_allocs(&d, alloc_state, cpu_map);             ------(5)
 return ret;
}

在每個 tl 層次,給每個 CPU 分配 sd、sg、sgc 空間
遍歷 cpu_map 裏所有 CPU,創建與物理拓撲結構對應的多級調度域
遍歷 cpu_map 裏所有 CPU, 創建調度組
將每個 CPU 的 rq 與 rd(root_domain) 進行綁定
free 掉分配失敗或者分配成功多餘的內存
所以,可運行進程隊列與調度域和調度組的關係如下圖所示:
image.png
總結
這裏用一張圖來總結下 CPU 拓撲,調度域初始化的過程,如下所示:
image.png
根據已經生成的 CPU 拓撲,調度域和調度組,最終可以生成如下圖所示的關係圖。
image.png
在上面的結構中,頂層的 DIE domain 覆蓋了系統中所有的 CPU,4 個 A53 是 Cluster 0,共享 L2 cache,兩外 2 個 A72 是 Cluster 1,共享 L2 cache。那麼每個 Cluster 可以認為是一個 MC 調度域,左邊的 MC 調度域中有 4 個調度組,右邊的 MC 調度域中有 2 個調度組,每個調度組中只有 1 個 CPU。整個 SOC 可以認為是高一級別的 DIE 調度域,其中有兩個調度組,Cluster 0 屬於一個調度組,Cluster 1 屬於另一個調度組。跨 Cluster 的負載均衡是需要清除 L2 cache 的,開銷是很大的,因此 SOC 級別的 DIE 調度域進行負載均衡的開銷比 MC 調度域更大一些。
到目前為止,我們已經將內核的調度域構建起來了,CFS 可以利用 sched_domain 來完成多核間的負載均衡了。
何時做負載均衡?
CFS 任務的負載均衡器有兩種:
一種是針對 busy CPU 的 periodic balancer,用於進程在 busy CPU 上的均衡
一種是針對 idle CPU 的 idle balancer,用於把 busy CPU 上的進程均衡到 idle CPU 上來。
image.png
periodic balancer:週期性負載均衡是在時鐘中斷 scheduler_tick 中,找到該 domain 中最繁忙的 sched group 和 CPU runqueue,將其上的任務 pull 到本 CPU,以便讓系統的負載處於均衡的狀態。
image.png
nohz idle balancer:當其他的 CPU 已經進入 idle,本 CPU 任務太重,需要通過 IPI 將其他 idle 的 CPU 喚醒來進行負載均衡。
image.png
new idle balancer:本 CPU 上沒有任務執行,馬上要進入 idle 狀態的時候,看看其他 CPU 是否需要幫忙,來從 busy cpu 上 pull 任務,讓整個系統的負載處於均衡狀態。
image.png
負載均衡的基本過程
當一個 CPU 上進行負載均衡的時候,總是從 base domain 開始,檢查其所屬 sched group 之間的負載均衡情況,如果有不均衡情況,那麼會在該 CPU 所屬 Cluster 之間進行遷移,以便維護 Cluster 內各個CPU 的任務負載均衡。
load_balance 是處理負載均衡的核心函數,它的處理單元是一個調度域,其中會包含對調度組的處理。

static int load_balance(int this_cpu, struct rq *this_rq,
                        struct sched_domain *sd, enum cpu_idle_type idle,
                        int *continue_balancing)
{
        ......
redo:
        if (!should_we_balance(&env)) {
                *continue_balancing = 0;
                goto out_balanced;
        }

        group = find_busiest_group(&env);           ------(1)
        if (!group) {
                schedstat_inc(sd->lb_nobusyg[idle]);
                goto out_balanced;
        }

        busiest = find_busiest_queue(&env, group);  ------(2)
        if (!busiest) {
                schedstat_inc(sd->lb_nobusyq[idle]);
                goto out_balanced;
        }

        BUG_ON(busiest == env.dst_rq);

        schedstat_add(sd->lb_imbalance[idle], env.imbalance);

        env.src_cpu = busiest->cpu;
        env.src_rq = busiest;

        ld_moved = 0;
        if (busiest->nr_running > 1) {
                env.flags |= LBF_ALL_PINNED;
                env.loop_max  = min(sysctl_sched_nr_migrate, busiest->nr_running);

more_balance:
                rq_lock_irqsave(busiest, &rf);
                update_rq_clock(busiest);
                
                cur_ld_moved = detach_tasks(&env);  ------(3)

                rq_unlock(busiest, &rf);

                if (cur_ld_moved) {
                        attach_tasks(&env);         ------(4)
                        ld_moved += cur_ld_moved;
                }

                local_irq_restore(rf.flags);

                if (env.flags & LBF_NEED_BREAK) {
                        env.flags &= ~LBF_NEED_BREAK;
                        goto more_balance;
                }
                ......
        }
        ......
out:
        return ld_moved;
}

找到該 domain 中最繁忙的 sched group
在這個最繁忙的 group 中挑選最繁忙的 CPU runqueue, 作為 src
從這個隊列中選擇任務來遷移,然後把被選中的任務從其所在的 runqueue 中移除
從最繁忙的 CPU runqueue 中 pull 一些任務到當前可運行隊列 dst
image.png

user avatar kedixa 頭像 tulingxiaobian 頭像
2 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.