CGroups简介
CGroups是control groups的缩写,顾名思义就是把进程放到一个组里面统一加以控制,是Linux内核提供的一种可以限制、记录、隔离进程组(process groups)所使用的物理资源(如:cpu,memory,IO等等)的机制。CGroups为容器实现虚拟化提供了基本保证,是构建Docker等一系列虚拟化管理工具的基石。
CGroups提供了以下功能:
- 资源列表限制(Resource Limitation):CGroups可以对进程组使用的资源总额进行限制。如设定应用运行时使用内存的上限,一旦超过这个配额就发出OOM(Out of Memory)。
优先 - 列表级分配(Prioritization):通过分配的CPU时间片数量及硬盘IO带宽大小,实际上就相当于控制了进程运行的优先级。
资 - 列表源统计(Accounting): CGroups可以统计系统的资源使用量,如CPU使用时长、内存用量等等,这个功能非常适用于计费。
进程 - 列表控制(Control):CGroups可以对进程组执行挂起、恢复等操作。
CGroups相关概念与使用规则
CGroups相关概念
- 任务(task)。在CGroups中,任务就是系统的一个进程。
- 控制组(control group)。控制组就是一组按照某种标准划分的进程。CGroups中的资源控制都是以控制组为单位实现。一个进程可以加入到某个控制组,也从一个进程组迁移到另一个控制组。一个进程组的进程可以使用CGroups以控制组为单位分配的资源,同时受到CGroups以控制组为单位设定的限制。
- 层级(hierarchy)。控制组可以组织成hierarchical的形式,既一颗控制组树。控制组树上的子节点控制组是父节点控制组的孩子,继承父控制组的特定的属性。
- 子系统(subsytem)。一个子系统就是一个资源控制器,比如cpu子系统就是控制cpu时间分配的一个控制器。子系统必须附加(attach)到一个层级上才能起作用,一个子系统附加到某个层级以后,这个层级上的所有控制组都受到这个子系统的控制。
CGroups使用规则
CGroups、task、subsystem以及hierarchy四者间的相互关系及其使用规则:
- 一个子系统最多只能附加到一个层级,如果要附加到多个层级,当且仅当这些层级只有这唯一的子系统。
- 同一个层级可以附加一个或多个子系统。如下图,cpu和memory子系统附加到了一个层级。
- 一个任务可以是多个控制组的成员,但是这些控制组必须在不同的层级。在下图中可以看到,httpd进程已经加入到hierarchy A中的/cg1而不能加入同一个hierarchy中的/cg2,但是可以加入hierarchy B中的/cg3。实际上不允许加入同一个层级中的其他控制组也是为了防止出现矛盾,如CPU 子系统为/cg1分配了30%,而为/cg2分配了50%,此时如果httpd在这两个控制组中,就会出现矛盾。
- 系统中的进程(任务)创建子进程(任务)时,该子任务自动成为其父进程所在控制组的成员。然后可根据需要将该子任务移动到不同的控制组中,但开始时它总是继承其父任务的控制组。
CGroups使用方法简介
CGroups的用户界面以一个伪文件系统的方式实现,即用户可以通过文件操作实现CGroups的组织管理。
- CGroups v1初始系统会创建一个层级cgroup,如果没有的话可以手动创建,创建好之后可以使用df命令查看:
mount -t tmpfs cgroups /sys/fs/cgroup
[root@localhost cgroup]# df -h
Filesystem Size Used Avail Use% Mounted on
devtmpfs 478M 0 478M 0% /dev
tmpfs 489M 0 489M 0% /sys/fs/cgroup
- 层级创建好之后,需要创建控制组,使用mkdir命令,此控制器使用blkio子系统
$ [root@localhost cgroup]# mkdir -p /sys/fs/cgroup/blkio/g1
- 接着需要设定控制组的参数限制,对于blkio子系统,设定之前需要先查找设备的major和minor版本号:
[root@localhost cgroup]# cat /proc/partitions
major minor #blocks name
8 0 20971520 sda
8 1 1048576 sda1
8 2 19921920 sda2
- 最后需要验证下blkio的限制是否生效,使用dd命令写文件,并使用iostat查看实时速率:
[root@localhost blkio]# dd if=/dev/zero of=/tmp/file1 bs=512M count=1 oflag=direct
[root@localhost ~]# iostat 1 -d -h -y -k -p sda
Linux 3.10.0-514.el7.x86_64 (localhost.localdomain) 11/10/2017 _x86_64_ (1 CPU)
Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
sda 2.06 0.00 1055.67 0 1024
sda1 0.00 0.00 0.00 0 0
sda2 2.06 0.00 1055.67 0 1024
Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
sda 2.11 0.00 1077.89 0 1024
sda1 0.00 0.00 0.00 0 0
sda2 2.11 0.00 1077.89 0 1024
可以看到,对磁盘的写速率被限制在1MB/s,说明限制生效了。注意:此处dd命令使用“oflag=direct”,没有使用默认的bufferIO方式,下面会解释原因。
CGroups子系统介绍
子系统(subsyste)实际上就是CGroups的资源控制系统,每种子系统独立地控制一种资源,CGroups主要有以下几种子系统:
- blkio:这个子系统可以为块设备设定输入/输出限制,比如物理驱动设备(包括磁盘、固态硬盘、USB等)。
- cpu:这个子系统使用调度程序控制task对CPU的使用。
- cpuacct:这个子系统自动生成CGroups中task对CPU资源使用情况的报告。
- cpuset:这个子系统可以为CGroups中的task分配独立的CPU(针对多处理器系统)和内存。
- devices:这个子系统可以开启或关闭CGroups中task对设备的访问。
- freezer:这个子系统可以挂起或恢复CGroups中的task。
- memory:这个子系统可以设定CGroups中task对内存使用量的限定,并且自动生成这些task对内存资源使用情况的报告。
- perfevent:这个子系统使得CGroups中的task可以进行统一的性能测试。
- net_cls:这个子系统使用等级识别符(classid)标记网络数据包,从而允许 Linux 流量控制程序(TC:Traffic Controller)识别从具体CGroups中生成的数据包。
CGrooups V1的不足
1. 多层级(hierarchy)设计导致进程的管理较为混乱
CGroups v1为了提供灵活性,允许进程可以属于多个层级的不同控制组,但实际上,这种多层级除了增加代码的复杂度和理解困难外,并没有太大用处,因为子系统只能属于一个层级。一方面,跟踪进程所有controller变得复杂;另外,各个controller之间也很难协同工作(因为controller可能属于不同的hierarchy)。所以,在实际使用中,通常是每个层级一个controller。
2. blkio子系统无法对buffer IO进行限制
CGroups v1的blkio子系统只支持sync IO和direct IO,对于buffer IO,由于内核先将数据拷贝到page cache,然后再由后台内核线程定期刷到磁盘,因此对于控制层而言所有的blockIO操作都是内核flush线程发起的,导致无法进行IO限制。
此问题在4.2版本的内核中被CGroups的维护人员以补丁修复。
3. 内存子系统资源限制手段比较粗犷
CGroups v1内存控制器直接基于设定的资源使用阈值进行控制,而且比较粗犷,比如,memory子系统,根据memory.oom_control的设置,当任务使用的内存超过界限是,只有结束任务(默认动作)或者任务挂起两种动作。这种模式当有些任务的资源需求会出现峰值的时候影响比较大。
4. 事件通知机制不完善
CGroups v1每次进行事件通知都是通过fork和exec一个用户层帮助程序(userland helper binary)完成的,开销相对较大。这种设计也导致在内核内部的事件传递过滤机制更加复杂。
5. 控制器之间行为不一致、接口不统一
CGroups v1当新创建一个控制组的时候,有些控制器默认不会新增额外的限制,而有一些控制器则禁止任何资源的申请,需要用户手工配置。此外,不同控制器之间相同类型的控制模块命名方式、格式和单位很多都不一致,同样问题在统计和信息模块也存在,同时不同控制器的API比较混乱。
6. 基于线程的控制粒度
CGroups v1允许一个进程的各个线程属于不同的控制组,这种设计表明上增加了灵活性,但这和多层级的设计类似,除了增加操作、接口和代码实现的复杂度之外,实际上并没有什么用处。此外,如果一个进程的不同线程分属于同一层级的不同控制组,那么会引入资源竞争的问题,而且多数子系统对于这种行为是没有定义的,因此如果用户真的那么用了,那结果就不可知了。
CGroups V2
基于v1上述一些不足,CGroups的维护人员重新设计并开发了CGroups v2,并在2016年3月合入了linux内核4.5版本, v2的主要设计思想有以下三点:
- 单一层级代替多层级
- 基于进程粒度进行控制,而不是线程
- 聚焦于简单、清晰的使用与实现,而不是“极限的”灵活性
CGroups V2使用方法简介
还是以对shell进程的磁盘写操作进行速率限制为例,简单介绍下CGroups v2的使用方法,需要使用linux 内核4.5以上的版本。
- CGroups v2只有单一层级,因此不需要创建层级,直接在根控制组下创建控制组就可以:
[root@localhost]# mkdir /cgroup2/cg2
- 为了cg2控制组能够块设备的io,需要修改cgroup2的“cgroup.subtree_control”文件,在其中加入“+io”,表示允许控制组使用blkio子系统,修改之后查看cg2的”cgroup.controllers”文件,可以看到”io”:
[root@localhost]# echo "+io" > /cgroup2/cgroup.subtree_control
[root@localhost]# cat /cgroup2/cg2/cgroup.controllers
io
- 修改”io.max”文件,增加速率限制,任务就是当前的shell进程:
[root@localhost cg2]# echo "8:0 wbps=1048576" > io.max
[root@localhost]# echo $$ > /cgroup2/cg2/cgroup.procs
- 还是使用dd命令验证,现在不需要使用“oflag=direct”也可以生效了:
[root@localhost blkio]# dd if=/dev/zero of=/tmp/file1 bs=512M count=1
[root@localhost ~]# iostat 1 -d -h -y -k -p sda
Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
sda 3.12 0.00 1070.83 0 1028
sda1 0.00 0.00 0.00 0 0
sda2 3.12 0.00 1070.83 0 1028
Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
sda 2.11 0.00 1077.89 0 1024
sda1 0.00 0.00 0.00 0 0
sda2 2.11 0.00 1077.89 0 1024
CGroups V2的改进
下面将结合CGroups v1的不足介绍下v2的一些改进。
- CGroups v2使用单一层级,简化了管理和实现,同时使用的方式和规则也发生了变化。
每个控制组都有一个cgroup.controllers文件,列出子group可以开启的控制器。另外,还有一个 cgroup.subtree_control文件,用于控制开启/关闭子group的controller。
对照此图:
- a) 控制组对子控制组要么都开启,要么都不开启某个controller。比如,如果对B的cgroup.subtree_control写入memory controller,那么就会同时应用到C和D。
- b) 只有父控制组开启了controller,子控制组才能开启某个controller。比如,只有B开启了memory,C才能开启memory。但关闭不受这个限制,B开启了memory,C可以关闭。 但是,子控制组并不会自动开启父控制组的所有controller,需要手动去开启。
- c) 只有不包含task的控制组才能通过cgroup.subtree_control开启controller,根控制组除外。也就是说,只有叶子控制组才能对进程进行资源控制。例如,B不能包含进程信息。 这样做主要是为了防止父控制组中的进程与子控制组的进程发生资源竞争,而且关系不明确。
再举一个具体的例子,下面两张图就是分别对进程A(使用blkio和memory)、进程B(使用memory和pids)进行资源限制在v1和v2下的实现结构。
CGroups v1:
CGroups v2:
从图中可以看出,v2使得整个cgroup系统成为一棵树(而不是之前的多个树),而且所有进程的资源控制都在叶子控制组进行。这种单层级的方式很好的分开了两个事,一个是进程的分组,一个是对分组的资源控制。这样的模型更加清晰,因为cgroup代表的就是进程分组,子系统就是用于资源控制。
- 为了解决blkio子系统buffer IO页面缓存回写的问题,CGroups v2为使用blkio的控制组分配一个inode,并且将从这个inode的IO请求都算到这个控制组上,从而实现IO的限制。需要注意的是这个需要文件系统支持,目前支持的文件系统为ext2,ext4和 btrfs。
- CGroups v2优化了内存资源限制的手段,内存子系统在资源超限时会采用一些“调解的”手段,比如在有些情况下,会扩充“memory.high”的值,允许突破限制,而且不会再杀死内存超限的任务,这样对于一些有临时内存峰值的进程处理起来更加简单。
- CGroups v2为每一个非根控制组(non-root cgroup)增加了一个"cgroup.events"文件,用于集中的事件管理,并且保留了v1支持的eventfd()方式的事件通知。相对于v1,减少了大量的系统调用,并且可以使用一个进程来监控所有的事件。
- CGroups v2统一了控制器的行为,并且为控制器建立了统一的更新协议,这样控制器们可以提供最小化的并且一致的接口,简化了业务的使用。
- CGroups v2基于进程进行资源限制,不继续使用线程的粒度进行控制的原因如上文所说,基于线程的控制实际上并没有什么使用场景,而且增加了特性的复杂度,同时引入了特定场景资源竞争的问题。
总结
CGroups v2对控制组、层级和子系统的关系和使用规则重新进行了设计,并修复了一些v1版本的不足。单一层级的设计,基于进程的控制粒度以及简单、清晰不过分灵活的设计思路,都使得cgroup在向更简单、更实用的方向发展。
参考文档
Documentation/cgroup-v2.txt
Using cgroups to limit I/O
Docker背后的内核知识——cgroups资源限制
Merge branch 'for-4.5' of git://git.kernel.org/pub/scm/linux/kernel/git/tj/cgroup
下一代Cgroup——unified hierarchy