Docker教程(八)---Cgroups-3-相關命令彙總及Go Demo

語言: CN / TW / HK

本章主要記錄了 Cgroups 相關的命令和操作,最後用一個 Demo 演示瞭如何用 Go 語言操作 Cgroups。

主要是之前學習 Cgroups 的時候沒有找到相關命令,這裡簡單記錄一下 Cgroup 具體是如何建立刪除及管理的。

準備跟著《自己動手寫 docker》這本書從零開始實現一個簡易版的 docker,加深對 docker 的理解。

原始碼及相關教程見 Github ,歡迎 star。

1. hierarchy

建立

由於 Linux Cgroups 是基於核心中的 cgroup virtual filesystem 的,所以建立 hierarchy 其實就是將其掛載到指定目錄。

語法為: mount -t cgroup -o subsystems name /cgroup/name

  • 其中 subsystems 表示需要掛載的 cgroups 子系統
  • /cgroup/name 表示掛載點(一般為具體目錄)

這條命令同在核心中建立了一個 hierarchy 以及一個預設的 root cgroup。

例如:

$ mkdir cg1
$ mount -t cgroup -o cpuset cg1 ./cg1

比如以上命令就是掛載一個 cg1 的 hierarchy 到 ./cg1 目錄,如果指定的 hierarchy 不存在則會新建。

hierarchy 建立的時候就會就會自動建立一個 cgroup 以作為 cgroup樹中的 root 節點。

刪除

刪除 hierarchy 則是解除安裝。

語法為: umount /cgroup/name

  • /cgroup/name 表示掛載點(一般為具體目錄)

例如:

$ umount ./cg1

以上命令就是解除安裝 ./cg1 這個目錄上掛載的 hierarchy,也就是前面掛載的 cg。

hierarchy 解除安裝後,相關的 cgroup 都會被刪除。

不過 cg1 目錄需要手動刪除。

檔案含義

hierarchy 掛載後會生成一些檔案,具體如下:

為了避免干擾,未關聯任何 subsystem

$ mkdir cg1
$ mount -t cgroup -o none,name=cg1 cg1 ./cg1
$ tree cg1
cg1
├── cgroup.clone_children
├── cgroup.procs
├── cgroup.sane_behavior
├── notify_on_release
├── release_agent
└── tasks

具體含義如下:

  • cgroup.clone_children :這個檔案只對cpuset subsystem有影響,當該檔案的內容為1時,新建立的cgroup將會繼承父cgroup的配置,即從父cgroup裡面拷貝配置檔案來初始化新cgroup,可以參考 這裡
  • cgroup.procs :當前cgroup中的所有 程序 ID,系統不保證ID是順序排列的,且ID有可能重複
  • cgroup.sane_behavior :具體功能不詳,可以參考 這裡這裡
  • notify_on_release :該檔案的內容為1時,當cgroup退出時(不再包含任何程序和子cgroup),將呼叫release_agent裡面配置的命令。
    • 新cgroup被建立時將預設繼承父cgroup的這項配置。
  • release_agent :裡面包含了cgroup退出時將會執行的命令,系統呼叫該命令時會將相應cgroup的相對路徑當作引數傳進去。
    • 注意:這個檔案只會存在於root cgroup下面,其他cgroup裡面不會有這個檔案。
    • 相當於配置一個回撥用於清理資源。
  • tasks :當前cgroup中的所有 執行緒 ID,系統不保證ID是順序排列的

cgroup.procs 和 tasks 的區別見 cgroup 操作章節。

release_agent

當一個cgroup裡沒有程序也沒有子cgroup時,release_agent將被呼叫來執行cgroup的清理工作。

具體操作流程:

  • 首先需要配置 notify_on_release 以開啟該功能。
  • 然後將指令碼內容寫入到 release_agent 中去。
  • 最後cgroup退出時(不再包含任何程序和子cgroup)就會執行 release_agent 中的命令。
