Linux cgroup实用参考
2022-07-26 14:18:56 阿炯

本文对linux下cgroup(Control Group)在使用过程中的案例场景,拟通过使用方式、RPM系下的使用、DEB系的使用、各个子控制组的使用来展开介绍,同时会辅以使用方式中方法在不同软件包派系中的情况做穿插介绍,当然命令行的方式依然是主流。

控制组通常称为cgroup是Linux内核提供的一种功能,用于管理、限制和审核进程组。与nice类命令或在/etc/security/limits.conf进行设置等其他方法相比,cgroup更灵活,因为它们可以操作子进程集可能在不同的系统用户下,提供更为精细且全方面的控制集。本文主要通过实例来讲解各个子控制组的场景与用法,连带提供一些关联知识简介或链接;cgroup本身是一项目较复杂的系统性工程,涉及了操作系统的各个主要方面,作者又想将每个知识点做详解,因此文章的部分地方显得冗余,甚至啰嗦。

Linux的cgroup详细介绍
Linux cgroup使用参考
RedHat系列Linux服务控制
Debian下服务控制命令


1、原生的命令行操作:文件系统操作工具如ls、echo、cat、cd等指令来进行目录层级的切换,键值的修改;或直接通过cgcreate、cgset、cgexec和cgclassify之类的工具来实现。
2、Systemd的再封装:按systemd中的原生指令来配置实施。
3、使用规则应用守护进程(cgconfig与cgred)。

业务普遍不推荐上述原生(手动创建与后规则应用服务)与systemd这两种方式混用!

分别有如下的方式

1)、通过直接访问cgroup文件系统
以常见文件系统操作工具如ls,echo,cat,cd等指令来进行目录层级的切换,键值的修改。

2)、通过诸如cgcreate、cgset、cgexec和cgclassify之类的工具来实现cgrouplibcgroup包的一部分:
cgcreate -a user -t user -g memory,cpu:groupname

cgroup是层次的,可以在其下创建子控制组
cgcreate -g memory,cpu:groupname/foo
cgexec -g memory,cpu:groupname/foo bar

可以通过echo直接修改其中的变量值
将内存限制在10M
echo 10000000 > /sys/fs/cgroup/memory/groupname/foo/memory.limit_in_bytes

将CPU的占用量控制在10%左右(100/1024)
echo 100 > /sys/fs/cgroup/cpu/groupname/foo/cpu.shares

或通过cgset来进行设定,cgget来取得其中的项与值。

对已经在运行中的进程进行事后的控制
$ pidof bash
1327 1328
$ cgclassify -g memory,cpu:groupname/foo `pidof bash`
$ more /proc/1328/cgroup
...
11:memory:/groupname/foo
6:cpu:/groupname/foo

也可用echo来代替cgclassify(只能一次指定一个进程号)
echo pid > /sys/fs/cgroup/memory/groupname/cgroup.procs

如果想固化这些设置,可将相关的设定保存在配置文件中(即上述2、3中所述),再由相关的守护进程读取生效,如下文。

3)、使用systemd单元文件中的指令来指定服务和切片的限制(slices)

使用指令:systemctl status or systemd-cgls,systemd-cgtop类似于系统的top指令来查看:
Control Group    Tasks   %CPU   Memory  Input/s Output/s
user.slice    540  152,8     3.3G        -        -
user.slice/user-1000.slice    540  152,8     3.3G        -        -
user.slice/u…000.slice/session-1.scope     425  149,5     3.1G        -        -
system.slice    37      -   215.6M        -        -

systemd.slice(5)中的systemd单元文件可用于定义自定义cgroup配置,它们必须放在systemd目录中,例如/etc/systemd/system/,可以分配的资源控制选项记录可参考systemd.resource-control(5)。

下述的示例slice unit仅允许30%的单CPU使用量
/etc/systemd/system/my.slice
[Slice]
CPUQuota=30%

要记得重新加载守护进程(daemon-reload)以获取任何新的或更改过的.slice文件。

Service unit file
在文件中直接定义
[Service]
MemoryMax=1G

限制服务只能使用不超过1gigabyte的内存。

Grouping unit under a slice
[Service]
Slice=my.slice

另外还可以为用户指定规则集,方便在系统启动时自动适配;用户也可以自定义限制规则集。

用户为matlab自定义systemd slice
~/.config/systemd/user/matlab.slice

[Slice]
AllowedCPUs=0-5
MemoryHigh=5G

启动matlab
systemd-run --user --slice=matlab.slice /opt/MATLAB/bin/matlab -desktop

4)、使用规则守护进程

使用“规则引擎守护进程”自动将某些用户/组/命令移动到组/etc/cgrules.conf和cgcconfig.service:
首先在cgcconfig.service的配置文件(/etc/cgconfig.conf )定义控制规则;
完成后在cgred.service(cgrulesengd daemon)的配置文件(/etc/cgrules.conf)定义如何将控制规则适配到具体的应用上。

完成上述两步后,依次启动它们(有配置文件的语法检查),就可将相关的应用适配到对应的控制规则中(即使应用服务先于规则守护进程的启动)。

/etc/cgconfig.conf

group groupname {
  perm {
# who can manage limits
    admin {
      uid = $USER;
      gid = $GROUP;
    }
# who can add tasks to this group
    task {
      uid = $USER;
      gid = $GROUP;
    }
  }
# create this group in cpu and memory controllers
  cpu { }
  memory { }
}

group groupname/foo {
  cpu {
    cpu.shares = 100;
  }
  memory {
    memory.limit_in_bytes = 10000000;
  }
}

接上文的matlab,从systemd到/etc/cgconfig.conf

group matlab {
    perm {
        admin {
            uid = username;
        }
        task {
            uid = username;
        }
    }
    cpuset {
        cpuset.mems="0";
        cpuset.cpus="0-5";
    }
    memory {
        memory.limit_in_bytes = 5000000000;
    }
}

开始使用前都应该进行配置文件在语法设定上的检查
cgconfigparser -l /etc/cgconfig.conf

cgrulesengd - control group rules daemon(/etc/cgrules.conf)

使用'-d'来开启调试模式,相当于'-nvvvf -',将所有的日志信息打印到标准输出上。

当然也是可以加入到/etc/rc.local中以开机最后适用或做成相关的服务。

这里没有使用cgred.service来为应用适配控制规则,而是用指令来手动操作(不推荐):
cgexec -g memory,cpuset:matlab /opt/MATLAB/bin/matlab -desktop

查看进程的cgroup相关设定可以通过其/proc/PID/cgroup
more /proc/self/cgroup
more /proc/1/mounts

在root用户下使用systemd-run可在指定的slice中运行指令
# systemd-run --slice=my.slice command

以指定的用户的执行相关的指令:--uid=username
# systemd-run --uid=username --slice=my.slice command

使用非root的普通用户

如果满足某些条件,非特权用户可以将提供的资源划分为新的cGroup,非root用户必须使用cgroupv2才能管理cgroup资源。再看示例:

-------------------------------
/etc/cgconfig.conf
group lcatlimit {
    cpu {
        cpu.shares = 320;
        cpu.cfs_quota_us = 750000;
        cpu.cfs_period_us = 100000;
    }
}

-------------------------------
/etc/cgrules.conf
freeoa:godamtomcat  cpu    lcatlimit

# systemctl status cgred
● cgred.service - CGroups Rules Engine Daemon
   Loaded: loaded (/usr/lib/systemd/system/cgred.service; disabled; vendor preset: disabled)
   Active: inactive (dead)

# systemctl start cgred

# systemctl status cgred
● cgred.service - CGroups Rules Engine Daemon
   Loaded: loaded (/usr/lib/systemd/system/cgred.service; disabled; vendor preset: disabled)
   Active: active (running) since Mon 2022-03-28 11:04:37 CST; 9s ago
  Process: 7135 ExecStart=/usr/sbin/cgrulesengd $OPTIONS (code=exited, status=0/SUCCESS)
 Main PID: 7136 (cgrulesengd)
   CGroup: /system.slice/cgred.service
           └─7136 /usr/sbin/cgrulesengd -s -g cgred

