Cgroups初探

简单整理了一下cgroups

cgroups(Control Groups)可以限制、记录、隔离进程组使用的物理资源(包括CPU、memory、IO等),本质上是内核附加在程序上的一系列钩子(hooks),通过程序运行时对资源的调度触发相应的钩子以达到资源追踪和限制的目的。

cgroups关键术语:

  • task(任务): task表示系统的一个进程。
  • subsystem(子系统): subsystem就是一个资源调度控制器,比如CPU子系统控制CPU时间分配,内存子系统分配内存使用量。
  • cgroup(控制组): cgroups中的资源控制都以cgroup为单位实现,cgroup表示按照某种资源控制标准划分而成的任务组,包含一个或多个子系统。一个task可以加入一个cgroup,也可以从一个移到另外一个。
  • hierarchy(层级树): 一系列cgroup构成的树状结构的排列就是一个hierarchy,每个hierarchy通过绑定subsystem进行资源调度。整个系统可以有多个hierarchy。

cgroups特点:

  1. cgroups的API以伪文件系统的方式实现,用户可以通过文件操作实现对cgroups的管理。
  2. cgroups的管理操作单元细粒度可以达到线程级别,用户态的代码也可以针对系统分配的资源创建和销毁cgroups,然后实现资源的再分配和管理。
  3. 所有资源管理的功能都以subsystem的方式实现,接口统一。
  4. 刚创建的子进程和父进程处于同一个控制组。

cgroups四大功能:

  1. 资源限制:对进程组使用的资源总额进行限制。如内存超过配额就发出OOM(Out of Memory)。
  2. 优先级分配:通过分配的CPU时间片数量及硬盘IO带宽大小来实现,实际上就相当于控制了进程运行的优先级。
  3. 资源统计:统计系统的资源使用量,如CPU使用时长、内存使用量等等。
  4. 进程控制:可以对进程组执行挂起、恢复等操作。

组织结构:

类似于namespace,传统的进程都是先启动init进程作为根节点,再由init节点创建子进程作为子节点,而每个子节点都可以创建新的子节点,如此往复,形成一个树状结构。而cgroups也是类似的树状结构,子节点从父节点继承属性。但是,它们不同点在于cgroups中与init地位类似的hierarchy可以允许存在多个,这样就会形成一个森林。

基本规则:

  1. 同一个hierarchy可以附加一个或多个subsystem。
  2. 一个已经附加在某个hierarchy上的subsystem不能附加到其他含有别的subsystem的hierarchy上。
  3. 一个task不能属于同一个hierarchy的不同cgroup。
  4. 刚fork出的子进程在初始状态与其父进程处于同一个cgroup。

Subsystem简介及其与docker相关的配置参数:

subsystem实际上就是cgroups的资源控制系统,每种subsystem独立控制一种资源,主要有以下九种:

1、blkio:为块设备设定输入\输出限制,比如物理驱动(包括磁盘、固态硬盘、USB等)。

(1) 限额类参数:限额类主要有两种策略,一种是基于完全公平队列调度(CFQ:Completely Fair Queuing)的按权重分配各个cgroup所能占用总体资源的百分比,当资源空闲时可以充分利用,但只能用于最底层节点cgroup的配置;另一种是设定资源使用上限,这种限额在各个层次的cgroup都可以配置,但这种限制较为生硬,并且容器之间依然会出现资源的竞争。

按比例分配块设备IO资源:

  • blkio.weight:设定100-1000的一个整数值,作为分配设备时的相对权重比率。
  • blkio.weight_device:针对特定设备的权重比。

控制IO读写速度上限:

  • blkio.throttle.read_bps_device:设定每秒读取块设备的数据量上限。
  • blkio.throttle.write_bps_device:设定每秒写入块设备的数据量上限。
  • blkio.throttle.read_iops_device:设定每秒读操作次数上限。
  • blkio.throttle.write_iops_device:设定每秒写操作次数上限。

