容器原理之cgroup

以 docker 爲代表,輕量、便攜的 container 使得打包和發佈應用非常容易。系列文章容器原理主要分析 container 用到的核心技術,主要包括 Linux namespace,cgroups,overlayfs,看完這些內容,你將可以手動創建一個和 container 類似的環境。

cgroup(control group)是一個內核特性,用於限制、統計、隔離一組進程的資源(CPU、內存、磁盤、網絡等),首字母不要大寫。

“cgroup” stands for “control group” and is never capitalized.

  • 單數形式(cgroup)指所有特性,也可以作爲“cgroup controllers”的修飾。

  • 複數形式(cgroups)指多個 cgroup。

Google 工程師在 2006 年開始提出這個特性,最早叫“process containers[1]”,爲了避免造成歧義,在 2007 年改名爲“control group”,在 2008 年 1 月發佈的 Linux Kernel 2.6.24 合入主線分支。此後,在此基礎上又增加了一系列特性,包括 kernfs(僞文件系統,用於向用戶導出內核的設備模型),firewalling(基於預定義的安全規則管理網絡流量)和 unified hierarchy[2]。在 Linux Kernel 4.5 版本合入了 cgroup v2[3] 的實現代碼。

cgroup 功能

在雲原生場景下,可以通過 cgroups 限制每個容器可以使用的資源。例如,對於 kubernetes,可以用於決定是否調度 pod 到某個節點,從而確保每個容器中的應用都有足夠的可用資源。

cgroup 主要功能包括:

  • 資源限制:限制一組進程的可用資源閾值。

  • 資源監控:監控 cgroup 級別的資源使用狀態。

  • 進程控制:控制(掛起 or 恢復)cgroup 中的所有進程。

  • 優先級控制:控制不同 cgroup 的優先級,當資源不足時,優先滿足優先級高的 cgroup。

cgroup 模式

目前,cgroups 有兩個版本,cgroup V1 和 cgroup V2。V1 功能相對零散,不方便維護,V2 是未來的演進方向。在這種情況下,節點存在以下面 3 種 cgroups 模式:

  • legacy:只支持 cgroup V1

  • hybrid:同時支持 cgroup V1 和 cgroup V2

  • unified:只支持 cgroup V2

在 containerd 中,通過以下方式判斷處於哪種模式:

const unifiedMountpoint = "/sys/fs/cgroup"

// Mode returns the cgroups mode running on the host
func Mode() CGMode {
    checkMode.Do(func() {
        var st unix.Statfs_t
        // 沒掛載 /sys/fs/cgroup
        if err := unix.Statfs(unifiedMountpoint, &st); err != nil {
            cgMode = Unavailable
            return
        }
        switch st.Type {
        case unix.CGROUP2_SUPER_MAGIC:
            // /sys/fs/cgroup 掛載爲 cgroup2 文件系統格式
            cgMode = Unified
        default:
            cgMode = Legacy
            if err := unix.Statfs(filepath.Join(unifiedMountpoint, "unified"), &st); err != nil {
                return
            }
            // /sys/fs/cgroup/unified 掛載爲 cgroup2 文件系統格式
            if st.Type == unix.CGROUP2_SUPER_MAGIC {
                cgMode = Hybrid
            }
        }
    })
    return cgMode
}


  • Cgroup

也可以通過以下命令判斷:

[ $(stat -fc %T /sys/fs/cgroup/) = "cgroup2fs" ] && echo "unified" || ( [ -e \
/sys/fs/cgroup/unified/ ] && echo "hybrid" || echo "legacy")


通過 Linux 啓動參數修改 cgroups 模式。例如,使用 unified 模式(注意cgroup_no_v1=all):

GRUB_CMDLINE_LINUX="cgroup_enable=memory systemd.unified_cgroup_hierarchy=1 \
systemd.legacy_systemd_cgroup_controller=0 cgroup_no_v1=all"


使用 legacy 模式:

GRUB_CMDLINE_LINUX="cgroup_enable=memory systemd.unified_cgroup_hierarchy=0 \
systemd.legacy_systemd_cgroup_controller=1"


使用 hybrid 模式:

GRUB_CMDLINE_LINUX="cgroup_enable=memory systemd.unified_cgroup_hierarchy=1 \
systemd.legacy_systemd_cgroup_controller=1"


cgroup V1

基本概念