Mar 28 11:04:37 freeoahost systemd[1]: Starting CGroups Rules Engine Daemon...
Mar 28 11:04:37 freeoahost systemd[1]: Started CGroups Rules Engine Daemon.

使用lscgroup查看时依然没有cgconfig.conf或cgrules.conf中所定义的内容

# systemctl status cgconfig
● cgconfig.service - Control Group configuration service
   Loaded: loaded (/usr/lib/systemd/system/cgconfig.service; disabled; vendor preset: disabled)
   Active: inactive (dead)

# systemctl start cgconfig
# systemctl status cgconfig
● cgconfig.service - Control Group configuration service
   Loaded: loaded (/usr/lib/systemd/system/cgconfig.service; disabled; vendor preset: disabled)
   Active: active (exited) since Mon 2022-03-28 11:08:08 CST; 2s ago
  Process: 7595 ExecStart=/usr/sbin/cgconfigparser -l /etc/cgconfig.conf -L /etc/cgconfig.d -s 1664 (code=exited, status=0/SUCCESS)
 Main PID: 7595 (code=exited, status=0/SUCCESS)

Mar 28 11:08:07 freeoahost systemd[1]: Starting Control Group configuration service...
Mar 28 11:08:08 freeoahost systemd[1]: Started Control Group configuration service.

终于有相关的限制组的输出了
# lscgroup|grep cat
cpu,cpuacct:/lcatlimit

systemctl restart cgconfig  && systemctl restart cgred

验证:
手动对进程适用控制组在/proc中的cgroup查看

# more /proc/8073/cgroup
...
2:cpuacct,cpu:/
1:name=systemd:/user.slice/user-0.slice/session-1050377.scope

# more /proc/8073/cgroup
...
2:cpuacct,cpu:/lcatlimit
1:name=systemd:/user.slice/user-0.slice/session-1050377.scope

虽然被指定使用cgroup的普通用户在使用时可能会报失败,多是因为权限的问题:用户甚至可能无法访问规则组,可在创建规则组时指定相关用户或组即可。

cgexec -g cpuacct,cpu:/lcatlimit
Cgroups error: cgroup change of group failed
Specifying users and groups works fine when you create a subsystem.

cgcreate -t $USER:$USER -a $USER:$USER  -g cpu:testgroupcpu

为指定用户限制其所能使用的内存大小,可在cgroups中设定:
/etc/cgconfig.conf:
group memlimit {
    memory {
        memory.limit_in_bytes = 4294967296;
    }
}

上面创建了一个最大可用内存限制为4GiB的cgroup,接下来在规则组中进行适配:
/etc/cgrules.conf:
luser   memory   memlimit/

这将导致用户luser发起的所有进程运行在cgroup.conf中所创建的memlimit 控制组下,而不是手动设定。

cgred.conf与cgrules.conf的关系可从运行后的守护进程运行情况看出:
/usr/sbin/cgrulesengd -s -g cgred

5)、控制器类型与用户特性集
ControllerCan be controlled by userOptions
cpuRequires delegationCPUAccounting, CPUWeight, CPUQuota, AllowedCPUs, AllowedMemoryNodes
ioRequires delegationIOWeight, IOReadBandwidthMax, IOWriteBandwidthMax, IODeviceLatencyTargetSec
memoryYesMemoryLow, MemoryHigh, MemoryMax, MemorySwapMax
pidsYesTasksMax
rdmaNo?
eBPFNoIPAddressDeny, DeviceAllow, DevicePolicy


6)、清理或删除控制组

清理使用cgclear,删除规则使用cgdelete。cgclear清理用于卸载控制组的虚拟文件系统,可以使用'-l'参数来指定配置文件,以卸载其中定义的控制组规则,相当于从内核中卸载相关的控制组功能;cgdelete用于删除指定的控制规则组,如:cgdelete -g cpu:/lcwlimit。


7)、相关测试验证配合工具

配置工具(rpm、deb)
# yum install -y libcgroup-tools libcgroup
# apt-get install cgroup-tools cgroupfs-mount

安装完成后,相关的系统指令是以cg打头的,可以在系统终端上列出:
cgclassify    cgclear    cgconfigparser    cgcreate    cgdelete    cgexec    cgget    cgrulesengd    cgset    cgsnapshot

测试工具
stress-ng、matho-primes:主要用于CPU此类的计算资源
hdparm、fio、dd:硬盘I/O测试工具
memalct:C编写的内存分配工具(详见后文附件)

stress-ng -c 2 --cpu-method all -t 5m
stress-ng -c 2 --cpu-method all --timeout 300

stress-ng在内存使用上的测试不及预期(cpu的验证比matho-primes要好用),建议直接用c来写内存分配程序来测试。


使用实例选

----------------------------------------------------------------
1、CPU的使用限制

控制本机上的redis服务只能使用90%的cpu资源

#首先创建组
mkdir /sys/fs/cgroup/cpu/rdsoplimit
#90%使用率
echo 100000 > /sys/fs/cgroup/cpu/rdsoplimit/cpu.cfs_period_us
echo 90000 > /sys/fs/cgroup/cpu/rdsoplimit/cpu.cfs_quota_us
#测试
/usr/bin/cgexec -g cpu:rdsoplimit top
#上述测试没有问题,就将其适配到对应进程(PID)上
/usr/bin/cgclassify -g cpu:rdsoplimit 9344

注:老版本的redis是单线程的,通过上述的设置限制了其只能使用单核的90%的cpu算力。

#限制数据库进程使用系统16核中的14个核的100%使用率
echo 100000 > /sys/fs/cgroup/cpu/sqlimit/cpu.cfs_period_us
echo 1400000 > /sys/fs/cgroup/cpu/sqlimit/cpu.cfs_quota_us
/usr/bin/cgclassify -g cpu:sqlimit PID

上述操作非常适用于在终端下进行的即时+临时操作,在应用重启后或机器重启后就不再有效(因为应用的进程ID号会变化且手动的设定也会丢失)。如果想要从开机就对相关的应用或用户进程进行限制,就很有必要固化相关设置到配置文件中并开机运行。

另外可以直接通过将应用绑定到对应的cpu核心上,让其只能使用到被指定的核心上,全部使用该核心的算力资源(下文有示例),绑定cpu还涉及到Linux下的NUMA机制,后方会有提及。

Linux cgroup允许从用户组或进程的角度为其所能使用到的系统资源做限制。也可以使用cgconfig服务配置cgroup来控制CPU使用量,在/etc/cgconfig.conf中为每个服务节点上设置不限于CPU使用方面的限制。cgconfig服务用于创建的cgroup和管理子系统,它可以配置为在引导时启动并重新建立预定义的cgroup,从而使它们在重新引导后保持不变。启动该cgconfig服务会创建一个/cgroup与所有子系统一起安装的虚拟文件系统。可以运行'lscgroup'命令来验证。

Soft Limit Parameter
可以使用cpu.shares来设置软限制。该参数为整数值且必须大于等于2,用于指定cgroup中任务可用CPU时间的相对份额。例如,有两个任务要使用cpu且其cpu.shares设置为100,表示每个任务使用一半的CPU时间。设置软限制时,如果系统上有额外可用的CPU,则可能会超过分配的CPU时间片;应用进程可以继续使用CPU,直到与其他进程争用CPU达到限定或达到硬限制。

Hard Limit Parameters
设置了进程可以使用CPU时间片的硬限制,即cpu.cfs_period_us 和 cpu.cfs_quota_us参数。

1.cpu.cfs_period_us Specifies a period in microseconds (represented by us for µs) to indicate how often a cgroup's access to CPU resources should be reallocated. For example, if you want tasks in a cgroup to have access to a single CPU for 0.2 seconds in a 1 second window, set cpu.cfs_quota_us to 200000 and cpu.cfs_period_us to 1000000. The upper limit of the cpu.cfs_quota_us parameter is 1 second and the lower limit is 1000 microseconds.

1.cpu.cfs_period_us指定一个以微秒为单位的周期用us表示µs,以指示cgroup对CPU资源的访问应该重新分配的频率。例如,若希望cgroup中的任务在1秒窗口内访问单个CPU 0.2秒,请设置 cpu.cfs_quota_us配额为200000和cpu.cfs_period_us为1000000。cpu的cpu.cfs_quota_us参数上限为1秒,下限为1000微秒。