#建立新的cgroup用於演示
[email protected]:~/cgroup/demo$ sudo mkdir test
#先enable release_agent
[email protected]:~/cgroup/demo$ sudo sh -c 'echo 1 > ./test/notify_on_release'

#然後建立一個指令碼/home/dev/cgroup/release_demo.sh,
#一般情況下都會利用這個指令碼執行一些cgroup的清理工作,但我們這裡為了演示簡單,僅僅只寫了一條日誌到指定檔案
[email protected]:~/cgroup/demo$ cat > /home/dev/cgroup/release_demo.sh << EOF
#!/bin/bash
echo \$0:\$1 >> /home/dev/release_demo.log
EOF

#新增可執行許可權
[email protected]:~/cgroup/demo$ chmod +x ../release_demo.sh

#將該指令碼設定進檔案release_agent
[email protected]:~/cgroup/demo$ sudo sh -c 'echo /home/dev/cgroup/release_demo.sh > ./release_agent'
[email protected]:~/cgroup/demo$ cat release_agent
/home/dev/cgroup/release_demo.sh

#往test裡面新增一個程序,然後再移除,這樣就會觸發release_demo.sh
[email protected]:~/cgroup/demo$ echo $$
27597
[email protected]:~/cgroup/demo$ sudo sh -c 'echo 27597 > ./test/cgroup.procs'
[email protected]:~/cgroup/demo$ sudo sh -c 'echo 27597 > ./cgroup.procs'

#從日誌可以看出,release_agent被觸發了,/test是cgroup的相對路徑
[email protected]:~/cgroup/demo$ cat /home/dev/release_demo.log
/home/dev/cgroup/release_demo.sh:/test

2. cgroup

建立

建立 cgroup 很簡單,在父 cgroup 或者 hierarchy 目錄下新建一個目錄就可以了。

具體層級關係就和目錄層級關係一樣。

# 建立子cgroup cgroup-cpu
$ mkdir cgroup-cpu
$ cd cgroup-cpu
# 建立cgroup-cpu的子cgroup
$ mkdir cgroup-cpu-1

刪除

刪除也很簡單,刪除對應 目錄 即可。

注意:是刪除目錄 rmdir,而不是遞迴刪除目錄下的所有檔案。

如果有多層 cgroup 則需要先刪除子 cgroup,否則會報錯:

$ rmdir cgroup-cpu
# 如果cgroup中有程序正在本限制,也會出現這個錯誤,需要先停掉對應程序,或者把程序移動到另外的 cgroup 中(比如父cgroup)
rmdir: failed to remove 'cgroup-cpu': Device or resource busy

先刪除子 cgroup 就可以了:

$ rmdir cg1
$ cd ../
$ rmdir cgroup-cpu

也可以藉助 libcgroup 工具來建立或刪除。

使用 libcgroup 工具前,請先安裝 libcgroup 和 libcgroup-tools 資料包

redhat系統安裝:

$ yum install libcgroup
$ yum install libcgroup-tools

ubuntu系統安裝:

$ apt-get install cgroup-bin
# 如果提示cgroup-bin找不到,可以用 cgroup-tools 替換
$ apt-get install cgroup-tools

具體語法:

# controllers就是subsystem
# path可以用相對路徑或者絕對路徑
$ cgdelete controllers:path

例如:

$ cgcreate cpu:./mycgroup
$ cgdelete cpu:./mycgroup

新增程序

建立新的 cgroup 後,就可以往裡面新增程序了。注意下面幾點:

  • 在一顆 cgroup 樹裡面, 一個程序必須要屬於一個 cgroup
    • 所以不能憑空從一個 cgroup 裡面刪除一個程序,只能將一個程序從一個 cgroup 移到另一個 cgroup
  • 新建立的子程序將會自動加入父程序所在的 cgroup。
    • 這也就是 tasks 和 cgroup.proc 的區別。
  • 從一個 cgroup 移動一個程序到另一個 cgroup 時,只要有目的 cgroup 的寫入許可權就可以了,系統不會檢查源 cgroup 裡的許可權。
  • 使用者只能操作屬於自己的程序,不能操作其他使用者的程序,root 賬號除外。
