聊聊 Linux 分配 CPU 資源的第二種能力,對各個容器按照權重進行分配!
這篇文章中我們介紹了Linux內核給容器分配CPU資源的第一種方式,通過 period 和 quota 的組合來限制容器使用的CPU時間上限。但其實內核給實現CPU資源分配還存在第二種方式,那就是按權重分配。我們來看看這種分配方式是如何使用的,底層實現原理又是怎樣的。
一、Linux的完全公平調度器
在講容器權重分配之前,我們得先來回顧一下內核的完全公平調度器的實現。
Linux 內核中的完全公平調度器中每個邏輯核都有一個調度隊列 struct cfs_rq。每個調度隊列中都是用紅黑樹來組織的。紅黑樹的節點是 struct sched_entity, sched_entity 中既可以關聯具體的進程 struct task_struct 也可以關聯容器的 struct cfs_rq。
圖片
以下是完全公平調度器 cfs_rq 內核對象的定義。
// file:kernel/sched/sched.h
struct cfs_rq {
...
// 當前隊列中所有進程vruntime中的最小值
u64 min_vruntime;
// 保存就緒任務的紅黑樹
struct rb_root_cached tasks_timeline;
...
}在該對象中,最核心的是這個 rb_root_cached 類型的對象,這個對象的數據結構就是以紅黑樹來組織的。在紅黑樹的節點中,放是一個調度實體 sched_entity 對象。這個對象有可能是屬于普通進程 task_struct 的,也有可能是屬于容器進程組 task_group。
//file:kernel/sched/sched.h
struct task_group {
...
struct sched_entity **se;
struct cfs_rq **cfs_rq;
unsigned long shares;
}//file:include/linux/sched.h
struct task_struct {
...
struct sched_entity se;
}不管 sched_entity 是對應的進程也好,還是容器也罷,都會包含一個虛擬運行時間 vruntime 字段,和一個用來存權重數據的 load 字段。
圖片
在進程調度的過程中,每個邏輯核上有一個定時器,節拍性地觸發調度從紅黑樹上判斷是否要用最左側調度實體替換調當前正在運行的進程。在選擇進程進行切換時,雖然都多種策略,但最核心的是要保持所有調度實體的 vruntime 的公平。換句話說,不管 Linux 系統上有多少個使用完全公平調度器的進程(使用實時調度策略的進程除外),他們最終的 vruntime 基本會保持一致。
二、權重的設置
上節我們講到完全公平調度器運轉是基于 vruntime 的來維持所有調度實體公平地使用 CPU 資源的。但現實情況是,有的服務確實是需要多使用一些CPU 資源,另一些服務只需要少使用一點就可以。例如說某臺服務機是云上的一臺服務器,有的用戶購買了 8 核套餐,有的用戶只購買的 1 核。在計算 vruntime 的時候必然需要一些策略。
為了實現這個需求,每個調度實體中的都有一個權重就非常地有用了。
//file:include/linux/sched.h
struct sched_entity {
struct load_weight load;
u64 vruntime;
...
}
struct load_weight {
unsigned long weight;
u32 inv_weight;
};對于普通進程來說,這個權重可以使用 nice 命令來間接地修改。在容器中,在 cgroup v1 下可以通過 cgroupfs 下的 cpu.shares 文件來修改,在cgroup v2 下通過 cpu.weight / cpu.weight.nice 來修改。
在 cgroup v1 中,對 cpu.shares 的修改會執行到 cpu_shares_write_u64 這個函數中。
//file:kernel/sched/core.c
static struct cftype cpu_legacy_files[] = {
{
.name = "shares",
.read_u64 = cpu_shares_read_u64,
.write_u64 = cpu_shares_write_u64,
},
...
}在 cgroup v2 中,對 cpu.weight 的修改會執行到 cpu_weight_write_u64 函數中。
//file:kernel/sched/core.c
static struct cftype cpu_files[] = {
{
.name = "weight",
.flags = CFTYPE_NOT_ON_ROOT,
.read_u64 = cpu_weight_read_u64,
.write_u64 = cpu_weight_write_u64,
},
...
}不管是 cgroup v1 修改 cpu.shares 時執行 cpu_shares_write_u64,還是 cgroup v2 修改 cpu.weight 是執行 cpu_weight_write_u64,最終都會調用到 __sched_group_set_shares 來把權重信息 shares 記錄到調度實體 se 上去的。
//file:kernel/sched/fair.c
static int __sched_group_set_shares(struct task_group *tg, unsigned long shares)
{
......
tg->shares = shares;
for_each_possible_cpu(i) {
struct sched_entity *se = tg->se[i];
for_each_sched_entity(se)
update_cfs_group(se);
}
}
}具體的設置是在 update_cfs_group 中完成的,它依次調用 reweight_entity、update_load_set 來把權重值記錄到調度實體上。這樣后面就可以通過調度實體 se->load->weight 找到進程或容器的權重信息了。
//file:kernel/sched/fair.c
static inline void update_load_set(struct load_weight *lw, unsigned long w)
{
lw->weight = w;
lw->inv_weight = 0;
}三、容器 CPU 權重分配實現
完全公平調度器是維持的所有調度實體的 vruntime 的公平。但是 vruntime 會根據權重來進行縮放,vruntime 的實現是 calc_delta_fair 函數。
// file:kernel/sched/fair.c
static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
if (unlikely(se->load.weight != NICE_0_LOAD))
delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
return delta;
}在這個函數中,NICE_0_LOAD 宏對應的是 1024。如果權重是 1024,那么 vruntime 就正好等于實際運行時間。否則會進入到 __calc_delta 中來根據權重和實際運行時間來折算一個 vruntime 增量來。__calc_delta 函數為了追求極致的性能,實現上比較復雜一些,源碼就不給大家展示了。我們只把它用到的縮放算法展示如下:
vruntime = (實際運行時間 * ((NICE_0_LOAD * 2^32) / weight)) >> 32如果權重 weight 較高,那同樣的實際運行時間算出來的 vruntime 就會偏小,這樣它就會在調度中獲得更多的 CPU。如果權重 weight 較低,那算出來的 vruntime 就會比實際運行時間偏大。這樣它就會在調度的過程中獲得的 CPU 時間就會較少。完全公平調度器就是這樣簡單地實現了 CPU 資源的按權重分配。
我們再舉個例子,假如有一個 8 核的物理上,上面運行著 A 服務、B 服務、C 服務的一些容器。
圖片

