2.cpu.cfs_quota_us Specifies the total amount of runtime in microseconds (represented by us for µs), for which all tasks in the Drill cgroup can run during one period (as defined by cpu.cfs_period_us). When tasks in the Drill cgroup use up all the time specified by the quota, the tasks are throttled for the remainder of the time specified by the period and they cannot run until the next period. For example, if tasks in the Drill cgroup can access a single CPU for 0.2 seconds out of every 1 second, set cpu.cfs_quota_us to 200000 and cpu.cfs_period_us to 1000000. Setting the cpu.cfs_quota_us value to -1 indicates that the group does not have any restrictions on CPU. This is the default value for every cgroup, except for the root cgroup.

2.cpu.cfs_quota_us指定以微秒为单位的运行时间片总量用us表示µs,对于该总量,cgroup中的所有任务都可以在一个时段内运行由cpu.cfs_period_us定义。当应用中cgroup中的任务用完配额指定的所有时间片时,这些任务将在时段指定的剩余时间内被限制使用时间片,直到下一个时段才能继续运行相当于被暂停。例如,应用的cgroup中的任务每1秒可以访问一个CPU 0.2秒,请设置cpu.cfs_quota_us配额为200000和cpu.cfs_period_us为1000000。设置cpu.cfs_quota_us的值为-1时表示组对CPU时间片的使用没有任何限制。这是每个cgroup除根cgroup外的默认值。

配置CPU限制

Complete the following steps to set a hard and/or soft CPU limit for the Drill process running on the node:
完成以下步骤,为节点上运行的钻取进程设置硬和/或软CPU限制:

在cgroup的配置文件(/etc/cgconfig.conf)中加入控制规则

group drillcpu {
    cpu {
        cpu.shares = 320;
        cpu.cfs_quota_us = 400000;
        cpu.cfs_period_us = 100000;
    }
}

cpu.shares就是上文所提及的软限制,其它两个参数:cpu.cfs_quota_us 与 cpu.cfs_period_us就是硬限制,如果只想用其一,可删除其中一种限制。

设置软限制时,将特定数量的CPU共享分配给配置中的应用cgroup,计算CPU共享可参照:
1024x(CPU allocated to APP/Total available CPU)

因此,上述的CPU shares计算如下:
1024x(10/32) = 320

当设置了硬限制:即cpu.cfs_quota_us 与 cpu.cfs_period_us参数,如上述的设置表示应用进程仅可以全量使用4个CPU。

如果想临时在会话级使用cgroup的硬设定,可以做如下操作:
echo 400000 > /sys/fs/cgroup/cpu/appcpu/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/appcpu/cpu.cfs_period_us

在某控制组目录下的设定操作(这里使用了tee指令来实现了追加增补)

# echo 0 | tee -a cpuset.cpus

need a memory node as well
# echo 0 | tee -a cpuset.mems

And we can attach our process to the newly created cpuset
# echo 4367 | tee -a tasks

group nolimit {
cpuset {
# No alternate memory nodes if the system is not NUMA
cpuset.mems="0";
# Make my 2 CPU cores available to tasks
cpuset.cpus="0,1";
}
}

使用taskset查询进程对cpu的亲和度
# taskset -p 971
pid 971's current affinity mask: 3

使用'taskset -cp'可以获得进程所绑定到的处理器列表:
# taskset -cp 971
pid 971's current affinity list: 0,1

CPU亲和性(affinity)表示为位掩码,最低阶位对应于第一个逻辑CPU,最高阶位对应于最后一个逻辑CPU。

有两个CPU 0x01和0x10,分配给进程,与掩码0x11匹配后(基于16进制)是3。

再举一个例子来更好地解释它。如果有8个处理器,它们将按以下方式编号:
00000001
00000010
00000100
00001000
00010000
00100000
01000000
10000000

if your process has access to the 8 of them, the mask will be 11111111, which is 0x1 + 0x2 + 0x4 + 0x8 + 0x10 + 0x20 + 0x40 + 0x80 = 0xff in base 16.

if your process has only access to the 1st and 2nd one, as it was the case for me the mask is 11 or 3 in base 16.

now if the process has only access to the 1st and the 8th one, the mask would be 10000001, which is 0x51 in base 16.

使用taskset -p指令将进程绑定到第一个处理器:
# taskset -p 1 971
pid 971's current affinity mask: 3
pid 971's new affinity mask: 1

如果想运行一个新命令,而不是对现有命令进行操作,这实际上是默认的行为且并不复杂:taskset [参数]

使用cgroup对本地cpu核心的绑定测试

使用stress-ng对有16核心cpu使用量的测试过程
(1).numactl查看内存分配
numactl --hardware

(2).创建规则组
#为普通用户创建规则组
cgcreate -a freeoa:freeoa -t freeoa:freeoa -g cpuset:/cpuslit
#设定应用只能第6至9这4个核
cgset -r cpuset.cpus=6-9 cpuslit
#设定内存节点,依据上述的numactl结果
cgset -r cpuset.mems=0 cpuslit

可在该用户下查看设定:cgget -g cpuset:/cpuslit
如果出现:cgroup change of group failed之类的错误很可能系权限问题。
cgset Error changing group of pid XXX: No space left on device,该问题的报错是一个误导:多是cpuset.mems没有被设置。。与cpuset.cpus设置一样,最好是显式地设置该选项。

(3).拉起应用
cgexec -g cpuset:/cpuslit  stress-ng -c 6 --cpu-method all -t 5m

然后通过top指令观察(top界面按数字1即可看到所有cpu的使用统计):stress-ng只会占用第6到9核心,基本上的us为100%;系统中有6个stress-ng进程,前两个进程基本上可以使用到100%的cpu,后面4个进程的各自的cpu使用量基本上50%左右浮动。达到上述要求的设定。

注意:这种绑定到cpu核心貌似对已经运行的进程来设定不怎么能起到作用(cgclassify)。如果是多任务与多核心的场景,还是推荐使用绑定到cpu的方式。

----------------------------------------------------------------
2、内存控制

内存分为物理与虚拟交换内存,在设定时需要对两同时进行设定,这点要注意;通常与CPU一起做成计算型资源的限制。

#创建分组
# cgcreate -g cpu,cpuset,memory:freeoacg10

#设置限制使用cpu0
# cgset -r cpuset.cpus=0 freeoacg10

#设置限制cpu最多用到50%
# cgset -r cpu.cfs_period_us=100000 freeoacg10
# cgset -r cpu.cfs_quota_us=50000 freeoacg10

#设置限制内存最大使用大小,单位默认是byte,在此限制最多用256M
# cgset -r memory.limit_in_bytes=268435456 freeoacg10

测试,使用top工具可以看到下面的matho-primes进程使用了第一个cpu,50%的资源
# cgexec -g cpuset,cpu,memory:freeoacg10 matho-primes 0 999999999 > /dev/null &

报错:cgroup change of group failed,后逐个测试,发现系cpuset在其中组合时出错。

发现在测试内存分配时遇到的问题:程序在分配内存时并没有按预期的进行,能突破内存的限制;通过监控观察发现:物理内存随着时间在使用上有所上升,但交换虚拟内存的使用上升的更快,同时也会导致该交换分区所在硬盘写IO同步增长vm.swappiness参数的设置似乎直接的内存请求分配没有起到应有的作用。在些可以推断:因使用了交换分区,导致程序在请求分配内存时没有按cgroup中的设定来进行限制,将内存的请求放到交换分区中,尽管操作系统中还有足够多的物理内存;使用'swapoff -a'关闭主机所有的交换分区后,再运行该内存分配程序,即可达到预期。

cgcreate -t freeoa:freeoa -a freeoa:freeoa -g memory:/cgex
cgset -r memory.limit_in_bytes=268435456 cgex
cgset -r memory.memsw.limit_in_bytes=268435456 cgex

