cgroups 是Linux內核提供的一種可以限制單個進程或者多個進程所使用資源的機制,可以對 cpu,內存等資源實現精細化的控制,目前越來越火的輕量級容器 Docker 就使用了 cgroups 提供的資源限制能力來完成cpu,內存等部分的資源控制。
另外,開發者也可以使用 cgroups 提供的精細化控制能力,限制某一個或者某一組進程的資源使用。比如在一個既部署了前端 web 服務,也部署了後端計算模塊的八核服務器上,可以使用 cgroups 限制 web server 僅可以使用其中的六個核,把剩下的兩個核留給後端計算模塊。
本文從以下四個方面描述一下 cgroups 的原理及用法:
- cgroups 的概念及原理
- cgroups 文件系統概念及原理
- cgroups 使用方法介紹
- cgroups 實踐中的例子
###概念及原理
####cgroups子系統
cgroups 的全稱是control groups,cgroups爲每種可以控制的資源定義了一個子系統。典型的子系統介紹如下:
- cpu 子系統,主要限制進程的 cpu 使用率。
- cpuacct 子系統,可以統計 cgroups 中的進程的 cpu 使用報告。
- cpuset 子系統,可以爲 cgroups 中的進程分配單獨的 cpu 節點或者內存節點。
- memory 子系統,可以限制進程的 memory 使用量。
- blkio 子系統,可以限制進程的塊設備 io。
- devices 子系統,可以控制進程能夠訪問某些設備。
- net_cls 子系統,可以標記 cgroups 中進程的網絡數據包,然後可以使用 tc 模塊(traffic control)對數據包進行控制。
- freezer 子系統,可以掛起或者恢復 cgroups 中的進程。
- ns 子系統,可以使不同 cgroups 下面的進程使用不同的 namespace。
這裏面每一個子系統都需要與內核的其他模塊配合來完成資源的控制,比如對 cpu 資源的限制是通過進程調度模塊根據 cpu 子系統的配置來完成的;對內存資源的限制則是內存模塊根據 memory 子系統的配置來完成的,而對網絡數據包的控制則需要 Traffic Control 子系統來配合完成。本文不會討論內核是如何使用每一個子系統來實現資源的限制,而是重點放在內核是如何把 cgroups 對資源進行限制的配置有效的組織起來的,和內核如何把cgroups 配置和進程進行關聯的,以及內核是如何通過 cgroups 文件系統把cgroups的功能暴露給用戶態的。
####cgroups 層級結構(Hierarchy)
內核使用 cgroup 結構體來表示一個 control group 對某一個或者某幾個 cgroups 子系統的資源限制。cgroup 結構體可以組織成一顆樹的形式,每一棵cgroup 結構體組成的樹稱之爲一個 cgroups 層級結構。cgroups層級結構可以 attach 一個或者幾個 cgroups 子系統,當前層級結構可以對其 attach 的 cgroups 子系統進行資源的限制。每一個 cgroups 子系統只能被 attach 到一個 cpu 層級結構中。
比如上圖表示兩個cgroups層級結構,每一個層級結構中是一顆樹形結構,樹的每一個節點是一個 cgroup 結構體(比如cpu_cgrp, memory_cgrp)。第一個 cgroups 層級結構 attach 了 cpu 子系統和 cpuacct 子系統, 當前 cgroups 層級結構中的 cgroup 結構體就可以對 cpu 的資源進行限制,並且對進程的 cpu 使用情況進行統計。 第二個 cgroups 層級結構 attach 了 memory 子系統,當前 cgroups 層級結構中的 cgroup 結構體就可以對 memory 的資源進行限制。
在每一個 cgroups 層級結構中,每一個節點(cgroup 結構體)可以設置對資源不同的限制權重。比如上圖中 cgrp1 組中的進程可以使用60%的 cpu 時間片,而 cgrp2 組中的進程可以使用20%的 cpu 時間片。
####cgroups與進程
上面的小節提到了內核使用 cgroups 子系統對系統的資源進行限制,也提到了 cgroups 子系統需要 attach 到 cgroups 層級結構中來對進程進行資源控制。本小節重點關注一下內核是如何把進程與 cgroups 層級結構聯繫起來的。
在創建了 cgroups 層級結構中的節點(cgroup 結構體)之後,可以把進程加入到某一個節點的控制任務列表中,一個節點的控制列表中的所有進程都會受到當前節點的資源限制。同時某一個進程也可以被加入到不同的 cgroups 層級結構的節點中,因爲不同的 cgroups 層級結構可以負責不同的系統資源。所以說進程和 cgroup 結構體是一個多對多的關係。
上面這個圖從整體結構上描述了進程與 cgroups 之間的關係。最下面的P
代表一個進程。每一個進程的描述符中有一個指針指向了一個輔助數據結構css_set
(cgroups
subsystem set)。 指向某一個css_set
的進程會被加入到當前css_set
的進程鏈表中。一個進程只能隸屬於一個css_set
,一個css_set
可以包含多個進程,隸屬於同一css_set
的進程受到同一個css_set
所關聯的資源限制。
上圖中的"M×N Linkage"說明的是css_set
通過輔助數據結構可以與 cgroups 節點進行多對多的關聯。但是 cgroups 的實現不允許css_set
同時關聯同一個cgroups層級結構下多個節點。
這是因爲 cgroups 對同一種資源不允許有多個限制配置。
一個css_set
關聯多個 cgroups 層級結構的節點時,表明需要對當前css_set
下的進程進行多種資源的控制。而一個
cgroups 節點關聯多個css_set
時,表明多個css_set
下的進程列表受到同一份資源的相同限制。
###cgroups文件系統
Linux 使用了多種數據結構在內核中實現了 cgroups 的配置,關聯了進程和 cgroups 節點,那麼 Linux 又是如何讓用戶態的進程使用到 cgroups 的功能呢? Linux內核有一個很強大的模塊叫 VFS (Virtual File System)。 VFS 能夠把具體文件系統的細節隱藏起來,給用戶態進程提供一個統一的文件系統 API 接口。 cgroups 也是通過 VFS 把功能暴露給用戶態的,cgroups 與 VFS 之間的銜接部分稱之爲 cgroups 文件系統。下面先介紹一下 VFS 的基礎知識,然後再介紹下 cgroups 文件系統的實現。
####VFS
VFS 是一個內核抽象層,能夠隱藏具體文件系統的實現細節,從而給用戶態進程提供一套統一的 API 接口。VFS 使用了一種通用文件系統的設計,具體的文件系統只要實現了 VFS 的設計接口,就能夠註冊到 VFS 中,從而使內核可以讀寫這種文件系統。 這很像面向對象設計中的抽象類與子類之間的關係,抽象類負責對外接口的設計,子類負責具體的實現。其實,VFS本身就是用 c 語言實現的一套面向對象的接口。
#####通用文件模型
VFS 通用文件模型中包含以下四種元數據結構:
-
超級塊對象(superblock object),用於存放已經註冊的文件系統的信息。比如ext2,ext3等這些基礎的磁盤文件系統,還有用於讀寫socket的socket文件系統,以及當前的用於讀寫cgroups配置信息的 cgroups 文件系統等。
-
索引節點對象(inode object),用於存放具體文件的信息。對於一般的磁盤文件系統而言,inode 節點中一般會存放文件在硬盤中的存儲塊等信息;對於socket文件系統,inode會存放socket的相關屬性,而對於cgroups這樣的特殊文件系統,inode會存放與 cgroup 節點相關的屬性信息。這裏面比較重要的一個部分是一個叫做 inode_operations 的結構體,這個結構體定義了在具體文件系統中創建文件,刪除文件等的具體實現。
-
文件對象(file object),一個文件對象表示進程內打開的一個文件,文件對象是存放在進程的文件描述符表裏面的。同樣這個文件中比較重要的部分是一個叫 file_operations 的結構體,這個結構體描述了具體的文件系統的讀寫實現。當進程在某一個文件描述符上調用讀寫操作時,實際調用的是 file_operations 中定義的方法。 對於普通的磁盤文件系統,file_operations 中定義的就是普通的塊設備讀寫操作;對於socket文件系統,file_operations 中定義的就是 socket 對應的 send/recv 等操作;而對於cgroups這樣的特殊文件系統,file_operations 中定義的就是操作 cgroup 結構體等具體的實現。
-
目錄項對象(dentry object),在每個文件系統中,內核在查找某一個路徑中的文件時,會爲內核路徑上的每一個分量都生成一個目錄項對象,通過目錄項對象能夠找到對應的 inode 對象,目錄項對象一般會被緩存,從而提高內核查找速度。
#####cgroups文件系統的實現
基於 VFS 實現的文件系統,都必須實現 VFS 通用文件模型定義的這些對象,並實現這些對象中定義的部分函數。cgroup 文件系統也不例外,下面來看一下 cgroups 中這些對象的定義。
首先看一下 cgroups 文件系統類型的結構體:
static struct file_system_type cgroup_fs_type = {
.name = "cgroup",
.mount = cgroup_mount,
.kill_sb = cgroup_kill_sb,
};
這裏面兩個函數分別代表安裝和卸載某一個 cgroup 文件系統所需要執行的函數。每次把某一個 cgroups 子系統安裝到某一個裝載點的時候,cgroup_mount 方法就會被調用,這個方法會生成一個 cgroups_root(cgroups層級結構的根)並封裝成超級快對象。
然後看一下 cgroups 超級塊對象定義的操作:
static const struct super_operations cgroup_ops = {
.statfs = simple_statfs,
.drop_inode = generic_delete_inode,
.show_options = cgroup_show_options,
.remount_fs = cgroup_remount,
};
這裏只有部分函數的實現,這是因爲對於特定的文件系統而言,所支持的操作可能僅是 super_operations 中所定義操作的一個子集,比如說對於塊設備上的文件對象,肯定是支持類似 fseek 的查找某個位置的操作,但是對於 socket 或者 cgroups 這樣特殊的文件系統,就不支持這樣的操作。
同樣簡單看下 cgroups 文件系統對 inode 對象和 file 對象定義的特殊實現函數:
static const struct inode_operations cgroup_dir_inode_operations = {
.lookup = cgroup_lookup,
.mkdir = cgroup_mkdir,
.rmdir = cgroup_rmdir,
.rename = cgroup_rename,
};
static const struct file_operations cgroup_file_operations = {
.read = cgroup_file_read,
.write = cgroup_file_write,
.llseek = generic_file_llseek,
.open = cgroup_file_open,
.release = cgroup_file_release,
};
本文並不去研究這些函數的代碼實現是什麼樣的,但是從這些代碼可以推斷出,cgroups 通過實現 VFS 的通用文件系統模型,把維護 cgroups 層級結構的細節,隱藏在 cgroups 文件系統的這些實現函數中。
從另一個方面說,用戶在用戶態對 cgroups 文件系統的操作,通過 VFS 轉化爲對 cgroups 層級結構的維護。通過這樣的方式,內核把 cgroups 的功能暴露給了用戶態的進程。
###cgroups使用方法
####cgroups文件系統掛載
Linux中,用戶可以使用mount命令掛載 cgroups 文件系統,格式爲: mount -t cgroup -o subsystems name /cgroup/name
,其中
subsystems 表示需要掛載的 cgroups 子系統, /cgroup/name 表示掛載點,如上文所提,這條命令同時在內核中創建了一個cgroups 層級結構。
比如掛載 cpuset, cpu, cpuacct, memory 4個subsystem到/cgroup/cpu_and_mem 目錄下,就可以使用mount -t
cgroup -o remount,cpu,cpuset,memory cpu_and_mem /cgroup/cpu_and_mem
在centos下面,在使用yum install libcgroup
安裝了cgroups模塊之後,在 /etc/cgconfig.conf 文件中會自動生成
cgroups 子系統的掛載點:
mount {
cpuset = /cgroup/cpuset;
cpu = /cgroup/cpu;
cpuacct = /cgroup/cpuacct;
memory = /cgroup/memory;
devices = /cgroup/devices;
freezer = /cgroup/freezer;
net_cls = /cgroup/net_cls;
blkio = /cgroup/blkio;
}
上面的每一條配置都等價於展開的 mount 命令,例如mount -t cgroup -o cpuset cpuset /cgroup/cpuset
。這樣系統啓動之後會自動把這些子系統掛載到相應的掛載點上。
####子節點和進程
掛載某一個 cgroups 子系統到掛載點之後,就可以通過在掛載點下面建立文件夾或者使用cgcreate命令的方法創建 cgroups 層級結構中的節點。比如通過命令cgcreate
-t sankuai:sankuai -g cpu:test
就可以在 cpu 子系統下建立一個名爲 test 的節點。結果如下所示:
[root@idx cpu]# ls
cgroup.event_control cgroup.procs cpu.cfs_period_us cpu.cfs_quota_us cpu.rt_period_us cpu.rt_runtime_us cpu.shares cpu.stat lxc notify_on_release release_agent tasks test
然後可以通過寫入需要的值到 test 下面的不同文件,來配置需要限制的資源。每個子系統下面都可以進行多種不同的配置,需要配置的參數各不相同,詳細的參數設置需要參考 cgroups 手冊。使用 cgset 命令也可以設置 cgroups 子系統的參數,格式爲 cgset
-r parameter=value path_to_cgroup
。
當需要刪除某一個 cgroups 節點的時候,可以使用 cgdelete 命令,比如要刪除上述的 test 節點,可以使用cgdelete -r cpu:test
命令進行刪除
把進程加入到 cgroups 子節點也有多種方法,可以直接把 pid 寫入到子節點下面的 task 文件中。也可以通過 cgclassify 添加進程,格式爲 cgclassify
-g subsystems:path_to_cgroup pidlist
,也可以直接使用 cgexec 在某一個 cgroups 下啓動進程,格式爲gexec
-g subsystems:path_to_cgroup command arguments
.
###實踐中的例子
相信大多數人都沒有讀過 Docker 的源代碼,但是通過這篇文章,可以估計 Docker 在實現不同的 Container 之間資源隔離和控制的時候,是可以創建比較複雜的 cgroups 節點和配置文件來完成的。然後對於同一個 Container 中的進程,可以把這些進程 PID 添加到同一組 cgroups 子節點中已達到對這些進程進行同樣的資源限制。
通過各大互聯網公司在網上的技術文章,也可以看到很多公司的雲平臺都是基於 cgroups 技術搭建的,其實也都是把進程分組,然後把整個進程組添加到同一組 cgroups 節點中,受到同樣的資源限制。
筆者所在的廣告組,有一部分任務是給合作的廣告投放網站生成“商品信息”,廣告投放網站使用這些信息,把廣告投放在他們各自的網站上。但是有時候會有惡意的爬蟲過來爬取商品信息,所以我們生成了另外“一小份”數據供優先級較低的用戶下載,這時候基本能夠區分開大部分惡意爬蟲。對於這樣的“一小份”數據,對及時更新的要求不高,生成商品信息又是一個比較費資源的任務,所以我們把這個任務的cpu資源使用率限制在了50%。
首先在 cpu 子系統下面創建了一個 halfapi 的子節點:cgcreate abc:abc -g cpu:halfapi
。
然後在配置文件中寫入配置數據:echo 50000 > /cgroup/cpu/halfapi/cpu.cfs_quota_us
。cpu.cfs_quota_us
中的默認值是100000,寫入50000表示只能使用50%的
cpu 運行時間。
最後在這個cgroups中啓動這個任務:cgexec -g "cpu:/halfapi" php halfapi.php half >/dev/null 2>&1
在 cgroups 引入內核之前,想要完成上述的對某一個進程的 cpu 使用率進行限制,只能通過 nice 命令調整進程的優先級,或者 cpulimit 命令限制進程使用進程的 cpu 使用率。但是這些命令的缺點是無法限制一個進程組的資源使用限制,也就無法完成 Docker 或者其他雲平臺所需要的這一類輕型容器的資源限制要求。
同樣,在 cgroups 之前,想要完成對某一個或者某一組進程的物理內存使用率的限制,幾乎是不可能完成的。使用 cgroups 提供的功能,可以輕易的限制系統內某一組服務的物理內存佔用率。 對於網絡包,設備訪問或者io資源的控制,cgroups 同樣提供了之前所無法完成的精細化控制。
###結束語
本文首先介紹了 cgroups 在內核中的實現方式,然後介紹了 cgroups 如何通過 VFS 把相關的功能暴露給用戶,然後簡單介紹了 cgroups 的使用方法,最後通過分析了幾個 cgroups 在實踐中的例子,進一步展示了 cgroups 的強大的精細化控制能力。
筆者希望通過整篇文章的介紹,讀者能夠瞭解到 cgroups 能夠完成什麼樣的功能,並且希望讀者在使用 cgroups 的功能的時候,能夠大體知道內核通過一種什麼樣的方式來實現這種功能。