背景
https://mp.weixin.qq.com/s/1U1-qd-rlc1xHHsvd5l8Kg中可以瞭解到Kubernetes的資源模型透過Cgroups的相關子系統實現,其中對於CPU來說,它的Limits設定依賴於cfs.cpu_quota_us和cfs.cpu_period_us兩個引數,而說起兩個引數就不得不提起CFS頻寬控制了。
為了實現這個CPU頻寬控制的標的,Paul Turner和Bharata Rao先後提出了對CFS頻寬控制的核心補丁,在CFS bandwidth control中對他們的工作有相關的描述。按照CFS Bandwidth Control的描述,CFS頻寬控制是用來限制一個任務組能夠消耗的CPU的上限,具體來說就是透過設定period和quota,指定在period時間裡該任務組最多能夠消耗quota的CPU時間。
定義在kernel/sched/sched.h:363中:
struct task_group {
/* schedulable entities of this group on each CPU */
struct sched_entity **se; // 執行物體
/* runqueue "owned" by this group on each CPU */
struct cfs_rq **cfs_rq;
struct cfs_bandwidth cfs_bandwidth; // CPU頻寬
}
其中包含了一個型別為sched_entity的成員。sched_entity也即排程物體,它可以是行程,也可以是行程組,甚至使用者。這些排程物體按照vruntime時間序,以紅黑樹的形式組織,並由cfs_rq管理,同樣是task_group的欄位,它的定義在:kernel/sched/sched.h:488:
struct cfs_rq {
struct rq *rq; /* CPU runqueue to which this cfs_rq is attached */
};
它包含了一個rq結構的成員,這是該cfs_rq附著到每個CPU run queue的指標。
在回到task_group的定義裡,還可以看到cfs_bandwidth的結構體成員,它的定義在:`kernel/sched/sched.h:337`:
struct cfs_bandwidth {
ktime_t period; // period
u64 quota; // quota
};
可以看到period和quota正是cfs_bandwidth的成員。那麼綜上所述,CPU頻寬控制的物件是task_group,配置好的period和quota都是作用在task_group上,然後再分配給task_group下的cfs_rq。可以以下圖作為示例:
進一步說,可以認為給task_group分配的quota是一個global quota pool,然後假定每個cfs_rq又擁有一個local quota pool,那麼cfs_rq要從global quota pool中申請一定數量的CPU時間,這個時間叫做slice,可以透過/proc/sys/kernel/sched_cfs_bandwidth_slice_us,目前預設值為5ms。見下圖:
當某個cfs_rq申請到slice以後就會開始執行,當前slice消耗完後就會繼續申請。當在一個period的時間global quota pool已經分配乾凈,此時該cfs_rq就會進入一個叫做throttled的狀態,在這個狀態中任務因為無法獲取CPU時間所以無法被執行。這個throttle的統計狀態可以透過cpu.stat cgroup進行檢視。當該period結束後,global quota pool會被掃清並可以分配slice,如此進入下一個週期。
那麼回到前面所說的cfs_bandwidth定義裡:
struct cfs_bandwidth {
ktime_t period; // period
u64 quota; // quota
u64 runtime; // 剩餘時間,隨任務執行減少
struct hrtimer period_timer; // 高精度週期定時器,與period時間相等
struct hrtimer slack_timer; // 實現延遲歸還
struct list_head throttled_cfs_rq;
/* Statistics: */ // cpu.stat的傳回值
int nr_periods; // 經歷的時段數,單位為次數
int nr_throttled; // 發生throttle的次數
u64 throttled_time; // 被throttle的時間總和
};
可以瞭解到period的週期性由一個高精度定時器period_timer實現,被throttle的cfs_rq會掛到throttle_cfs_rq連結串列中。runtime的值最初跟quota一樣,但隨著任務執行會逐步減少。cpu.stats的值來自廈門的nr_periods、nr_throttled和throttled_time。
[cfs.go(https://gist.github.com/bobrik/2030ff040fad360327a5fab7a09c4ff1#file-cfs-go)。
這段程式碼的主要功能是執行一個可以造成CPU密集的sha512加密演演算法,並執行5ms,然後進入sleep狀態,待sleep結束以後繼續進行sha512加密,如此迴圈。透過引數可以配置sleep的時間和迴圈次數iteration。
為了方便測試,採用go 1.12.5的Docker映象進行測試,首先看一下不開啟頻寬控制的時候,執行下麵的命令:
`docker run –rm -it -v $(pwd):$(pwd) -w $(pwd) golang:1.12.5 go run cfs.go -iterations 20 -sleep 10ms`
得到結果:
可以從”burn took”看出來執行加密只需要5ms的時間,跟程式碼裡寫的是一樣的。
然後設定quota為25ms,period為100ms,程式slice是系統預設的5ms。比如執行下麵的命令:
docker run –rm -it –cpu-quota 25000 –cpu-period 100000 -v $(pwd):$(pwd) -w $(pwd) golang:1.12.5 go run cfs.go -iterations 20 -sleep 10ms
得到結果:
這個時候就會看到”burn took”有的時候會花費25ms,這個時間是怎麼來的?當任務開始執行的時候,先執行5ms,然後睡眠10ms,然後又執行5ms
也就是說當25ms的quota耗盡的時候,總共經歷了75ms(忽略第一個period)。此時由於quota已經耗盡,所以無法執行。然後從這一刻起直到period要掃清的第200ms,該cfs_rq就處於throttle的狀態。這部分throttle的時間剛好就是25ms。
註1:相關程式碼均參考核心5.1版本原始碼
註2:因學習需要略過部分不相關的程式碼
[CFS Bandwidth Control]
(https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt)
[CPU bandwidth control for CFS]
(https://landley.net/kdocs/ols/2010/ols2010-pages-245-254.pdf)
[CFS bandwidth control]
(https://lwn.net/Articles/428230/)
朋友會在“發現-看一看”看到你“在看”的內容