上述的cgroup中设定的内存(物理+交换)阀值就是1g,并不是物理内存+交换内存的2g!这里一定要注意,另外要注意的是交换内存的大小要大于等于物理内存的限制值。英文的原文如下:
memory.limit_in_bytes must be set before memory.memsw.limit_in_bytes, and memory.memsw.limit_in_bytes must be bigger than or equal to memory.limit_in_bytes.

可在cgconfig.conf中进行定义:
group group1 {
  memory {
    memory.limit_in_bytes = 512m;
    memory.memsw.limit_in_bytes = 512m;
  }
}

下文还会对此种方式进行细讲;如果不想这么幸苦的限制资源,优雅的中止那些资源使用很高的应用,可以参考使用Early OOM

----------------------------------------------------------------
3、硬盘IO

记住:任何对IO的重新测试都必须对系统的缓存进行全量的清空:
echo 3 > /proc/sys/vm/drop_caches

否则,测试可能是无效的!

# hdparm --direct -t /dev/sda
/dev/sda:
Timing O_DIRECT disk reads: 218 MB in  3.02 seconds =  72.22 MB/sec

依次启动cgconfig与cgred服务后再来测试

# hdparm --direct -t /dev/sda
/dev/sda:
Timing O_DIRECT disk reads:  12 MB in  3.03 seconds =   3.96 MB/sec

使用cgroup限制对本地硬盘的IO

找到硬盘对应的系统编号:<major>:<minor>

只能对硬盘设定IO限制,不能对分区设定,且对分区意义不大;后将要设定的阀值写入对应的限制项中,格式如下:"<major>:<minor> <bytes_per_second>"。

设备编号<major>:<minor>可从虚拟文件
/proc/diskstats
/proc/partitions
/sys/block/device_name/stat
或指令lsblk(第二列)中取得:TYPE为'disk'的MAJ:MIN值,直接复制出来就可用。限制值bytes_per_second为标准计算机的IO单位:1048576即为1MB/s。

这里限制了硬盘vdb的读取速率不超过100MB/s
#limit 100MB/s for vdb
# echo "252:16 104857600" > /sys/fs/cgroup/blkio/blkio.throttle.read_bps_device

#limit4vdb-read-speed:400MB/s by freeoa.net
/usr/bin/echo "252:16 419430400" > /sys/fs/cgroup/blkio/blkio.throttle.read_bps_device

Using fio with no buffering and directio I was able to throttle the iops to a device to 10 per second using
echo 8:32 10 > /sys/fs/cgroup/blkio/user.slice/blkio.throttle.{read|write}_iops_device

blkio.throttle.{read|write}_bps_device:对该硬盘设备的读|写的IO速率限制。

注意:这种限制是直接对硬盘进行限制,从使用者的角度讲是不可取的,甚至抑制了先进硬件的功能。应该与应用进行绑定,针对性的限制应用对IO的过度使用。

测试过程硬盘io没有cpu与内存那样的稳定?从网络写到本地的IO在cgroup中为什么老是测试通不过???

在测试过程中从本地读取并写向网络没有问题,但从网络读取写向本地却不能适配上cgroup规则???

将控制组中的读写设置为10MB/s(10485760)
cgcreate -a hto:hto -t hto:hto -g blkio:/lmtdskio
cgset -r blkio.throttle.read_bps_device="8:16 10485760" lmtdskio
cgset -r blkio.throttle.write_bps_device="8:16 10485760" lmtdskio

cgget -g blkio:/lmtdskio
cgexec -g blkio:/lmtdskio cmd

两块盘的通过网络写到本地盘没有能实现,但读确实基本上做到了。。。

#limit4vdb-read-speed:10MB/s
/usr/bin/echo "8:0 10485760" > /sys/fs/cgroup/blkio/blkio.throttle.read_bps_device
/usr/bin/echo "8:16 10485760" > /sys/fs/cgroup/blkio/blkio.throttle.read_bps_device

这种方法确实实现了读限速的功能,但是这是全局的限制,主动制约系统设备的性能是与调优相悖的;需要的是针对具体应用的限制,而非这种全局的限制,做到较为精准的设定控制。。。

小结:直接通过echo方式来设定blkio/blkio.throttle是全局的方式且能正常发挥作用,但通过设定好控制组,再将应用适配到控制组时发现对写的限制未及预期(在cgconfig中写入多块硬盘的IO限制时还容易报重复键冲突),通道是Linux对写操作还有特殊的控制:写是写到内存缓冲区,然后才是统一到写到磁盘中?

还真是这样的。

Linux的I/O子系统是的复杂度一点都不输入(虚拟)内存管理,甚至过之。

一些常用的IO工具测试:
dd if=/dev/device iflag=direct bs=1M
dd iflag=direct if=~/freeoa.iso | pv | dd oflag=direct of=/dev/sdb bs=8M

Direct I/O (open mode O_DIRECT)
强行清除各类缓存(caches)
sync && echo 3 > /proc/sys/vm/drop_caches

网上有说关闭硬盘的写缓存可以测试(但实测没能通过)
hdparm -W 0 /dev/sda

#开启硬盘的写缓存
hdparm -W 1 /dev/sda

另外可在文件系统挂载(/etc/fstab)时指定'sync'参数,可以关闭写缓冲(buffering),实测通过了上述的cgroup写限制测试。
/dev/sdb    /freeoa    xfs        sync    0   0

关于Linux下文件写缓存多说两名:
文件的读写方式各有千秋,对于文件的 I/O 分类也非常多,如硬盘自己的缓存、I/O硬件子系统(RAID)、VFS文件系统等,常见的有:
缓冲与非缓冲 I/O 与 直接与非直接 I/O
阻塞与非阻塞 I/O 与 同步与异步 I/O

缓冲与非缓冲 I/O
文件操作的标准库是可以实现数据的缓存,那么根据「是否利用标准库缓冲」,可以把文件 I/O 分为缓冲 I/O 和非缓冲 I/O:
缓冲 I/O,利用的是标准库的缓存实现文件的加速访问,而标准库再通过系统调用访问文件;
非缓冲 I/O,直接通过系统调用访问文件,不经过标准库缓存。

这里所说的「缓冲」特指标准库内部实现的缓冲。如很多程序遇到换行时才真正输出,而换行前的内容,其实就是被标准库暂时缓存了起来,这样做的目的是,减少系统调用的次数,毕竟系统调用是有 CPU 上下文切换的开销的。

直接与非直接 I/O
我们都知道磁盘 I/O 是非常慢的,所以 Linux 内核为了减少磁盘 I/O 次数,在系统调用后,会把用户数据拷贝到内核中缓存起来,这个内核缓存空间也就是「页缓存」,只有当缓存满足某些条件的时候,才发起磁盘 I/O 的请求。那么,根据是「否利用操作系统的缓存」,可以把文件 I/O 分为直接 I/O 与非直接 I/O:
直接 I/O,不会发生内核缓存和用户程序之间数据复制,而是直接经过文件系统访问磁盘;
非直接 I/O,读操作时,数据从内核缓存中拷贝给用户程序,写操作时,数据从用户程序拷贝给内核缓存,再由内核决定什么时候写入数据到磁盘。

如果在使用文件操作类的系统调用函数时,指定了 O_DIRECT 标志,则表示使用直接 I/O。如果没有设置过则默认使用的是非直接 I/O。如果用了非直接 I/O 进行写数据操作,内核什么情况下才会把缓存数据写入到磁盘?以下几种场景会触发内核缓存的数据写入磁盘:
在调用 write 的最后,当发现内核缓存的数据太多的时候,内核会把数据写到磁盘上;
用户主动调用 sync,内核缓存会刷到磁盘上;
当内存十分紧张,无法再分配页面时,也会把内核缓存的数据刷到磁盘上;
内核缓存的数据的缓存时间超过某个时间时,也会把数据刷到磁盘上。

更多详情请参考:
Linux IO 子系统
详解Linux的IO调度

----------------------------------------------------------------
CentOS7下通过systemd来操作Cgroup

