控制组详解
控制组(Control Group)是Linux内核的一个特性,用于控制进程对CPU、磁盘I/O、内存、网络等资源的使用。Cgroup对进程(以及它们未来的子进程)进行分区/聚合,并关联到一个或者多个子系统。Cgroup子系统是一个模块,它利用Cgroup的任务分组机制,对任务组中的进程施加影响。子系统包括:
子系统 | 说明 |
blkio | 为块设备设定输入/输出限制 |
cpu | 控制cgroup中任务的CPU使用 |
cpuacct | 自动生成cgroup中任务所使用的CPU报告 |
cpuset | 为cgroup中的任务分配独立CPU、内存节点 |
devices | 允许或者拒绝cgroup中的任务访问设备 |
freezer | 挂起或者恢复cgroup中的任务 |
memory | 设定cgroup中任务使用的内存限制,并自动生成由那些任务使用的内存资源的报告 |
net_cls | 使用classid标记网络数据包,可允许Linux流量控制程序tc识别从具体cgroup中生成的数据报 |
ns | 名称空间子系统 |
典型情况下,子系统是一个资源控制器(Resource controller) —— 它进行资源调度,或者按组进行资源配额(limits)。
Cgroup具有树状层次,子cgroup会继承父cgroup的设置。cgroup被映射到Linux的文件系统树中,对应cgroupfs类型的虚拟文件系统/sys/fs/cgroup,你可以基于文件系统接口来管理它。每个进程仅仅能属于树中单个节点,关联单组子系统。每个子系统为Cgroup树中的节点设置特定的状态。每个子系统都具有独立的树状层次,这让进程在不同子系统中,可被划分到不同的组。用户代码可以在虚拟文件系统中创建新的Cgroup,指定、查询任务所属的Cgroup。
某些使用,cgroup也指其中的那组进程。
控制组按如下方式扩展内核:
- 每个进程持有一个引用计数指针,指向一个css_set
- css_set持有一系列引用计数指针,指向一系列cgroup_subsys_state结构,每个结构对应已注册的Cgroup子系统
- cgroups文件系统可以被挂载,以便在用户空间进行读写,文件系统类型为cgroup。挂载cgroup层次时你可以指定逗号分隔的子系统列表,如果不指定则挂载全部子系统。如果当前存在活动的、具有完全一致的子系统集的cgroup层次,则挂载失败-EBUSY。不支持为活动的cgroup层次动态添加、删除子系统
- cgroup文件系统被卸载后,如果顶级cgroup下没有任何子cgroup则目标cgroup子系统的层次禁用,否则一直处于激活状态
- /proc下每个进程有一个cgroup文件:
12345678910111213# 子系统名:cgroup名12:pids:/system.slice/kubelet.service11:blkio:/system.slice/kubelet.service10:net_cls,net_prio:/9:perf_event:/8:cpu,cpuacct:/system.slice/kubelet.service7:hugetlb:/6:devices:/system.slice/kubelet.service5:cpuset:/4:freezer:/3:rdma:/2:memory:/system.slice/kubelet.service1:name=systemd:/system.slice/kubelet.service - 允许用户列出某个cgroup关联的所有任务
- 控制组在内核的一些非性能临界的代码中注册了一些简单的钩子:
- 在init/main.c中初始化根Cgroups、初始的css_set结构
- 在fork和exit时,将进程和css_set关联
- 进程可以被转移到别的组,css_set指针随之改变
Cgroups没有引入新的系统调用,对它的操控全部依赖于文件系统。要创建新cgroup可以执行mkdir系统调用。
每个cgroup都对应了文件系统中的目录,包含以下文件:
- tasks:关联到此组的PID列表。每个PID一行。支持写入线程ID,表示将线程移动到该组
- cgroup.procs:此组中的线程组ID列表
- notify_on_release:提示是否在退出时运行release agent,取值0或1。如果设置为1则组中最后一个进程退出,并且最后一个子组被移除后,调用release_agent指定的命令
- release_agent:仅在顶级cgroup(也就是子系统根目录)下存在,用于release通知
- 各子系统会有其它文件,用于配置当前cgroup的资源用量
通过层次化的cgroups,可以将一个大的系统拆分为嵌套的、动态变化的软分区:
- 每个子系统相互独立:
12345678910111213141516171819tree cgroup/ -L 1cgroup/├── blkio├── cpu -> cpu,cpuacct├── cpuacct -> cpu,cpuacct├── cpu,cpuacct├── cpuset├── devices├── freezer├── hugetlb├── memory├── net_cls -> net_cls,net_prio├── net_cls,net_prio├── net_prio -> net_cls,net_prio├── perf_event├── pids├── rdma├── systemd└── unified - 子系统内部,可以具有树状的、各不相同的控制组层次。例如下面显示了pids子系统的部分控制组结构:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253tree /sys/fs/cgroup/pidspids/├── cgroup.clone_children├── cgroup.procs├── cgroup.sane_behavior│ ├── cgroup.clone_children│ ├── cgroup.procs│ ├── notify_on_release│ ├── pids.current│ ├── pids.events│ └── pids.max├── docker│ ├── bcb6bb4c03fdf2d9db21dbfc983bb081bd431346adf3fe8cc00547606a46c659│ │ ├── cgroup.clone_children│ │ ├── cgroup.procs│ │ ├── notify_on_release│ │ ├── pids.current│ │ ├── pids.events│ │ ├── pids.max│ │ └── tasks│ ├── cgroup.clone_children│ ├── cgroup.procs│ ├── notify_on_release│ ├── pids.current│ ├── pids.events│ ├── pids.max│ └── tasks├── kubepods│ ├── besteffort│ │ ├── cgroup.clone_children│ │ ├── cgroup.procs│ │ ├── notify_on_release│ │ ├── pids.current│ │ ├── pids.events│ │ ├── pids.max│ │ ├── pod2c7515f2-44a5-4a46-9033-d7bd06441527│ └── burstable│ ├── cgroup.clone_children│ ├── cgroup.procs│ ├── notify_on_release│ ├── pids.current│ ├── pids.events│ ├── pids.max│ └── pod2a81b908-a5dd-465d-a284-bf918e53c248└── system.slice└── kubelet.service├── cgroup.clone_children├── cgroup.procs├── notify_on_release├── pids.current├── pids.events├── pids.max└── tasks
块IO控制器(Block IO Controller)能够以多种策略(例如带宽占比、最大带宽)来限制进程对存储设备(不管是叶子节点还是中间节点)的用量。
目前已经实现的策略有两种:
- 基于时间比例权重的磁盘用量划分(Proportional weight time based division of disk)策略,在IO调度器CFQ(Completely Fair Queuing)中实现,仅对使用了CFQ的叶子节点可用。CFQ于2.6.18版本成为默认的IO调度器。CFQ将:
- 进程提交的同步IO请求存放在各自(每个进程一个)对列中,然后为每个对列分配时间片,以访问磁盘。时间片的长度、对列最大深度取决于请求进程的优先级
- 所有异步IO请求被统一放置到几个对列中,每个对列的优先级不同
- 限流(Throttling)策略。用于限制进程对设备的最大IO速率。该策略在通用块设备层实现,可用于叶子节点或者高级别的逻辑设备节点(例如Device mapper)
文件 | 说明 | ||
blkio.weight | 使用所有块设备的默认权重,取值范围10-1000 | ||
blkio.weight_device | 可以按设备设置权重:
|
||
blkio.leaf_weight | 类似上面,但是仅仅用于指定,当和子cgroup争用块设备时的权重 | ||
blkio.leaf_weight_device | |||
blkio.time |
使用每块设备的磁盘时间,单位毫秒 格式:dev_maj:dev_minor time |
||
blkio.sectors | 使用每块设备传输的扇区数 | ||
blkio.io_service_bytes | 使用每块设备传输的字节数 | ||
blkio.io_serviced | 使用每块设备的IO次数 | ||
blkio.io_service_time | 使用每块设备累计消耗的、从IO请求发起到IO完成的时间。单位纳秒 | ||
blkio.io_wait_time | 在调度器对列中等待服务的总时间 | ||
blkio.io_merged | 被合并(到先前请求,成为单个IO请求)的IO请求的总数 | ||
blkio.io_queued | 正在排队的IO请求数量 |
文件 | 说明 |
blkio.throttle.read_bps_device |
最大读取速率,单位bytes/s 格式:dev_maj:dev_minor rate_bytes_per_second |
blkio.throttle.write_bps_device | 最大写入速率 |
blkio.throttle.read_iops_device | 最大读IOPS |
blkio.throttle.write_iops_device | 最大写IOPS |
blkio.throttle.io_serviced | 已经发起的IO请求总数 |
blkio.throttle.io_service_bytes | 已经发起的IO字节数 |
文件 | 说明 |
blkio.reset_stats | 写入任何整数,导致所有统计信息重置 |
对于Cgroups v1来说,必须开启O_DIRECT,也就是跳过操作系统内核的页面缓存,才能让blkio控制器的IO限速生效。
原因是,Cgroup v1的不同控制器之间没法建立联系,这导致页面缓存的writeback IO总是被根cgorup限制。
对于4.5版本引入的Cgroups v2来说,控制组只有单个层次——所有子系统在单棵树中管理。不是所有v1的控制器在v2可用——memory, io, rdma, pids是可用的。
Cgroup v1 blkio基于限流的策略,存在O_DIRECT的问题。因此对于容器环境,需要公平共享IO资源的话,可以考虑使用基于权重的策略(blkio.weight)+CFQ IO调度器。
先测试一下写入性能:
1 2 3 4 5 |
# 注意开启O_DIRECT,最大程度上避免内核页面缓存的影响 dd if=/dev/zero of=test bs=512M count=1 oflag=direct 1+0 records in 1+0 records out 536870912 bytes (537 MB) copied, 3.74837 s, 143 MB/s |
可以看到,不进行限制的前提下,sdc的顺序写可达140MB/s。
要按照bytes/s对块设备资源进行限制,首先需要得到目标资源的主、次版本号:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
major minor #blocks name 8 0 500107608 sda 8 1 61542400 sda1 8 2 314572800 sda2 8 3 123990360 sda3 8 16 1953514584 sdb 8 17 1953512448 sdb1 8 32 500107608 sdc 8 33 30720000 sdc1 8 34 1 sdc2 8 35 97278975 sdc3 8 37 15998976 sdc5 8 38 356106240 sdc6 8 48 250059096 sdd 8 49 250059064 sdd1 |
然后,在blkio控制器内创建一个新的控制组:
1 |
sudo mkdir -p /sys/fs/cgroup/blkio/test |
将test组对磁盘sdc的用量限制为1MB/s:
1 |
echo "8:32 1048576" > /sys/fs/cgroup/blkio/test/blkio.throttle.write_bps_device |
进行测试,将当前Shell进程加入到test组:
1 |
echo $$ > /sys/fs/cgroup/blkio/test/cgroup.procs |
此时在用dd进行写入,并用iostat观察,可以看到速度限制生效了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# iostat 1 -d -h -y -k -p sdc Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn sdc 4.00 0.00 1092.00 0 1092 sdc1 2.00 0.00 68.00 0 68 sdc2 0.00 0.00 0.00 0 0 sdc3 0.00 0.00 0.00 0 0 sdc5 0.00 0.00 0.00 0 0 sdc6 2.00 0.00 1024.00 0 1024 |
CPU审计控制器(CPU accounting controller)用于统计一组任务的CPU用量。统计的范围是,直接位于组内的进程用量+任何子孙代组的所有进程用量。
文件cpuacct.usage中记录CPU用量。文件cpuacct.stat进一步将CPU用量细分为user、system。单位USER_HZ,一个USER_HZ包含100个CPU Tick。
在大型计算机上,会同时存在多个处理器、复杂的内存缓存层次、多个具有非均等访问时间(non-uniform access times,NUMA)的内存节点。cpusets子系统可以提示内核,进行更加高效的进程调度和内存放置,减少内存访问时间和争用。
用于将一系列CPU核心和内存节点分配给一组进程:
- 通过sched_setaffinity(2)系统调用,在进程的CPU亲和性掩码中设置相应的位
- 通过mbind(2)和set_mempolicy(2)系统调用,包含内存节点到进程的内存策略
进程调度器不会将进程调度到非亲和核心,内核页分配器不会在分亲和节点上为进程分配内存。
文件 | 说明 |
cpuset.cpus | 亲和的CPU序号,必须是父cgroup的子集。取值0-2,16表示0 1 2 16 |
cpuset.mems | 亲和的内存节点,必须是父cgroup的子集。取值0-2,16表示0 1 2 16 |
cpuset.memory_migrate | 默认0,如果为1则强制触发内存页移动以满足cpuset.mems |
cpuset.cpu_exclusive | 默认0,是否独占CPU,仅当父cgroup为1时才可以设为1 |
cpuset.mem_exclusive | 默认0,是否独占内存节点,仅当父cgroup为1时才可以设为1 |
cpuset.mem_hardwall | 默认0,表示页面文件、缓冲等可共享数据的分配是否受到cpuset.mems的约束 |
cpuset.memory_pressure | 只读,显示内存压力 —— 当前组中进程释放内存(以满足新的内存请求)的速率 |
cpuset.sched_load_balance | 默认1,是否在cpuset.cpus之间保持负载均衡 |
cpuset.sched_relax_domain_level |
上个文件为1时,使用的负载均衡级别 |
cpuset.memory_pressure_enabled | 仅根,是否计算内存压力 |
设备白名单控制器(Device Whitelist Controller),用于限制对设备文件的open/mknod操作。
此子系统为当前组关联一个设备访问白名单,名单中的每个条目有四个字段:
- type:a(all)、c(char)、b(block)。all表示应用到所有类型的设备
- major:主设备号,整数或者*
- minor:次设备号,整数或者*
- access:r(read)、w(write)、m(mknod)的组合
根device group的白名单为a rwm,即无限制。
子group会从父group获得一个副本。如果父group禁止了对某个设备的访问,则子group无法启用。
示例:
1 |
echo 'c 1:3 mr' > devices.allow |
freezer的两个应用场景:
- 根据管理员的期望,在批量任务管理系统控制资源的调度。freezer用于标注需要启动/停止的任务组
- 运行状态检查点:允许checkpoint代码获得任务组的一致性镜像 —— freezer可以让一组任务进入静止状态,以便调用内核接口来收集静止任务的信息。静止状态的任务可以跨节点迁移
用户空间发起的SIGSTOP、SIGCONT调用并不总是能正确的暂停/恢复一个任务。原因是:
- SIGSTOP虽然不能被捕获、阻止、忽略,但是父任务可能看到此信号
- SIGCONT可以被进程捕获并忽略
相比之下,freezer使用内核代码确保目标任务无法干涉。
冻结父组,会导致任何后代组中的任务也被冻结。
文件 | 说明 | ||
freezer.state |
读取时,返回当前组的状态:THAWED、FREEZING、FROZEN 写入时,设置当前组的状态:
|
||
freezer.self_freezing | 只读,显示当前组的状态,THAWED返回0,否则返回1 | ||
freezer.parent_freezing | 只读,显示父组的状态 |
可以限制组对巨型转译后备缓冲(HugeTLB,用于提升进程地址转换的速度)的用量。由于HugeTLB不支持页回收,应用程序超过hugetlb限制后会收到SIGBUS错误。
文件 | 说明 |
hugetlb.<size>.limit_in_bytes | 读写允许的巨页总大小 |
hugetlb.<size>.max_usage_in_bytes | 显示记录到的最大巨页用量 |
hugetlb.<size>.usage_in_bytes | 显示当前巨页用量 |
hugetlb.<size>.failcnt | 显示由于hugetlb限制到的分配失败的次数 |
其中size表示巨页大小,支持16M或16G。
内存资源控制器可以将一个组的内存访问行为和系统其它部分隔离开来,其特性包括:
- 审计匿名页、文件缓存、交换缓存用量,并限制之
- 页面按照Cgroup创建LRU,不使用全局LRU
- memory + swap总量可以被审计和限制
- 软限制支持
- 用量阈值、内存压力通知器
- 禁用oom-killer的开关,oom通知器
- 根控制组不受到限制
文件 | 说明 |
cgroup.procs | 显示进程列表 |
cgroup.event_control | event_fd()的接口 |
memory.usage_in_bytes | 显示当前内存用量 |
memory.memsw.usage_in_bytes | 显示当前内存 + Swap用量 |
memory.limit_in_bytes | 设置/显示内存用量配额 |
memory.memsw.limit_in_bytes | 设置/显示内存 + Swap用量配额 |
memory.soft_limit_in_bytes |
设置/显示内存用量的软限制 软限制用于实现更多的内存共享,它允许控制组使用尽可能多的内存,前提是:
当系统检测到内存不足或争用时,控制组被push back到软限制。软限制是一种Best-effort特性,不做绝对保证 |
memory.failcnt | 显示内存用量到达限额的次数 |
memory.memsw.failcnt | 显示内存 + Swap用量到达限额的次数 |
memory.max_usage_in_bytes | 显示记录到的内存用量峰值 |
memory.memsw.max_usage_in_bytes | 显示记录到的内存 + Swap用量峰值 |
memory.stat |
显示多种统计信息: cache 页面缓存用量 |
memory.use_hierarchy | 设置/显示层次性审计 |
memory.pressure_level | 设置内存压力通知 |
memory.swappiness | 设置/显示vmscan的swappiness参数,参考sysctl的vm.swappiness |
memory.oom_control |
设置/显示OOM控制。设置为1可以禁用OOM-killer 你可以在OOM发生后获得通知:
当OOM发生后通过eventfd通知应用程序 当OOM-killer被禁用后,组中任务请求过量内存时会在OOM-waitqueue中挂起/休眠。放开限制后任务可以继续运行 |
memory.numa_stat | 显示各NUMA节点的内存用量统计信息 |
memory.kmem.limit_in_bytes | 设置/显示内核内存的硬限制 |
memory.kmem.usage_in_bytes | 显示当前内核内存用量 |
memory.kmem.failcnt | 显示内核内存用量到达限额的次数 |
memory.kmem.max_usage_in_bytes | 显示记录到的内核内存用量峰值 |
memory.kmem.tcp.limit_in_bytes | 显示/设置TCP缓冲内存限额 |
memory.kmem.tcp.usage_in_bytes | 显示当前TCP缓冲内存用量 |
memory.kmem.tcp.failcnt | 显示TCP缓冲内存用量到达限额的次数 |
memory.kmem.tcp.max_usage_in_byte | 显示记录到的TCP缓冲内存用量峰值 |
网络分类器(Network classifier)提供一个接口,用于为网络包标记一个类别标识符(class identifier,classid)。
流量控制器(Traffic Controller,tc)可以根据classid,为不同cgroup发出的网络包分配优先级。Netfilter(iptables)也可以用classid来对网络包进行各种操作。
创建一个net_cls子系统后,会产生一个net_cls.classid文件,其默认值为0。你可以向其写入0xAAAABBBB格式的十六进制数,其中AAAA是主句柄号,BBBB是从句柄号。读取net_cls.classid获得一个十进制数。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# 设置10:1句柄 echo 0x100001 > net_cls.classid # 读取 cat net_cls.classid # 1048577 # 配置tc tc qdisc add dev eth0 root handle 10: htb # 流量控制 tc class add dev eth0 parent 10: classid 10:1 htb rate 40mbit # 设置优先级 tc filter add dev eth0 parent 10: protocol ip prio 10 handle 1: cgroup # 配置Iptables iptables -A OUTPUT -m cgroup ! --cgroup 0x100001 -j DROP |
网络优先级子系统(Network priority)为管理员提供一个接口,允许动态设置不同应用程序发起的网络流量的优先级。
应用程序可以通过Socket选项SO_PRIORITY来设置流量优先级,但是:
- 遗留代码可能没有使用该选项
- 流量优先级属于运维职责而非编码职责
文件 | 说明 |
net_prio.prioidx | 只读,内核使用的、代表当前组的整数代码 |
net_prio.ifpriomap | 包含一系列 ifname priority的列表,例如eth0 5表示当前组向eth0发起的出口流量的优先级为5 |
该子系统可用于,在到达限制后禁止for/clone等创建新进程的调用。
文件 | 说明 |
pids.max | 设置允许的最大任务(进程、线程)数量 |
pids.current | 组中当前任务总数 |
最初引入Cgroups特性是在Linux 2.6.24,随着开发者增加越来越多的控制器,Cgroups变得越发的不一致和复杂。从3.10开始设计的新版本Cgroups V2解决这些缺点,并且在4.5正式发布。出于兼容性的原因,Cgroups V1仍然会长期存在。
目前V2仅仅实现了V1中控制器的子集,如果V2支持某个控制器,建议基于V2使用它。不支持同时在V1/V2中启用同一个控制器。
主要区别:
- V1支持基于独立的cgroupfs进行挂载每个控制器:
12345mount -t cgroup -o cpu none /sys/fs/cgroup/cpumount -t cgroup -o cpu,cpuacct none /sys/fs/cgroup/cpu,cpuacct# 当然你也可以挂载所有子系统mount -t cgroup -o all cgroup /sys/fs/cgroupV2中所有挂载的控制器,必须位于单一的文件系统层次中
- V2强制要求no internal processes规则,该规则要求,除了根cgroup之外,所有进程必须位于叶子cgroup中
- V2中非根cgroup包含文件cgroup.events,内容如下:
1234# 如果是1,表示该控制组或者它的子代控制组包含至少一个进程populated 1# 如果是1,表示当前控制组被冻结frozen 0 -
V2支持限制子代cgroup的深度(cgroup.max.depth)和数量(cgroup.max.descendants)
Leave a Reply