#--------------------------第一個shell視窗----------------------
#建立一個新的cgroup
[email protected]:~/cgroup/demo$ sudo mkdir test
[email protected]:~/cgroup/demo$ cd test

#將當前bash加入到上面新建立的cgroup中
[email protected]:~/cgroup/demo/test$ echo $$
1421
[email protected]:~/cgroup/demo/test$ sudo sh -c 'echo 1421 > cgroup.procs'
#注意:一次只能往這個檔案中寫一個程序ID,如果需要寫多個的話,需要多次呼叫這個命令

#--------------------------第二個shell視窗----------------------
#重新開啟一個shell視窗,避免第一個shell裡面執行的命令影響輸出結果
#這時可以看到cgroup.procs裡面包含了上面的第一個shell程序
[email protected]:~/cgroup/demo/test$ cat cgroup.procs
1421

#--------------------------第一個shell視窗----------------------
#回到第一個視窗,隨便執行一個命令,比如 top
[email protected]:~/cgroup/demo/test$ top
#這裡省略輸出內容

#--------------------------第二個shell視窗----------------------
#這時再在第二個視窗檢視,發現top程序自動加入了它的父程序(1421)所在的cgroup
[email protected]:~/cgroup/demo/test$ cat cgroup.procs
1421
16515
[email protected]:~/cgroup/demo/test$ ps -ef|grep top
dev      16515  1421  0 04:02 pts/0    00:00:00 top
[email protected]:~/cgroup/demo/test$

#在一顆cgroup樹裡面,一個程序必須要屬於一個cgroup,
#所以我們不能憑空從一個cgroup裡面刪除一個程序,只能將一個程序從一個cgroup移到另一個cgroup,
#這裡我們將1421移動到root cgroup
[email protected]:~/cgroup/demo/test$ sudo sh -c 'echo 1421 > ../cgroup.procs'
[email protected]:~/cgroup/demo/test$ cat cgroup.procs
16515
#移動1421到另一個cgroup之後,它的子程序不會隨著移動

#--------------------------第一個shell視窗----------------------
##回到第一個shell視窗,進行清理工作
#先用ctrl+c退出top命令
[email protected]:~/cgroup/demo/test$ cd ..
#然後刪除建立的cgroup
[email protected]:~/cgroup/demo$ sudo rmdir test

cgroup.procs vs tasks

#建立兩個新的cgroup用於演示
[email protected]:~/cgroup/demo$ sudo mkdir c1 c2

#為了便於操作,先給root賬號設定一個密碼,然後切換到root賬號
[email protected]:~/cgroup/demo$ sudo passwd root
[email protected]:~/cgroup/demo$ su root
[email protected]:/home/dev/cgroup/demo#

#系統中找一個有多個執行緒的程序
[email protected]:/home/dev/cgroup/demo# ps -efL|grep /lib/systemd/systemd-timesyncd
systemd+   610     1   610  0    2 01:52 ?        00:00:00 /lib/systemd/systemd-timesyncd
systemd+   610     1   616  0    2 01:52 ?        00:00:00 /lib/systemd/systemd-timesyncd
#程序610有兩個執行緒,分別是610和616

#將616加入c1/cgroup.procs
[email protected]:/home/dev/cgroup/demo# echo 616 > c1/cgroup.procs
#由於cgroup.procs存放的是程序ID,所以這裡看到的是616所屬的程序ID(610)
[email protected]:/home/dev/cgroup/demo# cat c1/cgroup.procs
610
#從tasks中的內容可以看出,雖然只往cgroup.procs中加了執行緒616,
#但系統已經將這個執行緒所屬的程序的所有執行緒都加入到了tasks中,
#說明現在整個程序的所有執行緒已經處於c1中了
[email protected]:/home/dev/cgroup/demo# cat c1/tasks
610
616