cgroups子系统实现了一种新的虚拟文件系统VFS类型,名为“cgroups”。所有cgroup操作可以由文件系统操作来完成,例如在cgroup文件系统中创建cgroup目录、写入或读取这些目录中的条目、挂载cgroup文件系统等。默认情况下,systemd自动创建slice、范围和服务单元的层次结构,为cgroup树提供统一的结构。服务、作用域和slice可由系统管理员手动创建,或由程序动态创建。默认情况下,操作系统定义了运行系统所需的许多内置服务。此外,默认情况下创建了四个slices:
-.slice — the root slice;
system.slice — the default place for all system services;
user.slice — the default place for all user sessions;
machine.slice — the default place for all virtual machines and Linux containers.

创建自定义slice

slice单元的每个名称都对应于层次中某个位置的路径。
子slice将继承父slice的设置。
破折号"-"字符用作路径组件的分隔符。

通过在“/etc/systemd/system.conf”中启用以下值来启用slice中的统计功能(含该功能模块)
DefaultCPUAccounting=yes
DefaultBlockIOAccounting=yes
DefaultMemoryAccounting=yes

重新启动节点以激活更改。

现在为systemd单元文件进一步保留使用CPU的CPUShares。

# cat /etc/systemd/system/stress2.service
[Unit]
Description=Put some stress

[Service]
CPUShares=1024
Type=Simple
ExecStart=...

监视每slice的CPU使用率

systemd-cgtop显示本地Linux控制组层次结构的顶部控制组,按其CPU、内存或磁盘I/O负载排序。定期刷新默认情况下每1s刷新一次,样式与top命令类似。资源使用率仅考虑相关层次结构中的控制组,即CPU使用率仅考虑“cpuacct”层次结构中的控制组,内存使用率仅考虑“内存”中的控制组,磁盘i/O使用率仅考虑“blkio”中的控制组。

当环境与工具准备就位后,就可以在cgroup的相关配置文件(/etc/cgconfig.conf)中定义了:
group app/editor {
  cpu {
    cpu.shares = 500;
  }
  memory {
    memory.limit_in_bytes = 1000000000;
  }
}

group app/browser {
  cpu {
    cpu.shares = 300;
  }
  memory {
    memory.limit_in_bytes = 1000000000;
  }
}

group app/util {
  cpu {
    cpu.shares = 300;
  }
  memory {
    memory.limit_in_bytes = 500000000;
  }
}

cpu.shares的最大值为1000,配置文件中设定为300表示了该限制最多能分到30%的CPU算力,memory.limit_in_bytes同理,但这里没有对虚拟内存进行设定。

设置完成上述的控制组后,接下来就应该来 设置适配的规则组了(/etc/cgrules.conf):
*:emacs         cpu,memory      app/editor/
*:conkeror      cpu,memory      app/browser/
*:firefox       cpu,memory      app/browser/
*:slack         cpu,memory      app/util/
*:dropbox       cpu,memory      app/util/

检查相关的规则是否适用到应用之上

cat /proc/`pidof dropbox`/cgroup | grep app
...
# 5:memory:/app/util
# 2:cpu,cpuacct:/app/util

使用smem来查看应用的内存使用情况

smem -P dropbox
...
# PID User     Command                         Swap      USS      PSS      RSS
# 1955 freeoa /home/freeoa/.dropbox-dis        0   191800   192388   201124

或借助于systemd来实现,下面在Debian 10 Buster上限制或禁用虚拟内存交换,可在内核引导参数上开启虚拟内存审计:
systemd.unified_cgroup_hierarchy=1 swapaccount=1

# vim /etc/default/grug
GRUB_CMDLINE_LINUX_DEFAULT="quiet systemd.unified_cgroup_hierarchy=1 swapaccount=1"

保存后执行update-grub2,重启后使之生效。

注意:多数情况下不需要如此显式地设定,除非阁下的发行版本比较老旧。

为指定用户(freeoa)创建控制组
cgcreate -t freeoa:freeoa -a freeoa:freeoa -g memory:/freeoa

设定物理内存限制
echo $(( 2048*1024*1024 )) | tee /sys/fs/cgroup/memory/freeoa/memory.limit_in_bytes #2 GB RAM

设定虚拟内存限制
echo $(( 2049*1024*1024 )) | tee /sys/fs/cgroup/memory/freeoa/memory.memsw.limit_in_bytes #2GB swap, only works if you have swap

立即适用
#在启动时自动适配
cgexec -g memory:freeoa belimitedapp
#对之前启动的相关进程号做适配
cgclassify -g memory:freeoa $(pidof belimitedapp)

使用'dmesg -T'可以看到cgroup的相关记录:
oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=/,mems_allowed=0,oom_memcg=/freeoa,task_memcg=/freeoa,task=belimitedapp,pid=2290,uid=1000
Memory cgroup out of memory: Killed process 2290 (belimitedapp) total-vm:2513452kB, anon-rss:322756kB, file-rss:57980kB, shmem-rss:0kB, UID:1000 pgtables:3444kB oom_score_adj:300
oom_reaper: reaped process 2290 (belimitedapp), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

在systemd中编写slice文件来开启资源控制

# /etc/systemd/system/freeoa-singleuser.slice
[Unit]
Description=Freeoa SingleUser Slice
Before=slices.target

[Slice]
MemoryAccounting=true
MemoryHigh=1G
MemorySwapMax=200M

Then, configure spawner to use it c.SystemdSpawner.slice=freeoa-singleuser.slice.

这里有一个疑问:MemorySwapMax的值小于MemoryHigh会不会失效,在命令行中的设定要求memory.limit_in_bytes的值显式大于memory.memsw.limit_in_bytes。

保存固化到/etc/cgconfig.conf中,以便重启后生效
group freeoa {
    perm {
        admin {
            uid = freeoa;
        }
        task {
            uid = freeoa;
        }
    }
    memory {
        memory.limit_in_bytes = 1585446912;
    }
}

#其它的控制组
group ...

语法检查
cgconfigparser -l /etc/cgconfig.conf

通过systemd联合实现一个配置解析服务
vim /lib/systemd/system/cgconfigparser.service

[Unit]
Description=cgroup config parser
After=network.target

[Service]
User=root
Group=root
ExecStart=/usr/sbin/cgconfigparser -l /etc/cgconfig.conf
Type=oneshot

[Install]
WantedBy=multi-user.target

做成自启动
systemctl enable cgconfigparser
systemctl start cgconfigparser


使用cgrulesengined
vim /etc/cgrules.conf
#username:appname        cgroup-type        cgrouprules/
freeoa:belimitedapp              memory          freeoa/
...

保存后调用/usr/sbin/cgrulesengd -vvv检查

cgroup也相有关的守护进程来简化此类的适配工作,默认的配置文件:/usr/share/doc/cgroup-tools/examples/cgred.conf

在/sys/fs/cgroup/memory/freeoa/tasks中记录了适配此控制规则的进程ID(一行一进程号)。


CentOS7中使用Cgroup搭配Systemd进行内存资源限制一例

(1).编写unit文件
命令为cgtdemo.service

文件内容如下:
[Unit]
Description=FreeOA-Cg-demo Service

[Service]
Type=simple
User=freeoa
ExecStart=/usr/bin/bash /home/freeoa/bin/test.sh

MemoryAccounting=true
MemoryLimit=200M

[Install]
WantedBy=multi-user.target

关于Type的定义:
simple 默认类型,这是最简单的服务类型。意思就是说启动的程序就是主体程序,这个程序要是退出那么一切都退出。
forking 标准Unix Daemon 使用的启动方式。启动程序后会调用 fork() 函数,父进程退出,留下变成Deamon的子进程。
oneshot 表示服务类型就是启动,执行完成后,没进程存在。

User字段表示指定用户启动程序。

MemoryLimit字段表示限制服务的物理内存占用,如果超过会被系统自动Kill。

另外test.sh的脚本定义如下,作用是耗内存:
#!/bin/bash
tmp="FreeOA"
while [ True ]
do
    tmp=$tmp$tmp
done

(2).将上述的文件拷贝到/usr/lib/systemd/system/目录下
# cp cgtdemo.service /usr/lib/systemd/system/

(3).配置开机启动
# systemctl enable cgtdemo.service
ln -s'/usr/lib/systemd/system/cgtdemo.service' '/etc/systemd/system/multi-user.target.wants/cgtdemo.service'