(2)统计与监控:以下的参数都是只读的状态报告,用于更好的统计、监控进程的io情况。

  • blkio.reset_stats:重置统计信息,写入一个int值就行。
  • blkio.time:统计cgroup对设备的访问时间。
  • blkio.io_serviced:统计cgroup对特定设备的IO操作次数(包括read、write、sync和async)。
  • blkio/sectors:统计cgroup对设备扇区访问次数。
  • blkio.io_service_bytes:统计cgroup对特定设备的IO操作的数据量。
  • blkio.io_queued:统计cgroup的队列中对IO操作的请求次数。
  • blkio.io_service_time:统计cgroup对特定设备的IO操作时间。
  • blkio.io_merged:统计cgroup将BIOS请求合并到IO操作请求的次数。
  • blkio.io_wait_time:统计cgroup在各设备中各类型IO操作在队列中的等待时间。

2、cpu:使用调度程序控制task对CPU对使用。

CPU资源控制也有两种策略,一种是完全公平调度(CFS:Completely Fair Scheduler)策略,提供了限额和按比例分配两种方式;另一种是实时调度(RT:Real-Time Scheduler)策略,针对实时进程按周期分配固定的运行时间。

CFS策略:

设定CPU使用周期使用时间上限:

  • cpu.cfs_period_us:设定周期时间。
  • cpu.cfs_quota_us:设定周期内最多可使用的时间。
  • cpu.stat:统计信息,包含经历了几个cfs_perioid_us周期(nr_periods)、task被限制的次数(nr_throttled)、task被限制的总时长(throttled_time)。

    按权重比例设定CPU的分配:

  • cpu.shares:设定一个大于2的整数,作为分配CPU时间时的权重比率。

RT策略:
  • cpu.rt_period_us:设定周期时间。
  • cpu.rt_runtime_us:设定周期中的运行时间。

3、cpuacct:自动生成cgroup中task对CPU资源使用情况等报告。

  • cpuacct.usage:统计cgroup中所有task的cpu使用时长。
  • cpuacct.stat:统计cgroup中所有task的用户态和内核态分别使用CPU的时长。
  • cpuacct.usage_percpu:统计cgroup中所有task使用每个cpu的时长。

4、cpuset:为cgroup中的task分配独立的CPU(只针对多处理器系统)和内存。

  • cpuset.cpus:填写cgroup可使用的CPU的编号。
  • cpuset.mems:填写cgroup可使用的memory node。

5、devices:开启和关闭cgroup中task对设备的访问。

  • devices.allow:允许名单(白名单)。
  • devices.deny:禁止名单(黑名单)。
  • devices.list:报告为这个cgroup中的task设定访问控制的设备。

6、freezer:挂起或恢复cgroup中的task。

  • freezer.state:包含三种状态,FROZEN停止,FREEZING正在停止(只读,不能写入),THAWED恢复。

7、memory:设定cgroup中task对内存使用量的限定,并自动生成这些task对内存资源使用情况的报告。

限额类:
  • memory.limit_bytes:强制限制最大内存使用量,-1代表无限制。
  • memory.soft_limit_bytes:软限制,只有比强制限制的值小才有意义,内存紧张时才有效。
  • memory.memsw.limit_bytes:设定最大内存与swap区内存之后的用量限制。
报警与自动控制:
  • memory.oom_control:改参数填0或1,0表示开启当进程使用资源超过限制时杀死进程,1表示不启用,默认都启用。当不启用时,进程实际使用内存超过限制时会被暂停到有空闲内存资源为止。
统计与监控类:
  • memory.usage_bytes:报告该cgroup中进程当前内存使用量。
  • memory.max_usage_bytes:报告该cgroup中进程内存的最大使用量。
  • memory.failcnt:报告内存达到memory.limit_in_bytes设定的限制值的次数。
  • memory.stat:报告大量的内存统计数据。
  • cache:页缓存,包括tmpfs。
  • rss:匿名和swap缓存,不包括tmpfs。
  • mapped_file:memory-mapped映射的文件大小,包括tmpfs。
  • pgpgin:存入内存的页数。
  • pgpgout:从内存中读出的页数。
  • swap:swap用量。
  • active_anon:活跃的LRU(Least Recently Used)列表中的匿名和swap缓存,包括tmpfs。
  • inactive_anon:活跃的LRU(Least Recently Used)列表中的匿名和swap缓存,包括tmpfs。
  • active_file:活跃的LRU(Least Recently Used)列表中的file-backed内存。
  • inactive_anon:活跃的LRU(Least Recently Used)列表中的file-backed内存。
  • unevictable:无法再生的内存。
  • hierarchical_memory_limit:包含memory cgroup的层级的内存限制。
  • hierarchical_memsw_limit:包含memory cgroup的层级的内存加swap限制。