#將616加入c2/tasks中
[email protected]:/home/dev/cgroup/demo# echo 616 > c2/tasks

#這時我們看到雖然在c1/cgroup.procs和c2/cgroup.procs裡面都有610,
#但c1/tasks和c2/tasks中包含了不同的執行緒,說明這個程序的兩個執行緒分別屬於不同的cgroup
[email protected]:/home/dev/cgroup/demo# cat c1/cgroup.procs
610
[email protected]:/home/dev/cgroup/demo# cat c1/tasks
610
[email protected]:/home/dev/cgroup/demo# cat c2/cgroup.procs
610
[email protected]:/home/dev/cgroup/demo# cat c2/tasks
616
#通過tasks,我們可以實現執行緒級別的管理,但通常情況下不會這麼用,
#並且在cgroup V2以後,將不再支援該功能,只能以程序為單位來配置cgroup

#清理
[email protected]:/home/dev/cgroup/demo# echo 610 > ./cgroup.procs
[email protected]:/home/dev/cgroup/demo# rmdir c1
[email protected]:/home/dev/cgroup/demo# rmdir c2
[email protected]:/home/dev/cgroup/demo# exit
exit

結論:將執行緒ID加到 cgroup1的 cgroup.procs 時,會把執行緒對應程序ID加入 cgroup.procs 且還會把當前程序下的全部執行緒ID加入到 tasks 中。

這裡看起來,程序和執行緒好像效果是一樣的。

區別來了,如果此時把某個執行緒ID移動到另外的 cgroup2 的 tasks 中,會自動把 執行緒ID對應的程序ID加入到 cgroup2 的 cgroup.procs 中,且只把對應執行緒加入 tasks 中。

此時 cgroup1和cgroup2 的 cgroup.procs 都包含了同一個程序ID,但是二者的 tasks 中卻包含了不同的執行緒ID。

這樣就實現了 執行緒粒度的控制 。但通常情況下不會這麼用,並且在cgroup V2以後,將不再支援該功能,只能以程序為單位來配置cgroup。

3. 演示

Docker 是如何使用 cgroup 的

我們知道 Docker 是通過 Cgroups 實現容器資源限制和監控的,那麼具體是怎麼用的呢?

先啟動一個容器:

[[email protected] memory]# docker run -itd -m 128m nginx
da82f9ebd384730dda7f831b4331c9e55893c100c83c0c9b0ce112436aa93416

這裡通過 docker run -m 引數設定了記憶體限制。

實際上 docker 會在 memory cgroup 上建立一個叫 docker 的子 cgroup

$ ls -l /sys/fs/cgroup/memory/docker/
-rw-r--r-- 1 root root 0 Jan  6 19:53 cgroup.clone_children
--w--w--w- 1 root root 0 Jan  6 19:53 cgroup.event_control
-rw-r--r-- 1 root root 0 Jan  6 19:53 cgroup.procs
# 可以發現這一長串ID和建立容器時列印的是一致的
drwxr-xr-x 2 root root 0 Jan  6 19:56 da82f9ebd384730dda7f831b4331c9e55893c100c83c0c9b0ce112436aa93416
# 省略其他檔案

說明 docker 是為每個容器建立了一個子 cgroup 來單獨限制。

[[email protected] docker]# cd da82f9ebd384730dda7f831b4331c9e55893c100c83c0c9b0ce112436aa93416/
[[email protected] da82f9ebd384730dda7f831b4331c9e55893c100c83c0c9b0ce112436aa93416]# cat memory.limit_in_bytes 
134217728
[[email protected] da82f9ebd384730dda7f831b4331c9e55893c100c83c0c9b0ce112436aa93416]# 

可以發現,這裡面限制的記憶體 134217728/1024/1024 剛好就是我們指定的 128M。