输出表明,注册的过程实际上就是将服务链接到/etc/systemd/system/目录下。

(4).测试
启动服务
# systemctl start cgtdemo.service

查看服务是否已经启动
# systemctl status cgtdemo.service
# systemctl status cgtdemo
● cgtdemo.service - FreeOA-Cg-demo Service
   Loaded: loaded (/usr/lib/systemd/system/cgtdemo.service; enabled; vendor preset: disabled)
   Active: active (running) since Mon 2022-06-27 15:45:47 CST; 4min 22s ago
 Main PID: 5637 (bash)
   Memory: 199.9M (limit: 200.0M)
   CGroup: /system.slice/cgtdemo.service
           └─5637 /usr/bin/bash /home/freeoa/bin/cgtest.sh

查看进程占用内存
# top

可以看到内存占用在上升,但始终维持在200M的物理内存左右,可以看见交换分区的使用在逐渐上升,同时还有较稳定的CPU使用率上升与硬盘IO。可见内存确实稳定下来了,但交换分区就不是这样了,操作系统将其交换到虚拟内存中。手动关闭操作系统上的虚拟分区看看。

# systemctl status cgtdemo
● cgtdemo.service - FreeOA-Cg-demo Service
   Loaded: loaded (/usr/lib/systemd/system/cgtdemo.service; enabled; vendor preset: disabled)
   Active: failed (Result: signal) since Mon 2022-06-27 15:58:28 CST; 555ms ago
  Process: 7038 ExecStart=/usr/bin/bash /home/freeoa/bin/cgtest.sh (code=killed, signal=KILL)
 Main PID: 7038 (code=killed, signal=KILL)

Jun 27 15:58:25 cos2 systemd[1]: Started FreeOA-Cg-demo Service.
Jun 27 15:58:28 cos2 systemd[1]: cgtdemo.service: main process exited, code=killed, status=9/KILL
Jun 27 15:58:28 cos2 systemd[1]: Unit cgtdemo.service entered failed state.
Jun 27 15:58:28 cos2 systemd[1]: cgtdemo.service failed.

只看见被系统中止了,但不知道是因什么原因导致的,在message中有相关的记录:
Jun 27 15:58:28 cos2 kernel: Task in /system.slice/cgtdemo.service killed as a result of limit of /system.slice/cgtdemo.service
Jun 27 15:58:28 cos2 kernel: memory: usage 204800kB, limit 204800kB, failcnt 16
Jun 27 15:58:28 cos2 kernel: memory+swap: usage 204800kB, limit 9007199254740988kB, failcnt 0
Jun 27 15:58:28 cos2 kernel: kmem: usage 0kB, limit 9007199254740988kB, failcnt 0
Jun 27 15:58:28 cos2 kernel: Memory cgroup stats for /system.slice/cgtdemo.service: cache:0KB rss:204800KB rss_huge:0KB mapped_file:0KB swap:0KB inactive_anon:0KB active_anon:204780KB inactive_file:0KB active_file:0KB unevictable:0KB
Jun 27 15:58:28 cos2 kernel: [ pid ]   uid  tgid total_vm      rss nr_ptes swapents oom_score_adj name
Jun 27 15:58:28 cos2 kernel: [ 7038]  1000  7038    89745    51496     114        0             0 bash
Jun 27 15:58:28 cos2 kernel: Memory cgroup out of memory: Kill process 7038 (bash) score 1008 or sacrifice child
Jun 27 15:58:28 cos2 kernel: Killed process 7038 (bash), UID 1000, total-vm:358980kB, anon-rss:204792kB, file-rss:1192kB, shmem-rss:0kB
Jun 27 15:58:28 cos2 systemd: cgtdemo.service: main process exited, code=killed, status=9/KILL
Jun 27 15:58:28 cos2 systemd: Unit cgtdemo.service entered failed state.
Jun 27 15:58:28 cos2 systemd: cgtdemo.service failed.

这个描绘比较详细具体了,但不能因此将操作系统的交换分区都关闭吧?

在boot loader的默认配置文件/etc/default/grub中设置更新后并重启主机后再测试:
systemd.unified_cgroup_hierarchy=1 swapaccount=1

# systemctl status cgtdemo
● cgtdemo.service - FreeOA-Cg-demo Service
   Loaded: loaded (/usr/lib/systemd/system/cgtdemo.service; enabled; vendor preset: disabled)
   Active: active (running) since Mon 2022-06-27 16:14:28 CST; 13s ago
 Main PID: 2693 (bash)
   Memory: 110.7M (limit: 200.0M)
   CGroup: /system.slice/cgtdemo.service
           └─2693 /usr/bin/bash /home/freeoa/bin/cgtest.sh

Jun 27 16:14:28 cos2 systemd[1]: Started FreeOA-Cg-demo Service.
# systemctl status cgtdemo
● cgtdemo.service - FreeOA-Cg-demo Service
   Loaded: loaded (/usr/lib/systemd/system/cgtdemo.service; enabled; vendor preset: disabled)
   Active: active (running) since Mon 2022-06-27 16:14:28 CST; 18s ago
 Main PID: 2693 (bash)
   Memory: 199.6M (limit: 200.0M)
   CGroup: /system.slice/cgtdemo.service
           └─2693 /usr/bin/bash /home/freeoa/bin/cgtest.sh

依然同前,修改为:cgroup_enable=memory swapaccount=1

仍无济于事,看来是在该服务的配置中没有指定交换分区用量限制导致对swap的使用没有限制?
在其配置文件(/usr/lib/systemd/system/cgtdemo.service)中加入此项:MemorySwapMax=100M

# systemctl status cgtdemo
● cgtdemo.service - FreeOA-Cg-demo Service
   Loaded: loaded (/usr/lib/systemd/system/cgtdemo.service; enabled; vendor preset: disabled)
   Active: active (running) since Mon 2022-06-27 16:30:57 CST; 1min 32s ago
 Main PID: 722 (bash)
   Memory: 199.8M (limit: 200.0M)
   CGroup: /system.slice/cgtdemo.service
           └─722 /usr/bin/bash /home/freeoa/bin/cgtest.sh

Jun 27 16:30:57 cos2 systemd[1]: Started FreeOA-Cg-demo Service.

依然会对交换分区不受限制的使用。。。

查证资料后:CentOS7自带的cgroup的版本是v1的,不支持对Swap用量进行限制?

参照上述的过程,可以知道是未设定交换内存的限制值,如果将MemorySwapMax写入systemd的配置文件中无法被识别,看来systemd对cgroupv1支持确认有限。
FreeOA.Net systemd: [/usr/lib/systemd/system/cgtdemo.service:12] Unknown lvalue 'MemorySwapMax' in section 'Service'

注意:原生命令行版本中无此功能设定限制。

----------------------------------------------------------------
使用cgroups来限制tomcat应用服务

对主机上的tomcat服务的cpu与内存进行限制使用,cpu.shares在10%左右默认在1024,内存限制在768M。
/etc/cgconfig.conf

mount {
    cpu = /sys/fs/cgroup/cpu;
    cpuacct = /sys/fs/cgroup/cpuacct;
    devices = /sys/fs/cgroup/devices;
    memory = /sys/fs/cgroup/memory;
    freezer = /sys/fs/cgroup/freezer;
}
group tomcat7 {
    perm {
        task {
            uid = tomcat7;
        }
        admin {
             uid = tomcat7;
        }
     }
     cpu {
         cpu.shares = 102;
     }
     memory {
         memory.limit_in_bytes = 805306368;
     }
}

接下来在规则服务cgred中按规则对上述的控制组进行适配:
/etc/cgrules.conf
tomcat7 cpu,memory tomcat7/

基本格式如下:
用户名        控制组        控制小组
user:command subsystems control_group

重启cgred与cgconfig这两个服务来使用设置生效:
systemctl stop cgred && systemctl stop cgconfig
systemctl start cgconfig && systemctl start cgred

将其设定为开机自启动
systemctl enable cgconfig && systemctl enable cgred

cgconfig.conf在工作站上更多示例

