在Linux中,使用Namespace技术帮助进程隔离出自己单独的空间,但怎么对这个空间进行限制?比如限制进程申请的资源大小等等,这就要用到Linux的Cgroups技术。
Linux Cgroups (Control Groups )提供了对一组进程及将来子进程的资源限制、控制和统计的能力,这些资源包括 CPU、内存、存储、网络等 通过 Cgroups ,可以方便地限制某个进程的资源占用,并且可以实时地监控进程的监控和统计信息。
3个组件
Cgroups只是个技术名称,它由三个组件组成,分别为cgroup、subsystem、hierarchy。
cgroup 是对进程分组管理的一种机制,一个cgroup包含一
组进程,并可以在这个cgroup上增加Linux subsystem的各种参数配置,将一组进程和一组subsystem的系统参数关联起来。subsystem 是一组资源空间模块,就像上面说的,用于资源限制,一般包含如下几项:
- blkio 设置对块设备(比如硬盘)输入输出的访问控制。
- cpu 设置 cgroup 中进程的 CPU 被调度的策略。
- cpuacct 可以统计 cgroup 中进程的 CPU 占用。
- cpuset 在多核机器上设置 cgroup 中进程可以使用的 CPU 和内存(此处内存仅使用于
NUMA 架构)。 - devices 控制 cgroup 中进程对设备的访问。
- freezer 用于挂起( suspend )和恢复( resume) cgroup 中的进程。
- memory 用于控制 cgroup 中进程的内存占用。
- net_els 用于将 cgroup 中进程产生的网络包分类,以便 Linux tc (traffic con oller )可
以根据分类区分出来自某个 cgroup 的包并做限流或监控。 - net_prio 设置 cgroup 中进程产生的网络流量的优先级。
- ns 这个 subsystem 比较特殊,它的作用是使 cgroup 中的进程在新的 Namespace fork
新进程 CNEWNS )时,创建出 个新的 cgroup ,这个 cgroup 包含新的 Namespace
的进程。
每个subsystem会关联到定义了相应限制的cgroup上,并对这个cgroup中的进程做相应的限制和控制。这些subsystem是逐步合并到内核中的。可以使用
lssubsys查看当前内核支持的subsystem:[root@master cgroup-test]$ lssubsys cpuset cpu,cpuacct memory devices freezer net_cls,net_prio blkio perf_event hugetlb pidshierarchy 的功能是把一组cgroup串成一个
树状的结构,一个这样的树便是一个hierarchy。通过这种树状结构,Cgroups可以做到继承。比如,系统对一组定时的任务进程通过 cgroupl 限制了 CPU 的使用率,然后其中有一个定时 dump 日志的进程还需要限制磁盘 IO ,为了避免限制了磁盘 IO 之后影响到其他进程,就可以创建 cgroup2 ,使其继承于 cgroupl 井限制磁盘的 IO ,这样 cgroup2 便继承了 cgroupl 中对 CPU 使用率的限制,并且增加了磁盘 IO 的限制而不影响到 cgroupl 中的其他进程。也就是说,hierarchy是一组cgroup组成的树。
组件之间的关系
不难看出,Cgroups是通过上面三个组件的相互协作实现的,他们的关系如下:
- 在创建了新的hierarchy之后,系统中所有的进程都会加入这个hierarchy的根cgroup中,这个cgroup是hierarchy默认创建的。对,没听错,所有的进程都会加进去,之后创建的进程也会,哪怕仅仅是用户随便创了个hierarchy玩。
- 一个subsystem只能附加到一个hierarchy上面,用于限制其下进程的资源使用。
- 一个hierarchy可以附加多个subsystem。
- 一个进程可以作为多个cgroup的成员(不然创建hierarchy时就无法把所有进程加进去)。但是,这些cgroup必须在不同的hierarchy中。
- 一个进程fork出子进程时,子进程是和父进程在同一个cgroup中的,也可以根据需要将其移动到其他cgroup中。
实验
首先,创建并挂载一个hierarchy,这里需要用mount操作,如下:
# 创建一个hierarchy挂载点
[root@master sakura]$ mkdir cgroup-test
# 挂载该hierarchy
[root@master sakura]$ sudo mount -t cgroup -o none,name=cgroup-test cgroup-test ./cgroup-test
# 该目录就成了hierarchy的根cgroup
[root@master sakura]$ ls ./cgroup-test/
cgroup.clone_children cgroup.event_control cgroup.procs cgroup.sane_behavior notify_on_release release_agent tasks
可以看到,当指定挂载的filesystem类型为cgroup后,目录下多个很多文件。这些文件就是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.procs一个记录了所以进程组的ID,tasks中记录了所有进程的ID:
[root@master cgroup-test]$ cat tasks
1
# ...当前所有进程ID
然后,在上述根cgroup中创建两个子cgroup,就是两个目录:
[root@master cgroup-test]$ mkdir cgroup1
[root@master cgroup-test]$ mkdir cgroup2
[root@master cgroup-test]$ tree
[root@master cgroup-test]# tree
.
├── cgroup1
│ ├── cgroup.clone_children
│ ├── cgroup.event_control
│ ├── cgroup.procs
│ ├── notify_on_release
│ └── tasks
├── cgroup2
│ ├── cgroup.clone_children
│ ├── cgroup.event_control
│ ├── cgroup.procs
│ ├── notify_on_release
│ └── tasks
├── cgroup.clone_children
├── cgroup.event_control
├── cgroup.procs
├── cgroup.sane_behavior
├── notify_on_release
├── release_agent
└── tasks
2 directories, 17 files
可以看到,在一个cgroup的目录下创建文件时,内核会把文件夹标记为这个cgroup的子cgroup,它们会继承父cgroup的属性。
一个进程在hierarchy中,只能在一个cgroup节点上存在,系统的所有进程都会默认在根cgroup上存在,可以将进程移动到其他cgroup上,只需要将进程ID写到对应的cgroup的tasks文件中即可。
[sakura@master cgroup-test]$ cd cgroup1
[sakura@master cgroup1]$ cat ../tasks | grep $$
101308
[sakura@master cgroup1]$ sudo sh -c "echo $$ >> tasks"
[sudo] sakura 的密码:
[sakura@master cgroup1]$ cat ../tasks | grep $$
[sakura@master cgroup1]$ cat ./tasks | grep $$
101308
[sakura@master cgroup1]$ cat /proc/$$/cgroup
23:name=cgroup-test:/cgroup1
11:freezer:/
10:pids:/user.slice
9:cpuset:/
8:memory:/user.slice
7:cpuacct,cpu:/user.slice
6:hugetlb:/
5:devices:/user.slice
4:blkio:/user.slice
3:perf_event:/
2:net_prio,net_cls:/
1:name=systemd:/user.slice/user-1000.slice/session-1.scope
可以看到,当进程ID被写入hierarchy中另一个cgroup的tasks中时,它会自动的从原cgroup的tasks中移除。通过最后一条命令可知,一个进程可以属于多个cgroup,但是cgroup必须位于不同的hierarchy。
但是到此为止,这个hierarchy并没有关联到任何的subsystem,所以没办法通过其中的cgroup来限制进程的资源占用。
实际上,系统已经为每个subsystem创建好了一个默认的hierarchy,位于/sys/fs/cgroup/下:
[root@master cgroup-test]$ ls /sys/fs/cgroup
blkio cpuacct cpuset freezer memory net_cls,net_prio perf_event systemd
cpu cpu,cpuacct devices hugetlb net_cls net_prio pids
或者用另外一种方法查看:
[root@master cgroup-test]$ mount | grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup-test on /home/sakura/cgroup-test type cgroup (rw,relatime,name=cgroup-test)
以memory举例,/sys/fs/cgroup/memory目录便是他的hierarchy的根cgroup。
[root@master memory]# ls
cgroup.clone_children memory.kmem.max_usage_in_bytes memory.memsw.limit_in_bytes memory.usage_in_bytes
cgroup.event_control memory.kmem.slabinfo memory.memsw.max_usage_in_bytes memory.use_hierarchy
cgroup.procs memory.kmem.tcp.failcnt memory.memsw.usage_in_bytes notify_on_release
cgroup.sane_behavior memory.kmem.tcp.limit_in_bytes memory.move_charge_at_immigrate release_agent
kubepods.slice memory.kmem.tcp.max_usage_in_bytes memory.numa_stat system.slice
machine.slice memory.kmem.tcp.usage_in_bytes memory.oom_control tasks
memory.failcnt memory.kmem.usage_in_bytes memory.pressure_level test
memory.force_empty memory.limit_in_bytes memory.soft_limit_in_bytes user.slice
memory.kmem.failcnt memory.max_usage_in_bytes memory.stat
memory.kmem.limit_in_bytes memory.memsw.failcnt memory.swappiness
不同于不做限制的hierarchy,附加了subsystem的hierarchy下会多出很多相关的配置文件,不同的subsystem有所不同。
接下来,在其中创建一个子cgroup,名为test:
[root@master memory]$ cd test
[root@master test]$ ls
cgroup.clone_children memory.kmem.slabinfo memory.memsw.failcnt memory.soft_limit_in_bytes
cgroup.event_control memory.kmem.tcp.failcnt memory.memsw.limit_in_bytes memory.stat
cgroup.procs memory.kmem.tcp.limit_in_bytes memory.memsw.max_usage_in_bytes memory.swappiness
memory.failcnt memory.kmem.tcp.max_usage_in_bytes memory.memsw.usage_in_bytes memory.usage_in_bytes
memory.force_empty memory.kmem.tcp.usage_in_bytes memory.move_charge_at_immigrate memory.use_hierarchy
memory.kmem.failcnt memory.kmem.usage_in_bytes memory.numa_stat notify_on_release
memory.kmem.limit_in_bytes memory.limit_in_bytes memory.oom_control tasks
memory.kmem.max_usage_in_bytes memory.max_usage_in_bytes memory.pressure_level
创建完毕后,然后在 memory.limit_in_bytes中限制该cgroup中的进程最大只能占用100M的内存,然后在该cgroup下启动一个占用200M的进程,查看情况。注意,当前的bash进程并不属于该cgroup,而stree进程是bash的子进程,所以要先把bash进程移入该cgroup,stree自然就进去了。
# 内存使用限制在100M
[root@master test]$ sudo sh -c "echo "100m" > memory.limit_in_bytes"
# 将当前bash进程加入该cgroup
[root@master test]$ sudo sh -c "echo $$ > tasks"
# 压力测试, 申请占用200M
[root@master test]$ stress --vm-bytes 200m --vm-keep -m 1
stress: info: [31547] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL: [31547] (415) <-- worker 31548 got signal 9
stress: WARN: [31547] (417) now reaping child worker processes
stress: FAIL: [31547] (451) failed run completed in 0s
# 重新测试,申请内存少于100M
[root@master test]$ stress --vm-bytes 99.99m --vm-keep -m 1
stress: info: [31840] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
#.. 成功
可以看到,当通过memory这一subsystem将cgroup下的进程占用内存限制在100M以内后,所有需要100M内存以上的进程都无法在该cgroup下运行,达到资源限制的目的。