cgroup v1 主要包括以下幾個概念:

  • subsystem(子系統):內核模塊,具體的資源控制器,可以被關聯到 cgroup 樹。不同資源的控制器不同,例如,內存子系統控制內存資源,CPU 子系統控制 CPU 資源。

  • cgroup(控制組):資源隔離的最小單位,表示一組進程和 cgroup 子系統的關聯,例如:通過內存子系統限制一組進程的可用內存資源總量。進程可以加入某個 cgroup,也可以從一個 cgroup 遷移到另一個 cgroup,同一進程不能同時存在同類型的兩個 cgroup 中。

  • hierarchy(層級):由一系列 cgroup 按照樹狀結構排列,每個節點都是一個 cgroup,子 cgroup 默認繼承父 cgroup 的參數和配置。系統可以有多個層級(cgroup 樹),每個層級可以和不同的 subsystem 關聯,每個 subsystem 只能和一個層級關聯,同一進程可以屬於多個層級,但在每個層級中只能屬於一個 cgroup 節點。

圖片

cgroup 類型(subsystem)

cgroup V1 支持的資源類型如下:

❯ ls -l /sys/fs/cgroup
total 0
dr-xr-xr-x  9 root root  0 Jun 12 20:56 blkio
lrwxrwxrwx  1 root root 11 Jun 12 20:56 cpu -> cpu,cpuacct
lrwxrwxrwx  1 root root 11 Jun 12 20:56 cpuacct -> cpu,cpuacct
dr-xr-xr-x 10 root root  0 Jun 12 20:56 cpu,cpuacct
dr-xr-xr-x  7 root root  0 Jun 12 20:56 cpuset
dr-xr-xr-x  9 root root  0 Jun 12 20:56 devices
dr-xr-xr-x  7 root root  0 Jun 12 20:56 freezer
dr-xr-xr-x  6 root root  0 Jun 12 20:56 hugetlb
dr-xr-xr-x 10 root root  0 Jun 12 20:56 memory
lrwxrwxrwx  1 root root 16 Jun 12 20:56 net_cls -> net_cls,net_prio
dr-xr-xr-x  7 root root  0 Jun 12 20:56 net_cls,net_prio
lrwxrwxrwx  1 root root 16 Jun 12 20:56 net_prio -> net_cls,net_prio
dr-xr-xr-x  7 root root  0 Jun 12 20:56 perf_event
dr-xr-xr-x  9 root root  0 Jun 12 20:56 pids
dr-xr-xr-x 10 root root  0 Jun 12 20:56 systemd


  • blkio:限制塊設備的 IO

  • cpu:通過調度程序控制可用 CPU 資源總量

  • cpuacct:CPU 資源的使用情況

  • cpuset:控制進程可用的 CPU 和內存

  • devices:控制對設備的訪問

  • freezer:掛起或恢復 cgroup 中的進程

  • hugetlb:控制進程可用的大頁內存

  • memory:控制進程可用的內存總量,同時統計內存使用情況

  • net_cls:將 cgroup 中的網絡包分類

  • net_prio:控制網絡流量的優先級

  • perf_event:監控 cgroup 的性能

  • pids:控制可創建的進程總數

Cgroup 子系統運行在內核態,不能直接和用戶交互,因此,需要通過文件系統提供和終端用戶的接口。在 Linux 中,表現爲 Cgroup 子系統掛載的文件系統目錄,用戶操作這些文件就可以直接和內核中的 Cgroup 對象交互。

不同的子系統對應的文件系統目錄包含不同的文件,但以下文件是所有子系統都有的:

cgroup.clone_children
cgroup.procs
notify_on_release
tasks


  • cgroup.clone_children

控制子 cgroup 是否繼承父 cgroup 的配置,只對 cpuset 子系統有效[4],並且在 cgroup V2 中已經移除[5]。

  • cgroup.procs

位於當前 cgroup 的 TGID(線程組 ID),TGID 是進程組中第一個進程的 PID。該文件是可寫的,向該文件寫入 TGID 即將對應線程組加入 cgroup,不保證文件中的 TGID 有序和不重複。

  • tasks

位於當前 cgroup 中 task 的 TID(線程 ID),即進程組中的所有線程的 ID。該文件是可寫的,將任務的 TID 寫入這個文件表示將其加入對應 cgroup,如果該任務的 TGID 在另一個 cgroup,會在 cgroup.procs 記錄該任務的 TGID,進程組中的其它 task 不受影響。不保證文件中的 TID 有序和不重複。

