Linux cgrpup技术解析和验证测试
- 1. cgroup技术解析和分类
- 1.1. 相关概念介绍
- 1.2 cgroup子系统
- 2. cgroup子系统详解
- 2.1 cpu子系统
- 2.2 cpuacct子系统
- 2.3 cpuset子系统
- 2.4 memory子系统
- 2.5 blkio子系统
- 2.6 ns子系统
- 3. cgroup使用
- 3.1 通用使用流程
- 3.1.1 限制进程的cpu资源
- 3.1.2 绑定cpu运行在固定的核心数上
- 3.1.3 限制进程的memory资源
- 3.2 进程性能测试
- 4. 疑问和思考
- 4.1 为什么通常不进行磁盘io隔离?
- 5. 参考文档
cgroup是于2.6内核由Google公司主导引入的,它是Linux内核实现资源虚拟化的技术基石,LXC(Linux Containers)和docker容器所用到的资源隔离技术,正是cgroup。
cgroup(control group)是Linux内核提供的一种机制,用于将进程组织成为一个层次化的层级结构,并为每个层级结构提供资源限制、优先级控制等功能。cgroup技术可以用于管理和控制系统中的进程,从而提供更好的资源管理和隔离性能。
linux 可以通过cgroup针对进程级别进行资源隔离和限制,该技术成为总多资源调度平台(如k8s、yarn等)进行资源隔离和限制的功能依据。对于进程资源的限制,通常能够实现cpu、内存、网络ns隔离,但是难以针对磁盘io性能进行资源隔离。
cgroup技术在Linux系统中得到广泛应用,特别是在容器化技术(如Docker)中。它可以帮助管理员更好地管理系统资源,并提供更好的性能和稳定性。
1. cgroup技术解析和分类
1.1. 相关概念介绍
cgroup(control group)是Linux内核提供的一种机制,用于将进程组织成为一个层次化的层级结构,并为每个层级结构提供资源限制、优先级控制等功能。cgroup技术可以用于管理和控制系统中的进程,从而提供更好的资源管理和隔离性能。
cgroup技术的主要作用有:
- 资源限制:cgroup可以限制进程组使用的资源,例如CPU使用时间、内存等。这可以防止某个进程组占用过多的资源,以保持系统的稳定性。
- 优先级控制:cgroup可以为进程组设置优先级,以确保关键任务能够获得足够的系统资源,而不被低优先级的任务占用。
- 资源统计:cgroup可以对每个进程组使用的资源进行统计,以便管理员了解系统中各个进程组的资源消耗情况。
- 进程隔离:cgroup可以将进程组隔离开来,防止它们互相影响。这在容器技术中尤其重要,可以确保容器间的资源隔离和安全性。
1.2 cgroup子系统
常见的相关概念
- 任务(task): 在cgroup中,任务就是一个进程。
- 控制组(control group): cgroup的资源控制是以控制组的方式实现,控制组指明了资源的配额限制。进程可以加入到某个控制组,也可以迁移到另一个控制组。
- 层级(hierarchy): 控制组有层级关系,类似树的结构,子节点的控制组继承父控制组的属性(资源配额、限制等)。
- 子系统(subsystem): 一个子系统其实就是一种资源的控制器,比如memory子系统可以控制进程内存的使用。子系统需要加入到某个层级,然后该层级的所有控制组,均受到这个子系统的控制。
相关概念的层级关系
两个任务组成了一个 Task Group,并使用了 CPU 和 Memory 两个子系统的 cgroup,用于控制 CPU 和 MEM 的资源隔离。
2. cgroup子系统详解
cgroup的子系统类型众多,但是并不是每一种类型子系统都会经常使用,本文重点介绍常用的子系统。其中cpu、memory、ns子系统在k8s环境中是重度依赖的。
- cpu: 限制进程的 cpu 使用率。
- cpuacct 子系统,可以统计 cgroups 中的进程的 cpu 使用报告。
- cpuset: 为cgroups中的进程分配单独的cpu节点或者内存节点。
- memory: 限制进程的memory使用量。
- blkio: 限制进程的块设备io。
- devices: 控制进程能够访问某些设备。
- net_cls: 标记cgroups中进程的网络数据包,然后可以使用tc模块(traffic control)对数据包进行控制。
- net_prio: 限制进程网络流量的优先级。
- huge_tlb: 限制HugeTLB的使用
- freezer:挂起或者恢复cgroups中的进程。
- ns: 控制cgroups中的进程使用不同的namespace。
2.1 cpu子系统
cpu子系统限制对CPU的访问,每个参数独立存在于cgroups虚拟文件系统的伪文件中。
由于linux是通过cpu时间切片的方式分配给相关进程cpu的运行时间,因此能够通过控制进程的cpu切片时间,从而控制进程能够获取到的cpu的资源。
参数解释如下
- cpu.shares: cgroup对时间的分配。比如cgroup A设置的是1,cgroup B设置的是2,那么B中的任务获取cpu的时间,是A中任务的2倍。
- cpu.cfs_period_us: 完全公平调度器的调整时间配额的周期。
- cpu.cfs_quota_us: 完全公平调度器的周期当中可以占用的时间。
- cpu.stat 统计值
- nr_periods 进入周期的次数
- nr_throttled 运行时间被调整的次数
- throttled_time 用于调整的时间
可以通过如下方式,计算一个进程能够使用的cpu核心资源数
c p u 核心数 = c p u . c f s _ q u o t a _ u s / c p u . c f s _ p e r i o d _ u s cpu核心数 =cpu.cfs\_quota\_us / cpu.cfs\_period\_us cpu核心数=cpu.cfs_quota_us/cpu.cfs_period_us
截图表示,对应cgroup子目录下1687270562000004809 下配置的tasks进程,最多只能使用4c
2.2 cpuacct子系统
子系统生成cgroup任务所使用的CPU资源报告,不做资源限制功能。
各参数解释如下:
- cpuacct.usage: 该cgroup中所有任务总共使用的CPU时间(ns纳秒)
- cpuacct.stat: 该cgroup中所有任务总共使用的CPU时间,区分user和system时间。
- cpuacct.usage_percpu: 该cgroup中所有任务使用各个CPU核数的时间。
通过cpuacct如何计算CPU利用率呢?可以通过cpuacct.usage来计算整体的CPU利用率,计算如下:
# 1. 获取当前时间(纳秒)
tstart=$(date +%s%N)
# 2. 获取cpuacct.usage
cstart=$(cat /xxx/cpuacct.usage)
# 3. 间隔5s统计一下
sleep 5
# 4. 再次采点
tstop=$(date +%s%N)
cstop=$(cat /xxx/cpuacct.usage)
# 5. 计算利用率
($cstop - $cstart) / ($tstop - $tstart) * 100
2.3 cpuset子系统
适用于分配独立的CPU节点和Mem节点,比如将进程绑定在指定的CPU或者内存节点上运行
各参数解释如下:
- cpuset.cpus: 可以使用的cpu节点
- cpuset.mems: 可以使用的mem节点
- cpuset.memory_migrate: 内存节点改变是否要迁移
- cpuset.cpu_exclusive: 此cgroup里的任务是否独享cpu
- cpuset.mem_exclusive: 此cgroup里的任务是否独享mem节点
- cpuset.mem_hardwall: 限制内核内存分配的节点(mems是用户态的分配)
- cpuset.memory_pressure: 计算换页的压力。
- cpuset.memory_spread_page: 将page cache分配到各个节点中,而不是当前内存节点。
- cpuset.memory_spread_slab: 将slab对象(inode和dentry)分散到节点中。
- cpuset.sched_load_balance: 打开cpu set中的cpu的负载均衡。
- cpuset.sched_relax_domain_level: the searching range when migrating tasks
- cpuset.memory_pressure_enabled: 是否需要计算 memory_pressure
- cgroup.procs: 配置需要限制的进程
如果希望限制进程运行在绑定到某些cpu核心上,可以按照如下操作
# 创建cpuset组
cd /sys/fs/cgroup/cpuset
mkdir 1687270562000004809
# 配置可运行的cpu列表
echo '0-4' > cpuset.cpus
# 配置可运行的memory架构列表
echo '0-1' > cpuset.mems
# 配置需要限制的进程
echo 42150 > cgroup.procs
查看对应进程运行在哪些cpu上
taskset -c -p 42150
说明配置生效
2.4 memory子系统
跟linux限制cpu限制不同(cpu能够通过cpu时间切片进行限制进程对cpu的使用量),linux无法控制进程对内存的使用需求,因此linux对的cgroup对进程的内存限制,是通过检测进程的内存占用量,如果超过cgroup的限制,linux会主动杀死相关进程,从而确保进程不超过内存使用量。
memory子系统主要涉及内存一些的限制和操作
主要有以下参数:
- memory.usage_in_bytes # 当前内存中的使用量
- memory.memsw.usage_in_bytes # 当前内存和交换空间中的使用量
- memory.limit_in_bytes # 设置or查看内存使用量
- memory.memsw.limit_in_bytes # 设置or查看 内存加交换空间使用量
- memory.failcnt # 查看内存使用量被限制的次数
- memory.memsw.failcnt # - 查看内存和交换空间使用量被限制的次数
- memory.max_usage_in_bytes # 查看内存最大使用量
- memory.memsw.max_usage_in_bytes # 查看最大内存和交换空间使用量
- memory.soft_limit_in_bytes # 设置or查看内存的soft limit
- memory.stat # 统计信息
- memory.use_hierarchy # 设置or查看层级统计的功能
- memory.force_empty # 触发强制page回收
- memory.pressure_level # 设置内存压力通知
- memory.swappiness # 设置or查看vmscan swappiness 参数
- memory.move_charge_at_immigrate # 设置or查看 controls of moving charges?
- memory.oom_control # 设置or查看内存超限控制信息(OOM killer)
- memory.numa_stat # 每个numa节点的内存使用数量
- memory.kmem.limit_in_bytes # 设置or查看 内核内存限制的硬限
- memory.kmem.usage_in_bytes # 读取当前内核内存的分配
- memory.kmem.failcnt # 读取当前内核内存分配受限的次数
- memory.kmem.max_usage_in_bytes # 读取最大内核内存使用量
- memory.kmem.tcp.limit_in_bytes # 设置tcp 缓存内存的hard limit
- memory.kmem.tcp.usage_in_bytes # 读取tcp 缓存内存的使用量
- memory.kmem.tcp.failcnt # tcp 缓存内存分配的受限次数
- memory.kmem.tcp.max_usage_in_bytes # tcp 缓存内存的最大使用量
截图表示,对应cgroup子目录下1687270562000004809 下配置的tasks进程,最多只能使用16g
2.5 blkio子系统
主要用于控制设备IO的访问。有两种限制方式:权重和上限,权重是给不同的应用一个权重值,按百分比使用IO资源,上限是控制应用读写速率的最大值。
按权重分配IO资源:
- blkio.weight:填写 100-1000 的一个整数值,作为相对权重比率,作为通用的设备分配比。
- blkio.weight_device: 针对特定设备的权重比,写入格式为 device_types:node_numbers weight,空格前的参数段指定设备,weight参数与blkio.weight相同并覆盖原有的通用分配比。
按上限限制读写速度:
- blkio.throttle.read_bps_device:按每秒读取块设备的数据量设定上限,格式device_types:node_numbers bytes_per_second。
- blkio.throttle.write_bps_device:按每秒写入块设备的数据量设定上限,格式device_types:node_numbers bytes_per_second。
- blkio.throttle.read_iops_device:按每秒读操作次数设定上限,格式device_types:node_numbers operations_per_second。
- blkio.throttle.write_iops_device:按每秒写操作次数设定上限,格式device_types:node_numbers operations_per_second
针对特定操作 (read, write, sync, 或 async) 设定读写速度上限
- blkio.throttle.io_serviced:针对特定操作按每秒操作次数设定上限,格式device_types:node_numbers operation operations_per_second
- blkio.throttle.io_service_bytes:针对特定操作按每秒数据量设定上限,格式device_types:node_numbers operation bytes_per_second
linux提供了磁盘io的cgroup限制,但是通常的k8s/yarn等调度系统都没有采用。主要的原因是,在不同的设备下,磁盘io性能差异很大,并且磁盘io的相关监控通常都有比较大的延迟,因此准确性不高。
2.6 ns子系统
Namespace 是 Linux 内核中实现的特性,本质上是一种资源隔离方案。
Namespace 提供了一种抽象机制,将原本全局共享的资源隔离成不同的集合,集合中的成员独享其原本全局共享的资源。
举个例子:进程 A 和进程 B 分别属于两个不同的 Namespace,那么进程 A 将可以使用 Linux 内核提供的所有 Namespace 资源:如独立的主机名,独立的文件系统,独立的进程编号等等。同样地,进程 B 也可以使用同类资源,但其资源与进程 A 使用的资源相互隔离,彼此无法感知。从用户层面来看,进程 A 读写属于 A 的 Namespace 资源,进程 B 读写属于 B 的 Namespace 资源,彼此之间安全隔离。
Docker 就是利用 Namespace 这个特性,实现了容器之间的资源隔离。本质上来看,每一个 Docker 容器就是宿主机进程,不同 Docker 容器就对应不同的宿主机进程,这样,不同容器(即不同进程)就可以采用 Namespace 资源隔离,使得每一个容器看起来都像是一个独立的小虚拟机。
Linux 内核提供了 7 种不同的 Namespace,如下所示:
Namespace | clone() 使用的 flag | 所隔离的资源 |
---|---|---|
Cgroup | CLONE_NEWCGROUP | Cgroup 根目录 |
IPC | CLONE_NEWIPC | System V IPC,POSIX 消息队列 |
Network | CLONE_NEWNET | 网络设备、协议栈、端口等 |
Mount | CLONE_NEWNS | 挂载点 |
PID | CLONE_NEWPID | 进程 ID |
User | CLONE_NEWUSER | 用户和组 ID |
UTS | CLONE_NEWUTS | 主机名和域名 |
3. cgroup使用
3.1 通用使用流程
3.1.1 限制进程的cpu资源
# 为对应的进程配置cpu的cgroup子系统
cd /sys/fs/cgroup/cpu
# 根据情况创建相关目录
mkdir 1687270562000004809
# 配置cpu使用情况,并绑定对应的进程pid
echo 400000 > cpu.cfs_quota_us
echo 100000 > cpu.cfs_period_us
echo pid >> tasks
示例限制进程pid能够最大使用4c的cpu。
另外,如果进程发生重启,pid更换后,需要配置对应的pid到tasks文件中
3.1.2 绑定cpu运行在固定的核心数上
# 为对应的进程配置cpu的cgroup子系统
cd /sys/fs/cgroup/cpuset/
# 根据情况创建相关目录
mkdir 1687270562000004809
# 配置cpu和mem运行的核心数,并绑定对应的cpu
echo 0-4 > cpuset.cpus
echo 0-1 > cpuset.mems
echo pid >> tasks
# 通过taskset查看pid运行在哪个cpu上
taskset -c -p pid
示例限制进程pid能够运行在0-4号cpu上。
另外,如果进程发生重启,pid更换后,需要配置对应的pid到tasks文件中
3.1.3 限制进程的memory资源
# 为对应的进程配置cpu的cgroup子系统
cd /sys/fs/cgroup/memory/
# 根据情况创建相关目录
mkdir 1687270562000004809
# 配置memory使用情况,并绑定对应的进程pid
echo 16777216000 > memory.limit_in_bytes
echo pid >> tasks
示例限制进程pid能够最多能够使用16g内存,如果内存申请的内存找过16g,linux会将其杀死。
另外,如果进程发生重启,pid更换后,需要配置对应的pid到tasks文件中
3.2 进程性能测试
模拟程序
while true;do echo hello;done
对应的pid 14686
-
启动后,对应的程序cpu使用率100%,表示占用1c的cpu
没有绑定到固定的cpu
-
绑定到一个固定的cpu
# 为对应的进程配置cpu的cgroup子系统
cd /sys/fs/cgroup/cpuset/
# 根据情况创建相关目录
mkdir 1687270562000004809
# 配置cpu和mem运行的核心数,并绑定对应的cpu
echo 0 > cpuset.cpus
echo 0-1 > cpuset.mems
echo 14686 >> tasks
# 通过taskset查看pid运行在哪个cpu上
taskset -c -p 14686
进程确实运行在了0号进程,并且把cpu完全占用
- 限制cpu使用率
# 为对应的进程配置cpu的cgroup子系统
cd /sys/fs/cgroup/cpu
# 根据情况创建相关目录
mkdir 1687270562000004809
# 配置cpu使用情况,并绑定对应的进程pid
echo 20000 > cpu.cfs_quota_us
echo 100000 > cpu.cfs_period_us
echo 14686 >> tasks
限制cpu使用是0.2c,相关的cpu使用率应该不高于20%
- 限制内存
由于该用例是cpu型,对内存测试不是非常使用,因此更换一个用例
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define CHUNK_SIZE 1024 * 1024 * 100
int main()
{
char *p;
int i;
for(i = 0; i < 20; i ++)
{
p = malloc(CHUNK_SIZE);
if(p == NULL)
{
printf("malloc error!");
return 0;
}
memset(p, 0, CHUNK_SIZE);
printf("malloc memory %d MB\n", (i + 1) * 100);
sleep(10);
}
while(1)
{
sleep(1);
}
return 0;
}
# 执行编译
gcc mem.c -o mem
# 执行命令
./mem
每10s申请100m内存
进程pid是9011
限制进程的内存
# 为对应的进程配置cpu的cgroup子系统
cd /sys/fs/cgroup/memory/
# 根据情况创建相关目录
mkdir 1687270562000004809
# 配置memory使用情况,并绑定对应的进程pid
echo 1073741824 > memory.limit_in_bytes
echo 9011 >> tasks
内存超过1g后,相关进程被杀死
4. 疑问和思考
4.1 为什么通常不进行磁盘io隔离?
以k8s集群为例,通常是多个pod运行在相同的node机器上,最常见的情况是,一些pod没有严格控制日志/标注输出的打印,日志/标注输出打印过多,从而导致大量占用磁盘io,将磁盘的io吃满,最终会影响到其他pod的正常的磁盘io读写。
那为什么k8s还没有针对磁盘的io进行cgroup限制呢? 原因是,磁盘io针对不同的磁盘设备表现不同,并且相关指标的检测的准确度、检测延迟以及磁盘io能够承担的磁盘极限有很大的不确定性,针对不同的磁盘io设备难以评估不同设备的磁盘iops,因此难以针对磁盘io进行限制。
5. 参考文档
暂无