group limitcpu{
    cpu {
        cpu.shares = 400;
    }
}

group limitmem{
    memory {
        memory.limit_in_bytes = 512m;
        memory.memsw.limit_in_bytes = 512m;
    }
}

group limitio{
    blkio {
        blkio.throttle.read_bps_device = "252:0    2097152";
    }
}

group browsers{
    cpu {
        cpu.shares = 200;
    }
    memory {
        memory.limit_in_bytes = 128m;
        memory.memsw.limit_in_bytes = 512m;
    }
}

在limitcpu cgroup中将此cgroup中进程可用的cpu份额限制为400,cpu.shares指定cgroup中任务可用的CPU时间的相对份额。
在limitmem cgroup中将cgroup进程可用的内存限制为512MB。
在limitio cgroup中将磁盘读取吞吐量限制为2MiB/s。这里我们将读取I/O限制为主磁盘/dev/vda,其中major:次要编号252:0和2MiB/s被转换为每秒字节数2x1024x1024 = 2097152。
在browsers cgroup中将cpu份额限制为200,可用内存限制为128MB。

需要重新启动cgconfig服务才能使/etc/cgconfig.conf文件中的更改生效。同样通过lscgroup来验证上面配置的cgroup是否正确加载进来,使用cgget来查看具体的规则组。

Cgred控制组规则引擎守护程序是一种根据/etc/cgrules.conf文件中设置的参数将任务移动到cgroup中的服务,文件中的条目可以采用以下两种形式之一:
user subsystems control_group
user:command subsystems control_group

user是指以"@"字符为前缀的用户名或组名。subsystems请参阅以逗号分隔的子系统名称列表。control_group表示cgroup的路径,command表示进程的进程名称或完整命令路径。/etc/cgrules.conf文件中的条目可以包含以下额外符号:
@ – 表示组而不是单个用户。例如,@admin表示管理组中的所有用户。
* – 代表“所有”。例如,*在用户字段中表示所有用户。
% – 表示与上面一行中的项目相同的项目。

现在添加希望限制的程序/流程,编辑/etc/cgrules.conf并在底部添加以下内容:
*:firefox       cpu,memory      browsers/
*:hdparm        blkio   limitio/
freeoa   blkio   limitio/
@admin:memhog  memory  limitmem/
*:cpuhog        cpu     limitcpu/

在上面的行中设置了以下规则:
由任何用户运行的firefox进程将自动添加到browsers cgroup并限制在cpu和内存子系统中。
由任何用户运行的hdparm进程将被添加到limitio cgroup,并将根据该cgroup中指定的参数值限制在blkio子系统中。
用户freeoa运行的所有进程都将添加到limitio cgroup并限制在blkio子系统中。
由admin组中的任何人运行的memhog进程将被添加到limitmem cgroup并限制在内存子系统中。
由任何用户运行的cpuhog进程将被添加到limitcpu cgroup并限制在cpu子系统中。

需要启动cgred服务以使cgrules配置更改生效,还需要确保cgred服务是自启动,以便相关规则在重新启动后依然存在。

配置文件检测
cgconfigparser -l /etc/cgconfig.conf -s 1664
cgrulesengd -v -s -g cgred

----------------------------------------------------------------
Devuan4.chimaera的测试过程

如何在Debian下使用Cgroups限制应用对CPU与内存的使用

Cgroups是一种灵活的Linux内核功能,用于限制、管理帐户资源的使用策略。cgroup是子系统的一组任务,通常是资源控制器,可作为文件系统类型挂载使用,并可在其上运行娄似于对普通文件的所有操作。可安装 libcgroup,cgmanager 或 systemd来实现,有如下几种的使用方式:
(1).直接以文件系统的方式访问By accessing the cgroup filesystem directly.
(2).使用cgmanager相关工具Using the cgm client (part of the cgmanager).
(3).cgroup-tools工具集中的指令Via cgcreate, cgexec and cgclassify (part of cgroup-tools).
(4).cgroup-tools服务及其配置文件Via cgconfig.conf and cgrules.conf (also part of cgroup-tools).

Debian下可以显式开启内存控制组的使用统计,在引导时开启此功能:
在BootLoader的配置文件(/etc/default/grub)中加入:
GRUB_CMDLINE_LINUX_DEFAULT="cgroup_enable=memory swapaccount=1"

保存后执行update-grub使之保存到引导配置文件中,以便下次引导生效。

cgcreate -t hto:hto -a hto:hto -g memory:/cgdex

cgcreate: libcgroup initialization failed: Cgroup is not mounted

/etc/default/grub
add the next line on GRUB_CMDLINE_LINUX_DEFAULT: systemd.unified_cgroup_hierarchy=1
并重启,却依然报同样的错误。。想起来了,devuan压根就没有用到systemd,修改为unified_cgroup_hierarchy=1再来,重启后还是如此。

难道是要安装cgroupfs-mount这个软件包才行?

果然,在安装cgroupfs-mount后重启主机生效!内核参数中只保留了一个:cgroup_no_v1=all参数,也不用在/etc/fstab中添加与cgroup虚拟文件系统的配置。

# dmesg |grep group
[    0.000000] Command line: BOOT_IMAGE=/boot/vmlinuz-5.10.0-9-amd64 root=UUID=efca50ff-b1a0-4570-bdfd-28126d36923f ro cgroup_no_v1=all
[    0.081801] Built 1 zonelists, mobility grouping on.  Total pages: 2080377
[    0.081804] Kernel command line: BOOT_IMAGE=/boot/vmlinuz-5.10.0-9-amd64 root=UUID=efca50ff-b1a0-4570-bdfd-28126d36923f ro cgroup_no_v1=all
[    0.112513] ftrace: allocated 143 pages with 5 groups
[    0.157527] Disabling cpuset control group subsystem in v1 mounts
[    0.157769] Disabling cpu control group subsystem in v1 mounts
[    0.157999] Disabling cpuacct control group subsystem in v1 mounts
[    0.158257] Disabling io control group subsystem in v1 mounts
[    0.158509] Disabling memory control group subsystem in v1 mounts
[    0.158749] Disabling devices control group subsystem in v1 mounts
[    0.158988] Disabling freezer control group subsystem in v1 mounts
[    0.159230] Disabling net_cls control group subsystem in v1 mounts
[    0.159339] Disabling perf_event control group subsystem in v1 mounts
[    0.159339] Disabling net_prio control group subsystem in v1 mounts
[    0.159339] Disabling hugetlb control group subsystem in v1 mounts
[    0.159339] Disabling pids control group subsystem in v1 mounts
[    0.159339] Disabling rdma control group subsystem in v1 mounts
[   32.909246] cgroup: Disabled controller 'cpuset'
[   32.927940] cgroup: Disabled controller 'cpu'
[   32.937820] cgroup: Disabled controller 'cpuacct'
[   32.949008] cgroup: Disabled controller 'blkio'
[   32.958905] cgroup: Disabled controller 'memory'
[   32.969646] cgroup: Disabled controller 'devices'
[   32.979633] cgroup: Disabled controller 'freezer'
[   32.990484] cgroup: Disabled controller 'net_cls'
[   33.000095] cgroup: Disabled controller 'perf_event'
[   33.010543] cgroup: Disabled controller 'net_prio'
[   33.020630] cgroup: Disabled controller 'hugetlb'
[   33.029277] cgroup: Disabled controller 'pids'
[   33.037647] cgroup: Disabled controller 'rdma'

cgcreate: can't create cgroup /cgdex: Cgroup one of the needed subsystems is not mounted

看来还不能这样禁用cgroup v1?只能将/etc/default/grub中的与cgorup相关的设置全部删除后生效。

cgset -r memory.limit_in_bytes=268435456 cgdex
cgset -r memory.memsw.limit_in_bytes=268435456 cgdex

hto@devfs:~$ cgexec -g memory:cgdex /tmp/memalct
Trun:1,malloc memory 100 MB
Trun:2,malloc memory 200 MB
已杀死