⚠️ 注意:如果向 cgroup.procs 寫入 TGID,系統會自動更新 tasks 文件中的內容爲該線程組中所有任務的 TID。

> cat cgroup.procs
> cat tasks
> echo 40790 > cgroup.procs
> cat tasks
40791
40792
40793
40794
40795


向 tasks 文件中寫入 TID,cgroup.procs 中的值也會更新爲對應 TGID,但是並不影響該線程組中的其它任務。

> echo 45345 > tasks
> cat cgroup.procs
45340
> cat tasks
45345


  • notify_on_release

是否開啓 release agent,如果該文件中的值爲 1,當 cgroup 中不包含任何 task 時(tasks 中的 TID 被全部移除),kernel 會執行 release_agent 文件(位於 root cgroup 的 release_agent 文件,例如/sys/fs/cgroup/memory/release_agent)的內容。所有非 root cgroup 從父 cgroup 繼承該值。

cgroup V2

基本概念

在 cgroup v2 中,去掉了層級(hierarchy)的概念,只有一個層級,所有 cgroup 在該層級中以樹形的方式組織,每個 cgroup 可以管理多種資源。

圖片

cgroup v2 不需要單獨掛載每個子系統。

❯ findmnt -R /sys/fs/cgroup
TARGET         SOURCE  FSTYPE  OPTIONS
/sys/fs/cgroup cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate


如何查看 cgroup 開啓了哪些 controller(和 cgroup v1 子系統的概念類似)呢?在 cgroup v2 中,每個 cgroup 目錄下有個名爲cgroup.controllers的可讀文件,記錄了當前 cgroup 啓用的 controller。根目錄下cgroup.controllers文件的內容記錄了當前系統支持的所有 controller。

❯ cat cgroup.controllers
cpuset cpu io memory pids


新建子 cgroup 時,cgroup.controllers 繼承父 cgroup 的 cgroup.subtree_control 值,子 cgroup 的 cgroup.subtree_control爲空,表示在該子 cgroup 下再次創建子 cgroup 時,默認不會啓用 controller。

創建一個testcgroup,此時,cgroup.subtree_control的值爲空:

❯ pwd
/sys/fs/cgroup
❯ mkdir test
❯ cat cgroup.subtree_control


cgroup.controllers的值和父 cgroup 的cgroup.subtree_control值相同:


# 父 cgroup 的 cgroup.subtree_control
❯ cat cgroup.subtree_control
cpuset cpu io memory pids

# test cgroup 的 cgroup.controllers
❯ cat test/cgroup.controllers
cpuset cpu io memory pids


test下再創建一個子 cgroup child,此時,childcgroup.controllers的內容爲空:

❯ mkdir test/child
❯ cat test/child/cgroup.controllers


子 cgroup child中的文件:

cgroup.controllers  cgroup.max.descendants  cgroup.threads  io.pressure
cgroup.events       cgroup.procs            cgroup.type     memory.pressure
cgroup.freeze       cgroup.stat             cpu.pressure
cgroup.max.depth    cgroup.subtree_control  cpu.stat


修改父 cgroup 的 cgroup.subtree_control,增加 cgroup controller:

❯ echo "+memory" > test/cgroup.subtree_control
❯ cat test/child/cgroup.controllers
memory


  1. 與此同時,子 cgroup child中增加了 memory 相關的文件:
❯ ls test/child
cgroup.controllers      cpu.stat             memory.oom.group
cgroup.events           io.pressure          memory.pressure
cgroup.freeze           memory.current       memory.reclaim
cgroup.max.depth        memory.drop_cache    memory.stat
cgroup.max.descendants  memory.events        memory.swap.current
cgroup.procs            memory.events.local  memory.swap.events
cgroup.stat             memory.high          memory.swap.max
cgroup.subtree_control  memory.low
cgroup.threads          memory.max
cgroup.type             memory.min
cpu.pressure            memory.numa_stat


刪除子 cgroup 中的 controller,也通過修改父 cgroup 的cgroup.subtree_control實現:

❯ echo "-memory" > test/cgroup.subtree_control
❯ cat test/child/cgroup.controllers


對一個 cgroup,如果 cgroup.procs的值不爲空,不能設置 cgroup.subtree_control的值。

❯ cat cgroup.procs
49347
❯ echo "+memory" > cgroup.subtree_control
echo: write error: device or resource busy


此時,需要將 cgroup 中的進程移動到其它子 cgroup,確保當前 cgroup 中cgroup.procs的值爲空:

❯ mkdir tmp
❯ echo 49347 > tmp/cgroup.procs
❯ echo "+memory" > cgroup.subtree_control
❯ cat child/cgroup.controllers
memory


未開啓任何 controller 時,cgroup 中包含以下文件:

❯ ls test/child
cgroup.controllers
cgroup.events
cgroup.freeze
cgroup.max.depth
cgroup.max.descendants
cgroup.procs
cgroup.stat
cgroup.subtree_control
cgroup.threads
cgroup.type
cpu.pressure
cpu.stat
io.pressure
memory.pressure


  • cgroup.controllers:當前 cgroup 開啓的 controller 列表。

  • cgroup.events:存在於非 root cgroup 中,包括兩個字段:populated 和 frozen。

❯ cat cgroup.events
populated 0
frozen 0


populated:如果當前 cgroup 和子層級中沒有存活的進程,populated 值爲 0,否則爲 1。值改變時會觸發 poll 和 notify 事件。考慮以下 cgroup 層級(括號中的數字代表 cgroup 中的進程數量):

A(4) - B(0) - C(1)
            \ D(0)


A,B 和 C 的 populated 值都爲 1,D 的 populated 值爲 0,如果 C 中對進程退出,則 B 和 C 中的 populated 值將變爲 0,並且會生成cgroup.events文件被修改的事件。

frozen:如果當前 cgroup 處於 frozen 狀態,值爲 1,否則爲 0。

  • cgroup.freeze:可讀可寫,可以通過向該文件寫入 1 將 cgroup 設置爲 freeze 狀態。默認值爲 0。
❯ cat cgroup.freeze
0
❯ echo 1 > cgroup.freeze
❯ cat cgroup.freeze
1
❯ cat cgroup.events
populated 0
frozen 1


  • cgroup.max.depth:當前 cgroup 允許創建子 cgroup 最大深度,如果實際深度大於或等於該值,嘗試創建新的子 cgroup 會失敗。默認值 max 不限制。

  • cgroup.max.descendants:當前 cgroup 允許創建子 cgroup 的最大數量,如果實際子 cgroup 數量大於該值,嘗試創建新的 cgroup 會失敗。默認值 max 不限制。

cgroup.max.depth 和 cgroup.max.descendants 的區別:

cgroup.max.descendants 限制當前 cgroup 下所有子 cgroup 的總和(包括所有子 cgroup)。

❯ cat cgroup.max.descendants
2
❯ mkdir test1
❯ mkdir test2
❯ mkdir test3
mkdir: cannot create directory ‘test3’: Resource temporarily unavailable


cgroup.max.depth 限制 cgroup 樹的深度:

❯ cat cgroup.max.depth
2
❯ mkdir test1
❯ mkdir test1/test2
❯ mkdir test1/test2/test3
mkdir: cannot create directory ‘test1/test2/test3’: Resource temporarily unavailable


  • cgroup.procs:可讀可寫,處於當前 cgroup 中的所有進程,每行一個 PID,沒有順序,並且可能存在重複(進程被移走後再次加入當前 cgroup)。向該文件中寫入 PID 可以將進程加入 cgroup。
❯ cat cgroup.procs
❯ echo 34256 > cgroup.procs
❯ cat cgroup.procs
34256


  • cgroup.threads:可讀可寫,處於當前 cgroup 中的所有線程,每行一個 TID,沒有順序,並且可能存在重複。

cgroup.procs中寫入 PID 後,cgroup 會向cgroup.threads文件中追加進程中所有線程的 TID。

❯ cat cgroup.threads
34256
34257
34258
34259
34260
34261


  • cgroup.stat:只讀文件,展示當前 cgroup 的狀態,包括以下字段:
❯ cat cgroup.stat
nr_descendants 2
nr_dying_descendants 0


nr_descendants:當前 cgroup 下子 cgroup 的總數。

mkdir test2
❯ cat cgroup.stat
nr_descendants 3
nr_dying_descendants 0


nr_dying_descendants:當前 cgroup 下正在被刪除的子 cgroup 數量。

  • cgroup.subtree_control:可讀可寫,當前 cgroup 的子 cgroup 啓用的 controller 列表。向該文件寫入以“+”或“-”爲前綴的 controller 名稱列表爲子 cgroup 啓用或禁用 controller。
❯ cat cgroup.subtree_control
❯ echo "+memory" > cgroup.subtree_control
❯ cat test1/cgroup.controllers
memory