所以 docker 使用 cgroup 其實很簡單,就是根據使用者指定的引數建立對應的 cgroup 限制。

Go Demo

其實挺簡單的,就是用 Go 翻譯了一遍上面的命令。

具體程式碼如下:

// cGroups cGroups初體驗
func cGroups() {
	// /proc/self/exe是一個符號連結,代表當前程式的絕對路徑
	if os.Args[0] == "/proc/self/exe" {
		// 第一個引數就是當前執行的檔名,所以只有fork出的容器程序才會進入該分支
		fmt.Printf("容器程序內部 PID %d\n", syscall.Getpid())
		// 需要先在宿主機上安裝 stress 比如 apt-get install stress
		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)
		}
	} else {
		// 主程序會走這個分支
		cmd := exec.Command("/proc/self/exe")
		cmd.SysProcAttr = &syscall.SysProcAttr{Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWNS | syscall.CLONE_NEWPID}
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		if err := cmd.Start(); err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
		// 得到 fork 出來的程序在外部namespace 的 pid
		fmt.Println("fork 程序 PID:", cmd.Process.Pid)
		// 在預設的 memory cgroup 下建立子目錄,即建立一個子 cgroup
		err := os.Mkdir(filepath.Join(cgroupMemoryHierarchyMount, "testmemorylimit"), 0755)
		if err != nil {
			fmt.Println(err)
		}
		// 	將容器加入到這個 cgroup 中,即將程序PID加入到cgroup下的 cgroup.procs 檔案中
		err = ioutil.WriteFile(filepath.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "cgroup.procs"),
			[]byte(strconv.Itoa(cmd.Process.Pid)), 0644)
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
		// 	限制程序的記憶體使用,往 memory.limit_in_bytes 檔案中寫入資料
		err = ioutil.WriteFile(filepath.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "memory.limit_in_bytes"),
			[]byte("100m"), 0644)
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
		cmd.Process.Wait()
	}
}

首先是一個 if 判斷,區分主程序和子程序,分別執行不同邏輯。

  • 主程序:fork 出子程序,並建立 cgroup,然後將子程序加入該 cgrouop
  • 子程序:執行 stress 命令,以消耗記憶體,便於檢視 memory cgroup 的效果

執行並測試:

lixd  ~/projects/docker/mydocker main $ go build main.go
lixd  ~/projects/docker/mydocker main $ sudo ./main
fork 程序 PID: 21827
當前程序 pid 1
stress: info: [7] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd

根據輸出可以知道,我們 fork 出的程序,pid 為 21827。

通過 pstree -pl 檢視程序關係:

$pstree -pl
init(1)─┬─init(8)───init(9)───fsnotifier-wsl(10)
        ├─init(12)───init(13)─┬─exe(20618)─┬─sh(20623)───stress(20624)───stress(20625)
        │                     │            ├─{exe}(20619)
        │                     │            ├─{exe}(20620)
        │                     │            ├─{exe}(20621)
        │                     │            └─{exe}(20622)
└─zsh(14)───sudo(21821)───main(21822)─┬─exe(21827)─┬─sh(21832)───stress(21833)───stress(21834)

可以看到 21827 程序 最終啟動了一個 21834 的 stress 程序。

top 檢視以下記憶體佔用:

PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
21834 root      20   0  208664 101564    272 D  35.2   1.3   0:14.38 stress

可以看到 RES 101564,也就是剛好100M,說明我們的 cgroup 是有效果的。

4. 小結

本文主要介紹了 hierarchy 和 cgroup 相關的操作,如建立刪除。

接著介紹了 hierarchy 中各個檔案含義,重點包括 release_agent 的作用以及 cgroup.procs 和 tasks 的區別。

最後簡答介紹了 Docker 是如何使用 cgroup 的,並提供了一個簡單的 Go 語言操作 cgroup demo。

5. 參考

cgroups(7) — Linux manual page

Linux Cgroup系列(02):建立並管理cgroup