没有 Cgroups,就没有 Docker

语言: CN / TW / HK

「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

Cgroups 是什么?

Cgroups 是 control groups 的缩写,是 Linux 内核提供的一种可以限制记录隔离进程组(progress groups)所使用的物理资源(如:cpu,memory,IO 等等)的机制。最初由 google 的工程师提出,后面被整合进 Linux 内核。

image.png

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

image.png

通过安装 cgroup 工具 shell $ 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中),实现资源限制

image

通过这张图可以看出: - 一个 subsystem 只能附加到一个 hierarchy 上面 - 一个 hierarchy 可以附加多个 subsystem - 一个进程可以作为多个 cgroup 的成员,但是这些 cgroup 必须在不同hierarchy 中。 - 一个进程 fork 出子进程时,子进程是和父进程在同一个 cgroup 中的,也可以根据需要将其移动到其他 cgroup 中。

cgroups 文件系统

cgroups 的底层实现被 Linux 内核的 VFS(Virtual File System)进行了隐藏,给用户态暴露了统一的文件系统 API 借口。我们来体验一下这个文件系统的使用方式:

  1. 首先,要创建并挂载一个hierarchy(cgroup树) shell $ 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 ```shell $ 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 的属性。

  1. 向cgroup中添加和移动进程 一个进程在一个Cgroups的hierarchy中,只能在一个cgroup节点上存在,系统的所有进程都会默认在根节点上存在,可以将进程移动到其他cgroup节点,只需要将进程ID写到移动到的cgroup节点的tasks文件中即可。 ```shell

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 下shell $ 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,已经没有了终端进程的 IDshell $ cd cgroup-test $ cat tasks | grep "3444"

返回为空

```

  1. 通过 subsystem 限制 cgroup 中进程的资源。

操作系统默认已为每一个 subsystem 创建了一个默认的 hierarchy,在sys/fs/cgroup/目录下 shell $ 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 ``` shell $ 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 可以对比运行前后的内存剩余量,大概只减少了100MBshell

运行前

$ 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 限制的

首先运行一个被限制内存的容器 shell $ sudo docker pull redis:4 $ sudo docker run -tid -m 100m redis:4 d79f22eb11d22c56a90f88e0aeb3cfda7cbe9639e2ab0e8532003a695e375e8d 查看原来的内存子系统绑定的cgroup,会看到里面多了子cgroup, docker shell $ 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 可以看到dockercgroup里面的d79f22eb11d22c56a90f88e0aeb3cfda7cbe9639e2ab0e8532003a695e375e8dcgroup 正好是我们 刚才创建的容器 ID,那么看一下里面吧 ```shell $ 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 时,也能从心中了然它时怎么做到对容器使用资源的限制的。