docker 深入理解之cgroups
cgroups 资源限制
cgroups 是什么
cgroups 最初名为process container,有Google工程师Paul Menage和Rohit Seth于 2006 年提出,后由域container有多重含义容易引起误会,就与2007年更名为control groups, 并整合进Linux内核,顾名思义就是把任务放到一个组里面同一管理,cgroups 是Linux内核提供方的一种机制,这种机制可以根据需要把一系列统一任务极其子任务整合(或分隔)到按资源划分等级不同组内,从而为系统资源管理提供一个统一的框架。
cgroups 的四个特点
cgroups的API以一个伪文件系统的方式实现,用户态的程序可以通过文件操作实现cgroups 的组织
cgroups的组织管理操作单元可以细粒到线程级别,另外用户可以创建销毁cgroups,从而实现资源再分配管理
所有资源管理的功能都以子系统方式实现,接口统一
子任务创建之初与其父进程处于同一个cgroups的控制组
cgroups 的作用
实现成功cgroups的主要目的是为不同用户层面的资源管理,提供一个统一化的接口。从单个方面的资源控制到操作系统层面的虚拟化,cgroups提供了四大功能
资源限制 | cgroups可以对任务使用的资源总额进行限制,如设定应用运行时使用的内存上限,一但超过这个配额就发出OOM提示 |
优先级分配 | 通过分配的CPU时间片数量及磁盘IO带宽大小,实际就相当于控制了任务运行的优先级 |
资源统计 | cgroups 可以统计系统的资源使用量,如CPU时长,内存使用量,这个功能非常适用于计费 |
任务控制 | cgroups可以对任务执行挂起 |
过去有一段时间,内核开发者甚至把namespace也作为一个cgroups的子系统加入进来,也就是说cgroups曾经甚至还包括了资源隔离的能力。但资源隔离会给cgroups带来许多问题,如pid namespace加入后,PID在循环出现的时候,cgroups会出现命名冲突、cgroups创建后进入新的namespace导致其他子系统脱离了控制,所以2011年就被移除了
cgroups 术语表
task(任务) | 在cgroups的术语中,任务表示系统的一个进程或者线程 |
cgroups(控制组) | cgroups中的资源控制都以cgroups以单位实现,cgroups表示按某种资源控制标准划分而成的任务组,包含一个或多个子系统,一个任务加入某个cgroups,也可以从某个cgroups签到另一cgroups |
subsystem(子系统) | cgroups中的子系统就是一个资源调度控制器。CPU子系统可以控制从CPU时间分配,内存子系统可以设置cgroups的内存使用量 |
hierarchy(层级) | 层级由一系列cgroups以一个树状结构排列而成,每个层级通过绑定对应的子系统进行资源控制。层级中cgroups的节点可以包含零个节点或多个子节点,子节点继承父节点挂载的子系统。整个操作系统可以有多个层级 |
组织结构与基本规则
传统的Unix任务管理,实际上是先启动init任务作为根节点,在有init创建子任务作为子节点,而每个子节点又可以创建新的子节点,如此往复,形成一个树状结构。而系统中多个cgroups系统也构成类似的树状结构,子节点从父节点继承属性
它们最大的不同在于,系统中的多个cgroups构成的层级并非单根结构,可以允许存在多个。如果任务模型是由init作为根节点构成的一棵树,那么系统中多个cgroups则是由多个层级构成的树林。这样做的目的很好理解,如果只有一个层级,那么所有子任务都将被迫邦定其的所有子系统,这会给某些任务造成不必要的限制。docker中,每个子系统独立构成一个层级,这样易于管理
*规则1:同一个层级可以附加一个或多个子系统,CPU和memory的子系统附加到一个层级
*规则2:一个子系统可以附加到多个层级,当且仅当目标层级只有唯一一个子系统时,小圈中的数字表示子系统附加的时间顺序,CPU子系统附加到层级A的同时不能再附加到层级B,因为层级B已经附加了内存子系统。如果层级B没有过内存子系统,那么CPU子系统同时附加到两个层级是允许的
*规则3:系统每次新建一个层级时,该系统上的所有任务默认加入这个新建的层级的初始化cgroups,这个cgroups也被称为root cgroups。对于创建的每个层级,任务只能存在于其中一个cgroups中,即一个子任务不能存在于同一个层级的不同cgroups中,但一个任务可以存在于可以存在于不同层级的多个cgroups中,如果操作时把一个任务添加到同一个层级中的一个cgroups中,则会将它从第一个cgroups中移除。下面httpd任务已经加入到层级A中的/cg1,而不能加入到同时加入同一个层级中/cg2.但是可以加入层级B中的/cg3
规则4:任务在fork/clone 自身时创建的子任务默认与原来任务在同一个cgroups中,但是子系统允许被移动到不同的cgroups中,即fork/clone 完成后,父子进程在cgroups方面是互不影响的。小圈中数字表示任务出现的时间顺序,当httpd刚fork出另一个httpd时两者在同一个层级中的cgroups中,但随着ID变成4840的httpd需要移到其他cgroups也是可以的,因为父任务间已经独立。一句话就是,初始化子任务时与父任务在同一个cgroups,但是这种关系随后可以改变
子系统介绍
子系统实际是cgroups的资源控制系统,每种子系统独立的控制一种资源,目前docker使用9中子系统,其中net_cls子系统在内核中已经广泛实现,docket尚未采用
blkio | 可以为快设备设定输入\输出限制,如物理驱动设备(磁盘、固态硬盘、USB等) |
CPU | 使用调度程序控制任务对CPU的使用 |
cpuacct | 自动生成cgroups中任务对cpu资源使用情况报告 |
cpuset | 可以为cgroups中的任务分配独立的CPU(针对多处理器系统)和内存 |
devices | 可以开启或者关闭cgroups中任务对设备的访问 |
freezer | 可以挂起或恢复cgroups中任务 |
memory | 可以设定cgroups中任务对内存使用量的限定,并且自动生成这些任务对内存资源的使用情况报告 |
perf_event | 使用后使cgroups的任务可以进行统一的性能测试 |
net_cls | docker没有直接使用它,它通过使用等级识别符标记网络数据包,从而允许Linux流量控制程序识别从具体cgroups中生成的数据包 |
子系统如何使用很重要,但docker并没有对cgroups本身做曾强,容器用户一般已不需要直接操作cgroups,Linux中cgroups的实现形式为一个文件系统,因此需要mount这个文件系统才能使用,挂载成功后,就能看到子系统
[root@mast ~]# mount -t cgroup cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd) cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,pids) cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,net_prio,net_cls) cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,devices) cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,perf_event) cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuacct,cpu) cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,hugetlb) cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,memory) cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuset) cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,freezer) cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,blkio)
查看挂载CPU子系统的控制组下的文件
[root@mast ~]# ls /sys/fs/cgroup/cpu cgroup.clone_children cgroup.procs cpuacct.stat cpuacct.usage_percpu cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat release_agent cgroup.event_control cgroup.sane_behavior cpuacct.usage cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release tasks
在/sys/fs/cgroup的CPU的子目录下创建控制组,控制组目录创建后,查看目录
[root@mast ~]# cd /sys/fs/cgroup/cpu [root@mast cpu]# mkdir cg1 [root@mast cpu]# ls cg1/ cgroup.clone_children cgroup.procs cpuacct.usage cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release cgroup.event_control cpuacct.stat cpuacct.usage_percpu cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks
设置PID为18828进程cup使用配额
[root@mast cpu]# echo 1404 >> /sys/fs/cgroup/cpu/cg1/tasks [root@mast cpu]# echo 20000 > /sys/fs/cgroup/cpu/cg1/cpu.cfs_quota_us
cgroups 实现方式及工作原理
cgroups的实现本质上是给任务挂钩子,当任务运行的过程中涉及某种资源时,就会触发钩子上所附带的子系统进行检测,根据资源类别不同,使用对应的技术进行资源限制和优先级分配
cgroups如果判断资源超限及超出后的措施
对于不的系统资源,cgroups提供统一的接口操作对资源进行控制和统计,但限制具体方式不同。内存子系统,会在描述内存状态的“mm struct” 结构体中记录它所属cgroups,当进程需要更多内存时,就会触发cgroups用量检测,用量超过cgroups规定的限额,则拒绝用户的内存申请,否则就给予相应内存并记录在cgroups的统计信息中,实际要比描述的复杂的多,不仅考虑内存的分配与回收,还需考虑不同类型的内存cache和swap等
进程需要所需要的内存超出cgroups最大限额以后,如果设置了OOM Control(内存超额控制),那么进程就手动OOM信号结束;否则进程就会被挂起,进入睡眠状态,直到cgroups中其他进程释放足够的内存资源位置。docker中默认是开启OOM Control的,其他子系统的实现于此类似,cgroups提供了多种资源限制的策略供用户选择。
cgroups与任务之间的关联关系
实际上,cgroups与任务之间是多对多的关系,所以它们不是直接关联,而是通过一个中间结构把双向的关联信息记录下来。每个任务结构体task_struct中都包含一个指针,可以查询到对应的cgroups的情况,同时也可以查到各个子系统状态,这些子系统状态也包含了找到任务的指针,不同类型子系统按照定义本身可以控制信息结构体,最终在自定c义的结构体中把子系统状态指针包含进去,然后通过内核container_of(这个宏可以通过一个结构体的成员找到结构自身)等宏定义来获取对应的结构体,关联到任务,以此达到资源限制的目的。同时为了让cgroups便于让用户理解,也为精简内核代码为cgroups提供熟悉的权限和命名空间管理。内核开发者按照Linux虚拟文件系统转换器(VFS)接口实现统一套名为cgroups的文件系统,非常巧妙的表示cgroups的层级概念,把各个子系统的实现都封装到文件系统的各项操作中。
docker在使用cgroups时注意事项
在实际使用的过程中,docker需要挂载cgroups文件系统新建一个层级,挂载时指定要绑定的子系统。把cgroups文件系统挂载上以后,就可以像操作文件一样对cgroups的层级进行浏览和操作管理(包括权限管理、子系统管理等)。处理cgroups文件系统之外,内核没有为cgroups的访问和操作添加任何系统调用
如果新建的层级结构要绑定子系统与目前已经存在的层级结构完全相同,那么新挂载的会重用已经存在-的那套(指向相同的css_set).否则,如果要绑定的子系统已经被别层级绑定,就会返回挂载失败的错误,如果一切顺利,挂载完成后层级就被激活并与相应的子系统关联起来,可以开始使用了
目前无法讲一个子系统绑定到激活的层级上,或者从一个激活的层级中解除某个子系统的绑定
当一个顶级的cgroups被卸载(umount)时,如果其中创建过深层次的后代cgroups目录,那么上一层的cgroups被卸载,层级也是激活的状态,其后代cgroups的配置依旧有效。只有递归式卸载层级中的所有cgroups,那个层级真正被删除
在创建的层级中创建文件夹,类似于fork了一个子cgroups,子cgroups中默继承父cgroups中的配置属性,但可以根据需求对配置参数进行属性,这样就把一个大的cgroups系统切割成一个个嵌套的、动态变化的“软分区”。
/sys/fs/cgroups/cpu/docker/<container-ID>下文件的作用
一个cgroups创建完成后,不管绑定任何子系统,其目录下都会生成几个文件,用来cgroups的相应信息。
tasks:文件中罗列了所有在该cgroups中任务的TID,即所有进程或者线程的ID,该文件并不保证任务的TID有序,把一个任务的TID写到这个文件里就意味着把这个任务加入到cgroups中,如果这个任务所在的任务组与其不在同一个cgroups中,那么会在cgroups.procs文件里记录一个该文件所在任务组的TGID值,但是该任务组的其他任务并不影响。
cgroups.procs:这个文件罗列出所有在该cgroups中的TGID(线程组ID),即线程组中第一个进程的PID。该文件并保证TGID有序或无序重复,写一个TGID到这个文件意味着就意味着把与其相关的线程加入到这个cgroups中
notify_on_release:填0或1 ,表示是否在cgroups中最后一个任务退出后通知运行releaseagent,默认是0,表示不运行
release_agebt: 指定release agent 执行脚本的文件路径,这个脚本通常用于自动化卸载无用的cgroups