沒有 Cgroups,就沒有 Docker
Cgroups 是什麼?
Cgroups 是 control groups 的縮寫,是 Linux 核心提供的一種可以限制、記錄、隔離程序組(progress groups)所使用的物理資源(如:cpu,memory,IO 等等)的機制。最初由 google 的工程師提出,後面被整合進 Linux 核心。
Cgroups 可以做什麼?
Cgroups 最初的目標是為資源管理提供的一個統一的框架,既整合需要的 cpuset 等子系統,也為未來開發新的子系統提供介面。現在的 cgroups 適用多種應用場景,從單個程序的資源控制,到實現作業系統層次的虛擬化(OS Level Virtualization)。
cgroups 子系統(subsystem)
它的實現原理是通過對各類 Linux subsystem 進行引數設定,然後將程序和這些子系統進行繫結。
Linux subsystem 有以下幾種:
- blkio
- cpu
- cpuacct 統計cgroup中程序的CPU佔用
- cpuset
- devices
- freezer 使用者掛起和恢復從group中的程序
- memeory 控制cgroup中程序的記憶體佔用
- net_cls
- net_prio
- ns
通過安裝 cgroup 工具
$ apt-get install cgroup-tools $ lssubsys -a cpuset cpu,cpuacct blkio memory devices freezer net_cls,net_prio perf_event hugetlb pids rdma
cgroups 層級結構(hierarchy)
hierarchy
的功能是把一組 cgroup 組織成一個樹狀結構,讓 Cgroup 可以實現繼承
一個 cgroup1 限制了其下的程序(P1、P2、P3)的 CPU 使用頻率,如果還想對程序P2進行記憶體的限制,可以在 cgroup1 下建立 cgroup2,使其繼承於 cgroup1,可以限制 CPU 使用率,又可以設定記憶體的限制而不影響其他程序。
核心使用 cgroups 結構體來表示對某一個或某幾個 cgroups 子系統的資源限制,它是通過一棵樹的形式進行組織,被成為hierarchy
.
cgroups 與程序
hierarchy、subsystem 與cgroup程序組間的關係
hierarchy 只實現了繼承關係,真正的資源限制還是要靠 subsystem
通過將 subsystem 附加到 hierarchy上,
將程序組 加入到 hierarchy下(task中),實現資源限制
通過這張圖可以看出:
- 一個 subsystem 只能附加到一個 hierarchy 上面
- 一個 hierarchy 可以附加多個 subsystem
- 一個程序可以作為多個 cgroup 的成員,但是這些 cgroup 必須在不同hierarchy 中。
- 一個程序 fork 出子程序時,子程序是和父程序在同一個 cgroup 中的,也可以根據需要將其移動到其他 cgroup 中。
cgroups 檔案系統
cgroups 的底層實現被 Linux 核心的 VFS(Virtual File System)進行了隱藏,給使用者態暴露了統一的檔案系統 API 藉口。我們來體驗一下這個檔案系統的使用方式:
- 首先,要建立並掛載一個hierarchy(cgroup樹)
$ mkdir cgroup-test
$ sudo mount -t cgroup -o none,name=cgroup-test cgrout-test ./cgroup-test
$ ls ./cgrpup-test
cgroup.clone_children cgroup.sane_behavior release_agent
cgroup.procs notify_on_release tasks
這些檔案就是這個hierarchy中cgroup根節點的配置項
cgroup.clone_children 會被 cpuset 的 subsystem 讀取,如果是1,子 cgroup 會繼承父 cgroup 的 cpuset 的配置。
notify_on_release 和 release_agent 用於管理當最後一個程序退出時執行一些操作
tasks 標識該 cgroup 下面的程序 ID,將 cgroup 的程序成員與這個 hierarchy 關聯
2.再建立兩個子 hierarchy建立剛剛建立好的hierarchy上cgroup根節點中擴展出的兩個子cgroup
$ cd cgroup-test
$ sudo mkdir cgroup-1
$ sudo mkdir cgroup-2
$ 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
2 directories, 14 files
可以看到,在一個 cgroup 的目錄下建立資料夾時,Kernel 會把資料夾標記為這個 cgroup 的子 cgroup,它們會繼承父 cgroup 的屬性。
- 向cgroup中新增和移動程序
一個程序在一個Cgroups的hierarchy中,只能在一個cgroup節點上存在,系統的所有程序都會預設在根節點上存在,可以將程序移動到其他cgroup節點,只需要將程序ID寫到移動到的cgroup節點的tasks檔案中即可。
# cgroup-test
$ ehco $$
3444
$ cat /proc/3444/cgroup
13:name=cgroup-test:/
12:cpuset:/
11:rdma:/
10:devices:/user.slice
9:perf_event:/
8:net_cls,net_prio:/
7:pids:/user.slice/user-1000.slice/[email protected]
6:memory:/user.slice/user-1000.slice/[email protected]
...
可以看到當前終端的程序在根 cgroup 下,我們現在把他移動到子 cgroup 下
$ cd cgroup-1
$ sudo sh -c "echo $$ >> tasks"
$ cat /proc/3444/cgroup
13:name=cgroup-test:/cgroup-1
12:cpuset:/
11:rdma:/
10:devices:/user.slice
9:perf_event:/
8:net_cls,net_prio:/
7:pids:/user.slice/user-1000.slice/[email protected]
6:memory:/user.slice/user-1000.slice/[email protected]
...
可以看到終端程序所屬的 cgroup 已將變成了 cgroup-1,再看一下父 cgroup 的tasks,已經沒有了終端程序的 ID
$ cd cgroup-test
$ cat tasks | grep "3444"
# 返回為空
- 通過 subsystem 限制 cgroup 中程序的資源。
作業系統預設已為每一個 subsystem 建立了一個預設的 hierarchy,在sys/fs/cgroup/
目錄下
$ ls /sys/fs/cgroup
blkio cpu,cpuacct freezer net_cls perf_event systemd
cpu cpuset hugetlb net_cls,net_prio pids unified
cpuacct devices memory net_prio rdma
可以看到記憶體子系統的 hierarchy 也在其中建立一個子cgroup
$ cd /sys/fs/cgroup/memory
$ sudo mkdir test-limit-memory && cd test-limit-memorysudo
# 設定最大記憶體使用為 100MB
$ sudo sh -c "echo "100m" > memory.limit_in_bytes"sudo sh -c "echo $$ > tasks"
sudo sh -c "echo $$ > tasks"
$ sudo sh -c "echo $$ > tasks"
# 執行佔用記憶體200MB 的 stress 經常
$ stress --vm-bytes 200m --vm-keep -m 1
可以對比執行前後的記憶體剩餘量,大概只減少了100MB
# 執行前
$ top
top - 12:04:12 up 6:45, 1 user, load average: 1.87, 1.29, 1.06
任務: 348 total, 1 running, 346 sleeping, 0 stopped, 1 zombie
%Cpu(s): 1.3 us, 0.9 sy, 0.0 ni, 97.7 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st
MiB Mem : 5973.4 total, 210.8 free, 2820.9 used, 2941.8 buff/cache
MiB Swap: 923.3 total, 921.9 free, 1.3 used. 2746.3 avail Mem
# 執行後
$ top
top - 12:04:57 up 6:45, 1 user, load average: 2.25, 1.44, 1.12
任務: 351 total, 3 running, 347 sleeping, 0 stopped, 1 zombie
%Cpu(s): 34.3 us, 32.8 sy, 0.0 ni, 21.1 id, 4.9 wa, 0.0 hi, 6.9 si, 0.0 st
MiB Mem : 5973.4 total, 118.6 free, 2956.7 used, 2898.1 buff/cache
MiB Swap: 923.3 total, 817.7 free, 105.5 used. 2604.5 avail Mem
說明 cgroup 的限制生效了
docker 中是怎樣進行 cgroup 限制的
首先執行一個被限制記憶體的容器
$ sudo docker pull redis:4
$ sudo docker run -tid -m 100m redis:4
d79f22eb11d22c56a90f88e0aeb3cfda7cbe9639e2ab0e8532003a695e375e8d
檢視原來的記憶體子系統繫結的cgroup,會看到裡面多了子cgroup, docker
$ ls /sys/fs/cgroup/memory
... docker
...
$ ls /sys/fs/cgroup/memory/docker
cgroup.clone_children memory.max_usage_in_bytes
cgroup.event_control memory.memsw.failcnt
cgroup.procs memory.memsw.limit_in_bytes
d79f22eb11d22c56a90f88e0aeb3cfda7cbe9639e2ab0e8532003a695e375e8d memory.memsw.max_usage_in_bytes
memory.failcnt memory.memsw.usage_in_bytes
memory.force_empty memory.move_charge_at_immigrate
memory.kmem.failcnt memory.numa_stat
memory.kmem.limit_in_bytes memory.oom_control
memory.kmem.max_usage_in_bytes memory.pressure_level
memory.kmem.slabinfo memory.soft_limit_in_bytes
memory.kmem.tcp.failcnt memory.stat
memory.kmem.tcp.limit_in_bytes memory.swappiness
memory.kmem.tcp.max_usage_in_bytes memory.usage_in_bytes
memory.kmem.tcp.usage_in_bytes memory.use_hierarchy
memory.kmem.usage_in_bytes notify_on_release
memory.limit_in_bytes tasks
可以看到docker
cgroup裡面的d79f22eb11d22c56a90f88e0aeb3cfda7cbe9639e2ab0e8532003a695e375e8d
cgroup 正好是我們
剛才建立的容器 ID,那麼看一下里面吧
$ cd /sys/fs/cgroup/memory/docker/d79f22eb11d22c56a90f88e0aeb3cfda7cbe9639e2ab0e8532003a695e375e8d
$ cat memory.limit_in_bytes
104857600cat
# 正好是100MB
總結
講述了 cgroups 的原理,它是通過三個概念(cgroup、subsystem、hierarchy)進行組織和關聯的,可以理解為
3層結構,將程序關聯在 cgroup 中,然後把 cgroup 與 hierarchy 關聯,subsystem 再與 hierarchy 關聯,從而在限制程序資源的基礎上達到一定
的複用能力。
講述了 docker 的具體實現方式,在使用 docker 時,也能從心中瞭然它時怎麼做到對容器使用資源的限制的。