Linux Cgroups介紹
上面是構建Linux容器的namespace技術,它幫進程隔離出自己單獨的空間,但Docker又是怎么限制每個空間的大小,保證他們不會互相爭搶呢?那么就要用到Linux的Cgroups技術。
概念
Linux Cgroups(Control Groups) 提供了對一組進程及將來的子進程的資源的限制,控制和統計的能力,這些資源包括CPU,內存,存儲,網絡等。通過Cgroups,可以方便的限制某個進程的資源占用,並且可以實時的監控進程的監控和統計信息。
Cgroups中的三個組件:
- cgroup
cgroup 是對進程分組管理的一種機制,一個cgroup包含一組進程,並可以在這個cgroup上增加Linux subsystem的各種參數的配置,將一組進程和一組subsystem的系統參數關聯起來。 -
subsystem
subsystem 是一組資源控制的模塊,一般包含有:- blkio 設置對塊設備(比如硬盤)的輸入輸出的訪問控制
- cpu 設置cgroup中的進程的CPU被調度的策略
- cpuacct 可以統計cgroup中的進程的CPU占用
- cpuset 在多核機器上設置cgroup中的進程可以使用的CPU和內存(此處內存僅使用於NUMA架構)
- devices 控制cgroup中進程對設備的訪問
- freezer 用於掛起(suspends)和恢復(resumes) cgroup中的進程
- memory 用於控制cgroup中進程的內存占用
- net_cls 用於將cgroup中進程產生的網絡包分類(classify),以便Linux的tc(traffic controller) 可以根據分類(classid)區分出來自某個cgroup的包並做限流或監控。
- net_prio 設置cgroup中進程產生的網絡流量的優先級
- ns 這個subsystem比較特殊,它的作用是cgroup中進程在新的namespace fork新進程(NEWNS)時,創建出一個新的cgroup,這個cgroup包含新的namespace中進程。
每個subsystem會關聯到定義了相應限制的cgroup上,並對這個cgroup中的進程做相應的限制和控制,這些subsystem是逐步合並到內核中的,如何看到當前的內核支持哪些subsystem呢?可以安裝cgroup的命令行工具(
apt-get install cgroup-bin
),然后通過lssubsys
看到kernel支持的subsystem。# / lssubsys -a cpuset cpu,cpuacct blkio memory devices freezer net_cls,net_prio perf_event hugetlb pids
-
hierarchy
hierarchy 的功能是把一組cgroup串成一個樹狀的結構,一個這樣的樹便是一個hierarchy,通過這種樹狀的結構,Cgroups可以做到繼承。比如我的系統對一組定時的任務進程通過cgroup1限制了CPU的使用率,然后其中有一個定時dump日志的進程還需要限制磁盤IO,為了避免限制了影響到其他進程,就可以創建cgroup2繼承於cgroup1並限制磁盤的IO,這樣cgroup2便繼承了cgroup1中的CPU的限制,並且又增加了磁盤IO的限制而不影響到cgroup1中的其他進程。
三個組件相互的關系:
通過上面的組件的描述我們就不難看出,Cgroups的是靠這三個組件的相互協作實現的,那么這三個組件是什么關系呢?
- 系統在創建新的hierarchy之后,系統中所有的進程都會加入到這個hierarchy的根cgroup節點中,這個cgroup根節點是hierarchy默認創建,后面在這個hierarchy中創建cgroup都是這個根cgroup節點的子節點。
- 一個subsystem只能附加到一個hierarchy上面
- 一個hierarchy可以附加多個subsystem
- 一個進程可以作為多個cgroup的成員,但是這些cgroup必須是在不同的hierarchy中
- 一個進程fork出子進程的時候,子進程是和父進程在同一個cgroup中的,也可以根據需要將其移動到其他的cgroup中。
這幾句話現在不理解暫時沒關系,后面我們實際使用過程中會逐漸的了解到他們之間的聯系的。
kernel接口:
上面介紹了那么多的Cgroups的結構,那到底要怎么調用kernel才能配置Cgroups呢?上面了解到Cgroups中的hierarchy是一種樹狀的組織結構,Kernel為了讓對Cgroups的配置更直觀,Cgroups通過一個虛擬的樹狀文件系統去做配置的,通過層級的目錄虛擬出cgroup樹,下面我們就以一個配置的例子來了解下如何操作Cgroups。
-
首先,我們要創建並掛載一個hierarchy(cgroup樹):
~ mkdir cgroup-test # 創建一個hierarchy掛載點 ~ sudo mount -t cgroup -o none,name=cgroup-test cgroup-test ./cgroup-test # 掛載一個hierarchy ~ ls ./cgroup-test # 掛載后我們就可以看到系統在這個目錄下生成了一些默認文件 cgroup.clone_children cgroup.procs cgroup.sane_behavior notify_on_release release_agent tasks
這些文件就是這個hierarchy中根節點cgroup配置項了,上面這些文件分別的意思是:
cgroup.clone_children
cpuset的subsystem會讀取這個配置文件,如果這個的值是1(默認是0),子cgroup才會繼承父cgroup的cpuset的配置。cgroup.procs
是樹中當前節點的cgroup中的進程組ID,現在我們在根節點,這個文件中是會有現在系統中所有進程組ID。notify_on_release
和release_agent
會一起使用,notify_on_release
表示當這個cgroup最后一個進程退出的時候是否執行release_agent
,release_agent
則是一個路徑,通常用作進程退出之后自動清理掉不再使用的cgroup。tasks
也是表示該cgroup下面的進程ID,如果把一個進程ID寫到tasks
文件中,便會將這個進程加入到這個cgroup中。
-
然后,我們創建在剛才創建的hierarchy的根cgroup中擴展出兩個子cgroup:
cgroup-test sudo mkdir cgroup-1 # 創建子cgroup "cgroup-1" cgroup-test sudo mkdir cgroup-2 # 創建子cgroup "cgroup-1" cgroup-test tree . |-- cgroup-1 | |-- cgroup.clone_children | |-- cgroup.procs | |-- notify_on_release | `-- tasks |-- cgroup-2 | |-- cgroup.clone_children | |-- cgroup.procs | |-- notify_on_release | `-- tasks |-- cgroup.clone_children |-- cgroup.procs |-- cgroup.sane_behavior |-- notify_on_release |-- release_agent `-- tasks
可以看到在一個cgroup的目錄下創建文件夾,kernel就會把文件夾標記會這個cgroup的子cgroup,他們會繼承父cgroup的屬性。
-
在cgroup中添加和移動進程:
一個進程在一個Cgroups的hierarchy中只能存在在一個cgroup節點上,系統的所有進程默認都會在根節點,可以將進程在cgroup節點間移動,只需要將進程ID寫到移動到的cgroup節點的tasks文件中。cgroup-1 echo $$ 7475 cgroup-1 sudo sh -c "echo $$ >> tasks" # 將我所在的終端的進程移動到cgroup-1中 cgroup-1 cat /proc/7475/cgroup 13:name=cgroup-test:/cgroup-1 11:perf_event:/ 10:cpu,cpuacct:/user.slice 9:freezer:/ 8:blkio:/user.slice 7:devices:/user.slice 6:cpuset:/ 5:hugetlb:/ 4:pids:/user.slice/user-1000.slice 3:memory:/user.slice 2:net_cls,net_prio:/ 1:name=systemd:/user.slice/user-1000.slice/session-19.scope
可以看到我們當前的
7475
進程已經被加到了cgroup-test:/cgroup-1
中。 -
通過subsystem限制cgroup中進程的資源
上面我們創建hierarchy的時候,但這個hierarchy並沒有關聯到任何subsystem,所以沒辦法通過那個hierarchy中的cgroup限制進程的資源占用,其實系統默認就已經把每個subsystem創建了一個默認的hierarchy,比如memory的hierarchy:~ mount | grep memory cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory,nsroot=/)
可以看到,在
/sys/fs/cgroup/memory
目錄便是掛在了memory subsystem的hierarchy。下面我們就通過在這個hierarchy中創建cgroup,限制下占用的進程占用的內存:memory stress --vm-bytes 200m --vm-keep -m 1 # 首先,我們不做限制啟動一個占用內存的stress進程 memory sudo mkdir test-limit-memory && cd test-limit-memory # 創建一個cgroup test-limit-memory sudo sh -c "echo "100m" > memory.limit_in_bytes" sudo sh -c "echo "100m" > memory.limit_in_bytes" # 設置最大cgroup最大內存占用為100m test-limit-memory sudo sh -c "echo $$ > tasks" # 將當前進程移動到這個cgroup中 test-limit-memory stress --vm-bytes 200m --vm-keep -m 1 # 再次運行占用內存200m的的stress進程
運行結果如下(通過top監控):
PID PPID TIME+ %CPU %MEM PR NI S VIRT RES UID COMMAND 8336 8335 0:08.23 99.0 10.0 20 0 R 212284 205060 1000 stress 8335 7475 0:00.00 0.0 0.0 20 0 S 7480 876 1000 stress PID PPID TIME+ %CPU %MEM PR NI S VIRT RES UID COMMAND 8310 8309 0:01.17 7.6 5.0 20 0 R 212284 102056 1000 stress 8309 7475 0:00.00 0.0 0.0 20 0 S 7480 796 1000 stress
可以看到通過cgroup,我們成功的將stress進程的最大內存占用限制到了100m。
Docker是如何使用Cgroups的:
我們知道Docker是通過Cgroups去做的容器的資源限制和監控,我們下面就以一個實際的容器實例來看下Docker是如何配置Cgroups的:
~ # docker run -m 設置內存限制 ~ sudo docker run -itd -m 128m ubuntu 957459145e9092618837cf94a1cb356e206f2f0da560b40cb31035e442d3df11 ~ # docker會為每個容器在系統的hierarchy中創建cgroup ~ cd /sys/fs/cgroup/memory/docker/957459145e9092618837cf94a1cb356e206f2f0da560b40cb31035e442d3df11 957459145e9092618837cf94a1cb356e206f2f0da560b40cb31035e442d3df11 # 查看cgroup的內存限制 957459145e9092618837cf94a1cb356e206f2f0da560b40cb31035e442d3df11 cat memory.limit_in_bytes 134217728 957459145e9092618837cf94a1cb356e206f2f0da560b40cb31035e442d3df11 # 查看cgroup中進程所使用的內存大小 957459145e9092618837cf94a1cb356e206f2f0da560b40cb31035e442d3df11 cat memory.usage_in_bytes 430080
可以看到Docker通過為每個容器創建Cgroup並通過Cgroup去配置的資源限制和資源監控。
用go語言實現通過cgroup限制容器的資源
下面我們在上一節的容器的基礎上加上cgroup的限制,下面這個demo實現了限制容器的內存的功能:
package main
import (
"os/exec"
"path"
"os"
"fmt"
"io/ioutil"
"syscall"
"strconv"
)
const cgroupMemoryHierarchyMount = "/sys/fs/cgroup/memory"
func main() {
if os.Args[0] == "/proc/self/exe" {
//容器進程
fmt.Printf("current pid %d", syscall.Getpid())
fmt.Println()
cmd := exec.Command("sh", "-c", `stress --vm-bytes 200m --vm-keep -m 1`)
cmd.SysProcAttr = &syscall.SysProcAttr{
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
cmd := exec.Command("/proc/self/exe")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
fmt.Println("ERROR", err)
os.Exit(1)
} else {
//得到fork出來進程映射在外部命名空間的pid
fmt.Printf("%v", cmd.Process.Pid)
// 在系統默認創建掛載了memory subsystem的Hierarchy上創建cgroup
os.Mkdir(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit"), 0755)
// 將容器進程加入到這個cgroup中
ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "tasks") , []byte(strconv.Itoa(cmd.Process.Pid)), 0644)
// 限制cgroup進程使用
ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "memory.limit_in_bytes") , []byte("100m"), 0644)
}
cmd.Process.Wait()
}
通過對Cgroups虛擬文件系統的配置,我們讓容器中的把stress進程的內存占用限制到了100m
。
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
10861 root 20 0 212284 102464 212 R 6.2 5.0 0:01.13 stress