8、perfevent:对cgroup中的task进行统一的性能测试。

9、net_cls:通过等级识别符标记网络数据包,从而允许linux流量控制程序(TC:traffic controller)识别从具体cgroup中生成的数据包。

Cgroups实现方式:

_config.yml

cgroups的实现实质上是给系统进程挂上钩子,当task运行的过程中涉及到某个资源时就会触发钩子上附带的subsystem进行检测,然后进行资源控制。Linux中管理task进程的数据结构为task_struct,其中与cgroups相关的字段主要有两个,css_set *cgroups和list_head cg_list。css_set *cgroups表示进程相关的cgroups信息的指针,一个task只对应一个css_set结构,但一个css_set结构可以被多个task使用。而list_head cg_list是一个链表的头指针,这个链表包含了所有链到同一个css_set的task进程。每个css_set结构中都包含了一个指向包含进程与一个特定subsystem相关信息的指针数组cgroup_subsys_state,而这个指针则指向了一个包含所有cgroup信息的cgroup结构,task进程就是通过这种方式与cgroup关联起来的。系统开始运行的时候,内核的主函数就会对root cgroups和css_set进程初始化,每次task进行fork/exit时,都会附加(attach)/分离(detach)对应的css_set。

此外,cgroup结构体中有一个list_head css_sets字段,它是一个指向包含cgroup与task之间多对多关系信息的链表的头指针cg_cgroup_link,是task和cgroup多对多的中间媒介。每个cg_cgroup_link中包含一个css_set *cg字段,指向了每一个task的css_set。css_set结构中包含task头指针,指向所有链到这个css_set的task的进程,到此刚好形成一个循环。添加cg_cgroup_link主要是出于性能方面的考虑,一是节省task结构体占用多内存,二是提升了fork()/exit()的速度。

当task从一个cgroup中移动到另一个时,如果要加入cgroup与现有的cgroup子系统相同的话,就重用现有的css_set,否则就分配一个新的。所有的css_set通过一个哈希表进行存放和查询,css_set结构中的hlist_node hlist就指向了这个css_set_table这个哈希表。

为了用精简的内核代码为cgroup提供熟悉的权限和命名空间管理,内核开发者们安装linux虚拟文件系统转换器(VFS:Virtual Filesystem Switch)的接口实现了一套名为cgroup的文件系统,巧妙的用来表示cgroups中的hierarchy概念,把各个subsystem的实现都封装到文件系统的各项操作中。定义subsystem的结构体是cgroup_subsys ,该结构体定义了一组函数接口,让各个子系统去实现,类似的思想还被用在了cgroup_subsys_state中,cgroup_subsys_state并没有定义控制信息,只是定义了都需要用到的公共信息,各个子系统可以按需取定义自己的控制信息结构体,最终在自定义的结构体中把cgroup_subsys_state包含进去,然后通过container_of等宏定义来获取对应的结构体。

Cgroups用户层体现:

在实际使用过程中,可以通过挂载(mount)cgroup文件系统新建一个层级结构,挂载时指定要绑定的子系统,默认绑定所有子系统。挂载后,就可以向操作文件一样对cgroups的hierarchy层级进行操作管理。在创建的hierarchy中创建文件夹,就类似于fork一个后代cgroup,后代cgroup中默认继承原有cgroup中的配置属性,但是可以根据需求对配置参数进行调整,这样就可以把一个大的cgroup系统分割成一个个嵌套的、可动态变化的“软分区”。

当一个顶层的cgroup文件系统被卸载(unmount)时,如果其中创建了后代cgroup目录,那么就算上层的cgroup被卸载了,后代的层级也是激活状态,其cgroup中配置依然有效。只有递归的卸载层级中的所有cgroup,那个层级才会被真正删除。

层级激活后,/proc目录下的每个task PID文件夹下都会新添加一个cgroup文件,可列出所在的层级,对其进行控制的子系统及对应cgroup文件系统的路径。

Written on December 3, 2017