2.2 Linux Cgroups介绍
2.1节讲述的是构建Linux容器的Namespace技术,它帮助进程隔离出自己单独的空间,但Docker是怎么限制每个空间的大小,保证它们不会互相争抢的呢?这就要用到Linux的Cgroups技术。
2.2.1 什么是Linux Cgroups
Linux Cgroups(Control Groups)提供了对一组进程及将来子进程的资源限制、控制和统计的能力,这些资源包括CPU、内存、存储、网络等。通过Cgroups,可以方便地限制某个进程的资源占用,并且可以实时地监控进程的监控和统计信息。
Cgroups中的3个组件
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_cls用于将cgroup中进程产生的网络包分类,以便Linux的tc(traffic controller)可以根据分类区分出来自某个cgroup的包并做限流或监控。
net_prio设置cgroup中进程产生的网络流量的优先级。
ns这个subsystem比较特殊,它的作用是使cgroup中的进程在新的Namespace中fork新进程(NEWNS)时,创建出一个新的cgroup,这个cgroup包含新的Namespace中的进程。
每个subsystem会关联到定义了相应限制的cgroup上,并对这个cgroup中的进程做相应的限制和控制。这些subsystem是逐步合并到内核中的,如何看到当前的内核支持哪些subsystem呢?可以安装cgroup的命令行工具(apt-get install cgroup-bin),然后通过lssubsys看到Kernel支持的subsystem。
hierarchy的功能是把一组cgroup串成一个树状的结构,一个这样的树便是一个hierarchy,通过这种树状结构,Cgroups可以做到继承。比如,系统对一组定时的任务进程通过cgroup1限制了CPU的使用率,然后其中有一个定时dump日志的进程还需要限制磁盘IO,为了避免限制了磁盘IO之后影响到其他进程,就可以创建cgroup2,使其继承于cgroup1并限制磁盘的IO,这样cgroup2便继承了cgroup1中对CPU使用率的限制,并且增加了磁盘IO的限制而不影响到cgroup1中的其他进程。
三个组件相互的关系
通过上面组件的描述,不难看出,Cgroups是凭借这三个组件的相互协作实现的。那么,这三个组件是什么关系呢?
系统在创建了新的hierarchy之后,系统中所有的进程都会加入这个hierarchy的cgroup根节点,这个cgroup根节点是hierarchy默认创建的,2.2.2小节在这个hierarchy中创建的cgroup都是这个cgroup根节点的子节点。
一个subsystem只能附加到一个hierarchy上面。
一个hierarchy可以附加多个subsystem。
一个进程可以作为多个cgroup的成员,但是这些cgroup必须在不同的hierarchy中。
一个进程fork出子进程时,子进程是和父进程在同一个cgroup中的,也可以根据需要将其移动到其他cgroup中。
这几句话现在不理解暂时没关系,在后面实际使用的过程中会逐渐了解到它们之间的联系的。
Kernel接口
前面介绍了那么多Cgroups结构的内容,那么到底要怎么调用Kernel才能配置Cgroups呢?通过前面的介绍了解到,Cgroups中的hierarchy是一种树状的组织结构,Kernel为了使对Cgroups的配置更直观,是通过一个虚拟的树状文件系统配置Cgroups的,通过层级的目录虚拟出cgroup树。下面,就以一个配置的例子来了解一下如何操作Cgroups。
1.首先,要创建并挂载一个hierarchy(cgroup树),如下。
这些文件就是这个hierarchy中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中。
2.然后,创建刚刚创建好的hierarchy上cgroup根节点中扩展出的两个子cgroup。
可以看到,在一个cgroup的目录下创建文件夹时,Kernel会把文件夹标记为这个cgroup的子cgroup,它们会继承父cgroup的属性。
3.在cgroup中添加和移动进程。
一个进程在一个Cgroups的hierarchy中,只能在一个cgroup节点上存在,系统的所有进程都会默认在根节点上存在,可以将进程移动到其他cgroup节点,只需要将进程ID写到移动到的cgroup节点的tasks文件中即可。
可以看到,当前的7475进程已经被加到cgroup-test:/cgroup-1中了。
4.通过subsystem限制cgroup中进程的资源。
在上面创建hierarchy的时候,这个hierarchy并没有关联到任何的subsystem,所以没办法通过那个hierarchy中的cgroup节点限制进程的资源占用,其实系统默认已经为每个subsystem创建了一个默认的hierarchy,比如memory的hierarchy。
可以看到,/sys/fs/cgroup/memory目录便是挂在了memory subsystem的hierarchy上。下面,就通过在这个hierarchy中创建cgroup,限制如下进程占用的内存。
本机为2GB内存,运行结果如下(通过top命令监控)。
可以看到,通过cgroup,我们成功地将stress进程的最大内存占用限制到了100MB。
2.2.2 Docker是如何使用Cgroups的
我们知道Docker是通过Cgroups实现容器资源限制和监控的,下面以一个实际的容器实例来看一下Docker是如何配置Cgroups的。
可以看到,Docker通过为每个容器创建cgroup,并通过cgroup去配置资源限制和资源监控。
2.2.3 用Go语言实现通过cgroup限制容器的资源
下面,在2.1节中Namespace容器demo的基础上,加上cgroup的限制,实现一个demo,使其能够具有限制容器内存的功能。
通过对Cgroups虚拟文件系统的配置,将容器中stress进程的内存占用限制到了100MB(宿主机内存2GB,5%即100MB)。