Linux cgroup技术解析和验证测试

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,如下所示:

Namespaceclone() 使用的 flag所隔离的资源
CgroupCLONE_NEWCGROUPCgroup 根目录
IPCCLONE_NEWIPCSystem V IPC,POSIX 消息队列
NetworkCLONE_NEWNET网络设备、协议栈、端口等
MountCLONE_NEWNS挂载点
PIDCLONE_NEWPID进程 ID
UserCLONE_NEWUSER用户和组 ID
UTSCLONE_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

  1. 启动后,对应的程序cpu使用率100%,表示占用1c的cpu
    在这里插入图片描述
    没有绑定到固定的cpu
    在这里插入图片描述

  2. 绑定到一个固定的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完全占用
在这里插入图片描述

  1. 限制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%
在这里插入图片描述

  1. 限制内存
    由于该用例是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. 参考文档

暂无

  • 21
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值