CFS任務的負載均衡(概述)
我們描述負載均衡的系列文章一共三篇,第一篇是框架部分,即本文,主要描述了負載均衡相關的原理、場景和框架。後面的兩篇是對均衡程式碼的情景分析,透過對tick balance、new idle balance和task placement等幾個典型的負載均衡來呈現其實現細節,稍後釋出,敬請期待。
本文出現的核心程式碼來自Linux5.10.61,如果有興趣,讀者可以配合程式碼閱讀本文。
一、什麼是負載均衡
1、什麼是CPU負載(load)
CPU load是一個很容易和CPU usage混淆的概念。CPU usage是CPU忙閒的比例,例如在一個週期為1000ms的視窗中觀察CPU的情況,如果500ms的時間在執行任務,500ms的時間處於idle狀態,那麼在這個視窗中CPU的usage是50%。CPU usage是一個直觀的概念,
所見即所得,然而它不能用來對比。
例如一個任務在小核300MHz頻率下執行1000ms,看到CPU usage是100%,同樣的任務,在大核3GHz下的執行50ms,CPU usage是5%。
這種場景下,同樣的任務,load是一樣的,但是CPU usage差別很大。CPU 利用率(utility)是另外一個容易混淆的概念。Utility和usage的共同點都是考慮了running time,區別是Utility進行了歸一化,即把running time歸一化到了系統中最大算力(超大核最高頻率)上的執行時間。為了能和CPU capacity進行運算,utility還歸一化到了1024。
CPU load和utility一樣,都進行了歸一化,但是概念還是不同的。Utility是一個和busy time(執行時間)相關的量,在CPU利用率沒有達到100%的時候,利用率基本上等於負載,利用率高的時候負載也大,一旦當CPU利用率達到了100%的時候,利用率其實是無法給出CPU負載的狀況,因為大家的利用率都是100%,利用率相等,但是並不意味著CPUs的負載也是相等的,因為這時候不同CPU上runqueue中等待執行的任務數目不同,直覺上runque上掛著10任務的CPU承壓比掛著5個任務的CPU的負載要更重一些。因此,早期的CPU負載是使用runqueue深度來描述的。
3.8版本的linux核心引入了PELT演算法來跟蹤每一個sched entity的負載,把負載跟蹤的演算法從per-CPU進化到per-entity。PELT演算法不但能知道CPU的負載,而且知道負載來自哪一個排程實體,從而可以更精準的進行負載均衡。
2、什麼是均衡
對於負載均衡而言,並不是把整個系統的負載平均的分配到系統中的各個CPU上。實際上,我們還是必須要考慮系統中各個CPU的算力,讓CPU獲得和其算力匹配的負載。例如在一個6個小核+2個大核的系統中,整個系統如果有800的負載,那麼每個CPU上分配100的負載其實是不均衡的,因為大核CPU可以提供更強的算力。
什麼是CPU算力(capacity),所謂算力就是描述CPU的能夠提供的計算能力。在同樣的頻率下,一個微架構是A77的CPU顯然算力要大於A57的CPU。如果CPU的微架構都是一樣的,那麼一個最大頻率是2.2GHz的CPU算力肯定是大於最大頻率是1.1GHz的CPU。因此,確定了微架構和最大頻率,一個CPU的算力就基本確定了。struct rq資料結構中有兩個和算力相關的成員:
成員 | 描述 |
---|---|
unsigned long cpu_capacity; | 可以用於CFS任務的算力 |
unsigned long cpu_capacity_orig; | 該CPU的原始算力,和微架構及其最大頻率相關 |
Cpufreq系統會根據當前的CPU util來調節CPU當前的執行頻率,也許觸發溫控,限制了該CPU的最大頻率,但這並不能改變cpu_capacity_orig。本文主要描述CFS任務的均衡(RT的均衡不考慮負載,是在另外的維度),因此均衡需要考慮的CPU算力是cpu_capacity,這個算力需要把CPU用於執行rt、dl、irq的算力以及溫控損失的算力去掉,即該CPU可用於CFS任務的算力。
因此,CFS任務均衡中使用的CPU算力(cpu_capacity成員)其實一個不斷變化的值,需要經常更新(參考update_cpu_capacity函式)。為了讓CPU算力和utility可以對比,實際上我們採用了歸一化的方式,即系統中處理能力最強的CPU執行在最高頻率的算力是1024,其他的CPU算力根據微架構和最大執行頻率相應的進行調整。
有了各個任務負載,將runqueue中的任務負載累加起來就可以得到CPU負載,配合系統中各個CPU的算力,看起來我們就可以完成負載均衡的工作,然而事情沒有那麼簡單,當負載不均衡的時候,任務需要在CPU之間遷移,不同形態的遷移會有不同的開銷。
例如一個任務在小核cluster上的CPU之間的遷移所帶來的效能開銷一定是小於任務從小核cluster的CPU遷移到大核cluster的開銷。因此,為了更好的執行負載均衡,我們需要構建和CPU拓撲相關的資料結構,也就是排程域和排程組的概念。
3、排程域(sched domain)和排程組(sched group)
負載均衡的複雜性主要和複雜的系統拓撲有關。由於當前CPU很忙,我們把之前執行在該CPU上的一個任務遷移到新的CPU上的時候,如果遷移到新的CPU是和原來的CPU在不同的cluster中,效能會受影響(因為會cache會變冷)。但是對於超執行緒架構,cpu共享cache,這時候超執行緒之間的任務遷移將不會有特別明顯的效能影響。NUMA上任務遷移的影響又不同,我們應該儘量避免不同NUMA node之間的任務遷移,除非NUMA node之間的均衡達到非常嚴重的程度。總之,一個好的負載均衡演算法必須適配各種cpu拓撲結構。為了解決這些問題,linux核心引入了sched_domain的概念。
核心中struct sched_domain來描述排程域,其主要的成員如下:
成員 | 描述 |
---|---|
Parent和child | Sched domain會形成層級結構,parent和child建立了不同層級結構的父子關係。對於base domain而言,其child等於NULL,對於top domain而言,其parent等於NULL。 |
groups | 一個排程域中有若干個排程組,這些排程組形成一個環形連結串列,groups成員就是連結串列頭。 |
min_interval max_interval |
做均衡也是需要開銷的,我們不可能時刻去檢查排程域的均衡狀態,這兩個引數定義了檢查該sched domain均衡狀態的時間間隔範圍 |
busy_factor | 正常情況下,balance_interval定義了均衡的時間間隔,如果cpu繁忙,那麼均衡要時間間隔長一些,即時間間隔定義為busy_factor x balance_interval |
imbalance_pct | 排程域內的不均衡狀態達到了一定的程度之後就開始進行負載均衡的操作。imbalance_pct這個成員定義了判定不均衡的門限 |
cache_nice_tries | 這個成員應該是和nr_balance_failed配合控制負載均衡過程的遷移力度。當nr_balance_failed大於cache_nice_tries的時候,負載均衡會變得更加激進。 |
nohz_idle | 每個cpu都有其對應的LLC sched domain,而LLC SD記錄對應cpu的idle狀態(是否tick被停掉),進一步得到該domain中busy cpu的個數,體現在(sd->shared->nr_busy_cpus) |
flags | 排程域標誌,下面有表格詳細描述 |
level | 該sched domain在整個排程域層級結構中的level。Base sched domain的level等於0,向上依次加一。 |
last_balance | 上次進行balance的時間點。透過基礎均衡時間間隔()和當前sd的狀態可以計算最終的均衡間隔時間(get_sd_balance_interval),last_balance加上這個計算得到的均衡時間間隔就是下一次均衡的時間點。 |
balance_interval | 定義了該sched domain均衡的基礎時間間隔 |
nr_balance_failed | 本sched domain中進行負載均衡失敗的次數。當失敗次數大於cache_nice_tries的時候,我們考慮遷移cache hot的任務,進行更激進的均衡操作。 |
max_newidle_lb_cost | 在該domain上進行newidle balance的最大時間長度(即newidle balance的開銷)。 最小值是sysctl_sched_migration_cost |
next_decay_max_lb_cost | max_newidle_lb_cost會記錄最近在該sched domain上進行newidle balance的最大時間長度,這個max cost不是一成不變的,它有一個衰減過程,每秒衰減1%,這個成員就是用來控制衰減的。 |
avg_scan_cost | 平均掃描成本 |
負載均衡統計成員 | 負載均衡的統計資訊 |
struct sched_domain_shared *shared | 為了降低鎖競爭,Sched domain是per-CPU的,然而有一些資訊是需要在per-CPU 的sched domain之間共享的,不能在每個sched domain上構建。這些共享資訊是:1、該sched domain中的busy cpu的個數2、該sched domain中是否有idle的cpu |
span_weight span |
該排程域的跨度,在手機場景下,span等於該sched domain中所有CPU core形成的cpu mask。span_weight說明該sched domain中CPU的個數。 |
關於排程域標記解釋如下:
排程域標記 | 描述 |
---|---|
SD_BALANCE_NEWIDLE | 標記domain是否支援newidle balance。 |
SD_BALANCE_EXEC SD_BALANCE_FORK SD_BALANCE_WAKE |
在exec、fork、wake的時候,該domain是否支援指定型別的負載均衡。這些標記符號主要用來在確定exec、fork和wakeup場景下選核的範圍。 |
SD_WAKE_AFFINE | 是否在該domain上考慮進行wake affine,即滿足一定條件下,讓waker和wakee儘量靠近 |
SD_ASYM_CPUCAPACITY | 該domain上的CPU上是否具有一樣的capacity?MC domain上的CPU算力一樣,但是DIE domain上會設定該flag |
SD_SHARE_CPUCAPACITY | 該domain上的CPU上是否共享計算單元?例如SMT下,兩個硬體執行緒被看做兩個CPU core,但是它們之間不是完全獨立的,會競爭一些硬體的計算單元。手機上未使用該flag |
SD_SHARE_PKG_RESOURCES | Domain中的cpu是否共享SOC上的資源(例如cache)。手機平臺上,MC domain會設定該flag。 |
排程域並不是一個平層結構,而是根據CPU拓撲形成層級結構。相對應的,負載均衡也不是一蹴而就的,而是會在多個sched domain中展開(例如從base domain開始,一直到頂層sched domain,逐個domain進行均衡)。具體如何進行均衡(自底向上還是自頂向下,在哪些domain中進行均衡)是和均衡型別和各個sched domain設定的flag相關,具體細節後面會描述。
在指定排程域內進行均衡的時候不考慮系統中其他排程域的CPU負載情況,只考慮該排程域內的sched group之間的負載是否均衡。對於base doamin,其所屬的sched group中只有一個cpu,對於更高level的sched domain,其所屬的sched group中可能會有多個cpu core。核心中struct sched_group來描述排程組,其主要的成員如下:
成員 | 描述 |
---|---|
next | sched domain中的所有sched group會形成環形連結串列,next指向groups連結串列中的下一個節點。 |
ref | 該sched group的引用計數 |
group_weight | 該排程組中有多少個cpu |
sgc | 該排程組的算力資訊 |
cpumask | 該排程組包括哪些CPU |
上面的描述過於枯燥,我們後面會使用一個具體的例子來描述負載如何在各個level的sched domain上進行均衡的,不過在此之前,我們先看看負載均衡的整體軟體架構。
二、負載均衡的軟體架構
負載均衡的整體軟體結構圖如下:
負載均衡模組主要分兩個軟體層次:核心負載均衡模組和class-specific均衡模組。核心對不同的型別的任務有不同的均衡策略,普通的CFS(complete fair schedule)任務和RT、Deadline任務處理方式是不同的,由於篇幅原因,本文主要討論CFS任務的負載均衡。
(1)判斷該任務是否適合當前CPU算力
(2)如果判定需要均衡,那麼需要在CPU之間遷移多少的任務才能達到平衡?有了任務負載跟蹤模組,這個問題就比較好回答了。
為了更好的進行高效的均衡,我們還需要構建排程域的層級結構(sched domain hierarchy),圖中顯示的是二級結構(這裡給的是邏輯結構,實際核心中的各個level的sched domain是per cpu的)。手機場景多半是二級結構,支援NUMA的伺服器場景可能會形成更復雜的結構。
透過DTS和CPU topo子系統,我們可以構建sched domain層級結構,用於具體的均衡演算法。在手機平臺上,負載均衡會進行兩個level:MC domain的均衡和DIE domain的均衡。
- 在MC domain上,我們會對跟蹤每個CPU負載狀態(sched group只有一個CPU)並及時更新其算力,使得每個CPU上有其匹配的負載。
- 在DIE domain上,我們會跟蹤cluster上所有負載(每個cluster對應一個sched group)以及cluster的總算力,然後計算cluster之間負載的不均衡狀況,透過inter-cluster遷移讓整個DIE domain進入負載均衡狀態。
有了上面描述的基礎設施,那麼什麼時候進行負載均衡呢?這主要和排程事件相關,當發生任務喚醒、任務建立、tick到來等排程事件的時候,我們可以檢查當前系統的不均衡情況,並酌情進行任務遷移,以便讓系統負載處於平衡狀態。
三、如何做負載均衡
1、一個CPU拓撲示例
我們以一個4小核+4大核的處理器來描述CPU的domain和group:
在上面的結構中,sched domain是分成兩個level,base domain稱為MC domain(multi core domain)
- 頂層的domain稱為DIE domain。頂層的DIE domain覆蓋了系統中所有的CPU
- 小核cluster的MC domain包括所有小核cluster中的cpu,同理,大核cluster的MC domain包括所有大核cluster中的cpu。
對於小核MC domain而言,其所屬的sched group有四個,cpu0、1、2、3分別形成一個sched group,形成了MC domain的sched group環形連結串列。
不同CPU的MC domain的環形連結串列首元素(即sched domain中的groups成員指向的那個sched group)是不同的,對於cpu0的MC domain,其groups環形連結串列的順序是0-1-2-3,對於cpu1的MC domain,其groups環形連結串列的順序是1-2-3-0,以此類推。大核MC domain也是類似,這裡不再贅述。
對於非base domain而言,其sched group有多個cpu,覆蓋其child domain的所有cpu。
例如上面圖例中的DIE domain,它有兩個child domain,分別是大核domain和小核domian,因此,DIE domain的groups環形連結串列有兩個元素,分別是小核group和大核group。不同CPU的DIE domain的環形連結串列首元素(即連結串列頭)是不同的,對於cpu0的DIE domain,其groups環形連結串列的順序是(0,1,2,3)--(4,5,6,7),對於cpu6的MC domain,其groups環形連結串列的順序是(4,5,6,7)--(0,1,2,3),以此類推。
為了減少鎖的競爭,每一個cpu都有自己的MC domain和DIE domain,並且形成了sched domain之間的層級結構。在MC domain,其所屬cpu形成sched group的環形連結串列結構,各個cpu對應的MC domain的groups成員指向環形連結串列中的自己的cpu group。在DIE domain,cluster形成sched group的環形連結串列結構,各個cpu對應的DIE domain的groups成員指向環形連結串列中的自己的cluster group。
2、負載均衡的基本過程
負載均衡不是一個全域性CPU之間的均衡,實際上那樣做也不現實,當系統的CPU數量較大的時候,很難一次性的完成所有CPU之間的均衡,這也是提出sched domain的原因之一。
我們以週期性均衡為例來描述負載均衡的基本過程。當一個CPU上進行週期性負載均衡的時候,我們總是從base domain開始(對於上面的例子,base domain就是MC domain),檢查其所屬sched group之間(即各個cpu之間)的負載均衡情況,如果有不均衡情況,那麼會在該cpu所屬cluster之間進行遷移,以便維護cluster內各個cpu core的任務負載均衡。
(1)找到MC domain中最繁忙的sched group
(2)找到最繁忙sched group中最繁忙的CPU(對於MC domain而言,這一步不存在,畢竟其sched group只有一個cpu)
(3)從選中的那個繁忙的cpu上拉取任務,具體拉取多少的任務到本CPU runqueue上是和不均衡的程度相關,越是不均衡,拉取的任務越多。
完成MC domain均衡之後,繼續沿著sched domain層級結構向上檢查,進入DIE domain,在這個level的domain上,我們仍然檢查其所屬sched group之間(即各個cluster之間)的負載均衡情況,如果有不均衡的情況,那麼會進行inter-cluster的任務遷移。基本方法和MC domain類似,只不過在計算均衡的時候,DIE domain不再考慮單個CPU的負載和算力,它考慮的是:
(1)該sched group的負載,即sched group中所有CPU負載之和
(2)該sched group的算力,即sched group中所有CPU算力之和
3、其他需要考慮的事項
之所以要進行負載均衡主要是為了系統整體的throughput,避免出現一核有難,七核圍觀的狀況。
然而,進行負載均衡本身需要額外的算力開銷,為了降低開銷,我們為不同level的sched domain定義了時間間隔,不能太密集的進行負載均衡。
之外,我們還定義了不均衡的門限值,也就是說domain的group之間如果有較小的不均衡,我們也是可以允許的,超過了門限值才發起負載均衡的操作。很顯然,越高level的sched domain其不均衡的threashhold越高,越高level的均衡會帶來更大的效能開銷。
在引入異構計算系統之後,任務在placement的時候可以有所選擇。如果負載比較輕,或者該任務對延遲要求不高,我們可以放置在小核CPU執行,如果負載比較重或者該該任務和使用者體驗相關,那麼我們傾向於讓它在算力更高的CPU上執行。為了應對這種狀況,核心引入了misfit task的概念。一旦任務被標記了misfit task,那麼負載均衡演算法要考慮及時的將該任務進行upmigration,從而讓過載任務儘快完成,或者提升該任務的執行速度,從而提升使用者體驗。
除了效能,負載均衡也會帶來功耗的收益。例如系統有4個CPU,共計8個進入執行態的任務(負載相同)。這些任務在4個CPU上的排布有兩種選擇:
(1)全部放到一個CPU上
(2)每個CPU runqueue掛2個任務
負載均衡演算法會讓任務均布,從而帶來功耗的收益。雖然方案一中有三個CPU是處於idle狀態的,但是那個繁忙CPU執行在更高的頻率上。而方案二中,由於任務均布,CPU處於較低的頻率執行,功耗會比方案一更低。
四、負載均衡場景分析
1、整體的場景描述
在linux核心中,為了讓任務均衡的分佈在系統的所有CPU上,我們主要考慮下面三個場景:
(1)負載均衡(load balance)。透過搬移cpu runqueue上的任務,讓各個CPU上的負載匹配CPU算力。
(2)任務放置(task placement)。當阻塞的任務被喚醒的時候,確定該任務應該放置在那個CPU上執行
(3)主動均衡(active upmigration)。當一個低算力CPU的runqueue中出現misfit task的時候,如果該任務持續執行,那麼負載均衡無能為力,因為它只負責遷移runnable狀態的任務。這種場景下,active upmigration可以把當前正在執行的misfit task向上遷移到算力更高的CPU上去。
2、Task placement
任務放置主要發生在:
(1)喚醒一個新fork的執行緒
(2)Exec一個執行緒的時候
(3)喚醒一個阻塞的程序
在上面的三個場景中都會呼叫select_task_rq來為task選擇一個適合的CPU core。
3、Load balance
Load balance主要有三種:
(1)在tick中觸發load balance。我們稱之tick load balance或者periodic load balance。具體的程式碼執行路徑是:
(2)排程器在pick next的時候,當前cfs runque中沒有runnable,只能執行idle執行緒,讓CPU進入idle狀態。我們稱之new idle load balance。具體的程式碼執行路徑是:
(3)其他的cpu已經進入idle,本CPU任務太重,需要透過ipi將其idle的cpu喚醒來進行負載均衡。我們稱之nohz idle load banlance,具體的程式碼執行路徑是:
如果沒有dynamic tick特性,那麼其實不需要進行nohz idle load balance,因為tick會喚醒處於idle的cpu,從而週期性tick就可以覆蓋這個場景。
4、Active upmigration
主動遷移是Load balance的一種特殊場景。
在負載均衡中,只要運用適當的同步機制(持有一個或者多個rq lock),runnable的任務可以在各個CPU runqueue之間移動,然而running的任務是例外,它不掛在CPU runqueue中,load balance無法覆蓋。為了能夠遷移running狀態的任務,核心提供了Active upmigration的方法(利用stop machine排程類)。這個feature原生核心沒有提供,故不再詳述。