dmesg的输出中看到的记录:
[  223.550924] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=/,mems_allowed=0,oom_memcg=/cgdex,task_memcg=/cgdex,task=memalct,pid=1641,uid=1000
[  223.551486] Memory cgroup out of memory: Killed process 1641 (memalct) total-vm:308180kB, anon-rss:261324kB, file-rss:588kB, shmem-rss:0kB, UID:1000 pgtables:552kB oom_score_adj:0
[  223.553510] oom_reaper: reaped process 1641 (memalct), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

完全达到预期!

重启后不对memory.memsw.limit_in_bytes进行设置看否能达到相关预期,这次设置内存阀值为512M。
cgcreate -t hto:hto -a hto:hto -g memory:/cgdex
cgset -r memory.limit_in_bytes=512m cgdex

实测不行,交换分区用量还超过了物理内存(超过设定的512后就开始使用交换分区),内存分配程序能正常结束。再来设定memsw后重跑。
cgset -r memory.memsw.limit_in_bytes=512m cgdex

Trun:1,malloc memory 100 MB
...
Trun:5,malloc memory 500 MB
已杀死

使用top甚至观察不到对swap的使用就结束了,将主机的物理内存调整至1G,而cgroup中的内存双限制调整为2G测试。

cgcreate -t hto:hto -a hto:hto -g memory:/cgdex
cgset -r memory.limit_in_bytes=1g cgdex
cgset -r memory.memsw.limit_in_bytes=1g cgdex

本次观察到了对swap的使用情况,同时也验证了vm.swappiness参数生效的情况,另外有个地方要注意:
上述的cgroup中设定的内存(物理+交换)阀值就是1g,并不是物理内存+交换内存的2g!这里一定要注意。英文的原文如下上文有述:
memory.limit_in_bytes must be set before memory.memsw.limit_in_bytes, and memory.memsw.limit_in_bytes must be bigger than or equal to memory.limit_in_bytes.

某些情形下要显式的开启swap用量限制,需要在启动时开启交换分区用时审计:将swapaccount=1加入到/etc/default/grub中:
GRUB_CMDLINE_LINUX="swapaccount=1"

保存后执行update-grub2并重启系统生效。

如果swap用量对相关的计算造成了干扰,但双不想禁用交换分区的功能,可以在控制组中关闭对它的使用,做到更为精细控制cgroupv1:
cgcreate -g "memory:$cgname"
cgset -r memory.limit_in_bytes="$limit" "$cgname"
cgset -r memory.swappiness=0 "$cgname"
bytes_limit=`cgget -g "memory:$cgname" | grep memory.limit_in_bytes | cut -d\  -f2`

----------------
cgcreate -a $USER:$USER -t $USER:$USER -g memory:/cmemt
cgset -r memory.limit_in_bytes=500M cmemt
cgset -r memory.swappiness=0 cmemt
#查看
cgget -g memory /cmemt

cgexec -g memory:cmemt mycmd args
Trun:1,malloc memory 100 MB
Trun:1,sleep:1,time:22:39:21
Trun:2,malloc memory 200 MB
Trun:2,sleep:2,time:22:39:22
Trun:3,malloc memory 300 MB
Trun:3,sleep:3,time:22:39:25
Trun:4,malloc memory 400 MB
Trun:4,sleep:4,time:22:39:28
Trun:5,malloc memory 500 MB
Trun:5,sleep:5,time:22:39:32
Killed

将memory.swappiness设置为0相当于对该应用关闭了对交换分区的使用,一旦对物理内存使用超过500M,就会被系统Kill;在CentOS7与Devuan4下均测试通过。

Devuan4下同样有cgconfig与cgred的配置文件,但没有在服务中找到其服务(不过手动做成服务也不是什么难事)。其测试过程反而比较简单:
cgconfig似乎是无服务的,cgred是后台服务,cgrulesengd这个守护进程默认使用上述两个配置文件。

首先使用cgconfigparser检查配置文件(包括每次修改过两大配置文件的内容后)
cgconfigparser -l /etc/cgconfig.conf

不执行这步会报进程ID(PID)绑定失败的错误
Executing rule hto for PID 1649... Will move pid 1649 to cgroup 'limitdiskio/'
Adding controller blkio
Warning: cgroup_attach_task_pid failed: 50002
Warning: failed to apply the rule. Error was: 50002
Cgroup change for PID: 1649, UID: 1000, GID: 1000, PROCNAME: /usr/bin/rsync FAILED! (Error Code: 50002)

完成测试后可将其加入/etc/rc.local中做成简单的开机自启服务:
/usr/sbin/cgconfigparser -l /etc/cgconfig.conf && /usr/sbin/cgrulesengd -d -s 'cgruled'

使用'-d'参数以调试模式在前台拉起进程以便观察细节。
root@devfs:~# cgrulesengd -d
CGroup Rules Engine Daemon log started
Current time: Thu Jul 21 22:18:07 2022
Opened log file: -, log facility: 0,log level: 7
Proceeding with PID 2236
Rule: *:firefox
  UID: any
  GID: any
  DEST: browsers/
  CONTROLLERS:
    cpu
    memory

Rule: *:hdparm
  UID: any
  GID: any
  DEST: limitdiskio/
  CONTROLLERS:
    blkio

Rule: *:curl
  UID: any
  GID: any
  DEST: limitnetio/
  CONTROLLERS:
    blkio

Rule: hto:rsync
  UID: 1000
  GID: N/A
  DEST: limitdiskio/
  CONTROLLERS:
    blkio

Rule: hto:memalct
  UID: 1000
  GID: N/A
  DEST: limitmem/
  CONTROLLERS:
    memory

...
Found matching rule hto for PID: 1656, UID: 1000, GID: 1000
Executing rule hto for PID 1656... Will move pid 1656 to cgroup 'limitdiskio/'
Adding controller blkio
OK!
Cgroup change for PID: 1656, UID: 1000, GID: 1000, PROCNAME: /usr/bin/rsync OK

...
Found matching rule hto for PID: 2243, UID: 1000, GID: 1000
Executing rule hto for PID 2243... Will move pid 2243 to cgroup 'limitmem/'
Adding controller memory
OK!
Cgroup change for PID: 2243, UID: 1000, GID: 1000, PROCNAME: /tmp/memalct OK
EXEC Event: PID = 2244, tGID = 2244

hto@devfs:/tmp$ /tmp/memalct
Trun:1,malloc memory 100 MB
Trun:1,sleep:1,time:22:18:23
...
Trun:10,malloc memory 1000 MB
Trun:10,sleep:5,time:22:19:00
已杀死

内存限制测试完成。

----------------------------------------------------------------

C语言编写的内存分配程序(memalct2.c)

#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define CHUNK_SIZE 1024*1024*100
/*auth by freeoa.net,malloc memory example 100MB per time*/

unsigned short int nochage(unsigned short int no);
char * iso8601(char local_time[32]);

void main(){
    char *p;
    int i;
    char time[32] = {0};
    for(i=1;i<=20;i++){
        p = malloc(sizeof(char) * CHUNK_SIZE);
        if(p == NULL){
            printf("Fail to malloc!");
            return ;
        }
        memset(p, 0, CHUNK_SIZE);
        printf("Trun:%d,malloc memory %d MB\n",i,i*100);
        unsigned short int sno=nochage(i);
        printf("Trun:%d,sleep:%d,time:%s\n",i,sno,iso8601(time));
        sleep(sno);
    }
}

unsigned short int nochage(unsigned short int no){
    return (no<=5)?(no):(5);
}

char * iso8601(char local_time[32]){
    time_t t_time;
    struct tm *t_tm;
    t_time = time(NULL);
    t_tm = localtime(&t_time);
    if(t_tm == NULL)
            return NULL;
    if(strftime(local_time, 32, "%H:%M:%S", t_tm) == 0)
        return NULL;
    local_time[31] = '\0';
    return local_time;
}

编译为可执行文件:
gcc -static memalct2.c -o memalct

----------------------------------------------------------------
参考链接

CGROUPS(7)
manpage4cgroups.7
manpage4cpuset.7
manage4taskset.1

debian-cgroups.7
kernel-parameters-v5.10

kernel-cgroup-v1
kernel-cgroup-v2

RHEL7 Resource_Management_Guide