當前 cgroup 中存在進程時,寫入不會成功:

❯ echo "+memory" > cgroup.subtree_control
echo: write error: device or resource busy


寫入當前 cgroup 沒有啓用的 controller 時,不會成功:

❯ cat cgroup.controllers
memory
❯ echo "+cpu" > cgroup.subtree_control
echo: write error: no such file or directory


當同時寫入多個 controller 時,要麼全成功,要麼全失敗,不存在部分成功的情況:

❯ echo "+memory +cpu" > cgroup.subtree_control
echo: write error: no such file or directory
❯ cat test1/cgroup.controllers


雖然啓用 memory controller 可以成功,但是啓用 cpu controller 的時候失敗了,所以 memory controller 也沒有啓用。

  • cgroup.type:可讀可寫,存在於非 root cgroup 中。可選的值:

“domain”:正常的有效 domain cgroup,默認類型。

“domain threaded”:threaded 類型 domain cgroup,作爲 threaded 子樹的根結點。

“domain invalid”:無效的 cgroup,不能啓用 controller,不能加入進程。可以轉換爲 threaded 類型的 cgroup。

“threaded”:threaded 類型 cgroup,位於 threaded 子樹中。

❯ cat cgroup.type
domain


  • cpu.pressure

當前 cgroup 的 CPU 壓力情況,基於 PSI[6](Pressure Stall Information)實現,這是 kernel 引入的一種評估系統壓力的機制。內容如下:

❯ cat cpu.pressure
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
full avg10=0.00 avg60=0.00 avg300=0.00 total=0


pressure值分爲兩行,avg10、avg60、avg300 分別表示 10s、60s、300s 時間週期內阻塞(stall)時間的百分比。total 是累計時間,單位 ms。

some行表示只有有一個任務在 cpu 資源上阻塞。full 行表示所有非 idle 狀態任務同時在 cpu 資源上阻塞,此時,cpu 資源完全浪費,嚴重影響性能。

在以下示例中,每個格子代表 10s,有顏色的格子代表在這段時間(爲了方便,以 10s 爲單位)內 cpu 處於阻塞狀態。

圖片

some 阻塞時間的百分比爲 80s/100s=80%,full 阻塞時間的百分比爲 30/100=30%。

  • io.pressure

cpu.pressure類似,表示當前 cgroup 內 io 資源阻塞時間的佔比。

❯ cat io.pressure
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
full avg10=0.00 avg60=0.00 avg300=0.00 total=0


  • memory.pressure

cpu.pressure類似,表示當前 cgroup 內 memory 資源阻塞時間的佔比。

❯ cat cpu.pressure
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
full avg10=0.00 avg60=0.00 avg300=0.00 total=0


  • cpu.stat:統計當前 cgroup 中 cpu 資源的使用情況。
❯ cat cpu.stat
usage_usec 2400
user_usec 0
system_usec 2400


usage_usec:總的 cpu 時間。

user_usec:用戶態進程佔用 cpu 的時間。

system_usec:內核態進程佔用 cpu 的時間。

V1 和 V2 主要區別

參考 facebook Chris Down 提到的例子[7]。

在 cgroup v1 中,每種資源一個層級。對於 bg 和 adhoc 兩個 cgroup,bg 需要限制 blkio 和 memory 兩種資源,adhoc 需要限制 memory 和 pids 兩種資源。cgroup v1 場景的視圖如下:

圖片

對於 cgroup v2,由於所有資源都在同一個 cgroup 下管理,通過 cgroup.sub_controller 控制子 cgroup 啓用的 controller,這樣一來,視圖以 cgroup 爲單位,更加清晰和便於管理。

圖片

參考資料

[1] process containers: https://lwn.net/Articles/236038/

[2] unified hierarchy: https://lkml.org/lkml/2014/3/13/503

[3] cgroup v2: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/diff/Documentation/cgroup-v2.txt?id=v4.5&id2=v4.4

[4] 只對 cpuset 子系統有效: https://www.kernel.org/doc/Documentation/cgroup-v1/cgroups.txt

[5] 在 cgroup V2 中已經移除: https://www.kernel.org/doc/Documentation/cgroup-v2.txt

[6] PSI: https://docs.kernel.org/accounting/psi.html#psi

[7] Chris Down 提到的例子: https://chrisdown.name/talks/cgroupv2/cgroupv2-fosdem.pdf

圖片

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章