
本章主要演示以下 cgroups 下各个 subsystem 的作用。
根据难易程度,依次演示了 pids 、cpu 和 memory 3 个 subsystem 的使用。
注:本文所有操作在 Ubuntu20.04 下进行。
1. pids
pids subsystem 功能是限制 cgroup 及其所有子孙 cgroup 里面能创建的总的 task 数量。
注意:这里的 task 指通过 fork 和 clone 函数创建的进程,由于 clone 函数也能创建线程(在 Linux 里面,线程是一种特殊的进程),所以这里的 task 也包含线程。
本文统一以进程来代表 task,即本文中的进程代表了进程和线程>
创建子 cgroup
创建子 cgroup,取名为 test
1
2
3
| #进入目录/sys/fs/cgroup/pids/并新建一个目录,即创建了一个子cgroup
lixd /home/lixd $ cd /sys/fs/cgroup/pids
lixd /sys/fs/cgroup/pids $ sudo mkdir test
|
再来看看 test 目录下的文件
1
2
3
4
| lixd /sys/fs/cgroup/pids $ cd test
#除了上一篇中介绍的那些文件外,多了两个文件
lixd /sys/fs/cgroup/pids/test $ ls
cgroup.clone_children cgroup.procs notify_on_release pids.current pids.events pids.max tasks
|
下面是这两个文件的含义:
- pids.current: 表示当前 cgroup 及其所有子孙 cgroup 中现有的总的进程数量
- pids.max: 当前 cgroup 及其所有子孙 cgroup 中所允许创建的总的最大进程数量
限制进程数
首先是将当前 bash 加入到 cgroup 中,并修改pids.max
的值,为了便于测试,这里就限制为1:
1
2
3
4
5
| #--------------------------第一个shell窗口----------------------
# 将当前bash进程加入到该cgroup
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/pids/test# echo $$ > cgroup.procs
#将pids.max设置为1,即当前cgroup只允许有一个进程
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/pids/test# echo 1 > pids.max
|
由于 bash 已经占用了一个进程,所以此时 bash 中已经无法创建新的进程了:
1
2
3
4
| root@DESKTOP-9K4GB6E:/sys/fs/cgroup/pids/test# ls
bash: fork: retry: Resource temporarily unavailable
bash: fork: retry: Resource temporarily unavailable
bash: fork: retry: Resource temporarily unavailable
|
创建新进程失败,于是命令运行失败,说明限制生效。
打开另一个 shell 查看
1
2
3
4
5
| lixd /mnt/c/Users/意琦行 $ cd /sys/fs/cgroup/pids/test
lixd /sys/fs/cgroup/pids/test $ ls
cgroup.clone_children cgroup.procs notify_on_release pids.current pids.events pids.max tasks
lixd /sys/fs/cgroup/pids/test $ cat pids.current
1
|
果然,pids.current 为 1,已经到 pids.max 的限制了。
当前 cgroup 和子 cgroup 之间的关系
当前 cgroup 中的 pids.current
和 pids.max
代表了当前 cgroup 及所有子孙 cgroup 的所有进程,所以子孙 cgroup 中的 pids.max 大小不能超过父 cgroup。
如果子 cgroup 中的 pids.max 设置的大于父 cgroup 里的值,会怎么样?
答案是子 cgroup 中的进程不光受子 cgroup 限制,还要受其父 cgroup 的限制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| #继续使用上面的两个窗口
#--------------------------第二个shell窗口----------------------
#将pids.max设置成2
dev@dev:/sys/fs/cgroup/pids/test$ echo 2 > pids.max
#在test下面创建一个子cgroup
dev@dev:/sys/fs/cgroup/pids/test$ mkdir subtest
dev@dev:/sys/fs/cgroup/pids/test$ cd subtest/
#将subtest的pids.max设置为5
dev@dev:/sys/fs/cgroup/pids/test/subtest$ echo 5 > pids.max
#将当前bash进程加入到subtest中
dev@dev:/sys/fs/cgroup/pids/test/subtest$ echo $$ > cgroup.procs
#--------------------------第三个shell窗口----------------------
#重新打开一个bash窗口,看一下test和subtest里面的数据
#test里面的数据如下:
dev@dev:~$ cd /sys/fs/cgroup/pids/test
dev@dev:/sys/fs/cgroup/pids/test$ cat pids.max
2
#这里为2表示目前test和subtest里面总的进程数为2
dev@dev:/sys/fs/cgroup/pids/test$ cat pids.current
2
dev@dev:/sys/fs/cgroup/pids/test$ cat cgroup.procs
3083
#subtest里面的数据如下:
dev@dev:/sys/fs/cgroup/pids/test$ cat subtest/pids.max
5
dev@dev:/sys/fs/cgroup/pids/test$ cat subtest/pids.current
1
dev@dev:/sys/fs/cgroup/pids/test$ cat subtest/cgroup.procs
3185
#--------------------------第一个shell窗口----------------------
#回到第一个窗口,随便运行一个命令,由于test里面的pids.current已经等于pids.max了,
#所以创建新进程失败,于是命令运行失败,说明限制生效
dev@dev:/sys/fs/cgroup/pids/test$ ls
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: Resource temporarily unavailable
#--------------------------第二个shell窗口----------------------
#回到第二个窗口,随便运行一个命令,虽然subtest里面的pids.max还大于pids.current,
#但由于其父cgroup “test”里面的pids.current已经等于pids.max了,
#所以创建新进程失败,于是命令运行失败,说明子cgroup中的进程数不仅受自己的pids.max的限制,还受祖先cgroup的限制
dev@dev:/sys/fs/cgroup/pids/test/subtest$ ls
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: Resource temporarily unavailable
|
pids.current > pids.max 的情况
并不是所有情况下都是 pids.max >= pids.current,在下面两种情况下,会出现 pids.max < pids.current 的情况:
- 设置 pids.max 时,将其值设置的比 pids.current 小
- 将其他进程加入到当前 cgroup 有可能会导致 pids.current > pids.max
- 因为 pids.max 只会在当前 cgroup 中的进程 fork、clone 的时候生效,将其他进程加入到当前 cgroup 时,不会检测 pids.max,所以可能触发这种情况
小结
作用:pids subsystem 用于限制 cgroups 下能够创建的 task(进程和线程)数。
原理:在调用 fork 和 clone 时对比 subsystem 中配置的 pids.max 和 pids.current 值来判断当前是否能够继续创建 task。
用法:配置 pids.max 防止容器消耗完 pid。
2. cpu
在 cgroup 里面,跟 CPU 相关的子系统有 cpusets、cpuacct 和 cpu。
本节只介绍 cpu 子系统,包括怎么限制 cgroup 的 CPU 使用上限及相对于其它 cgroup 的相对值。
创建子 cgroup
通用是创建子目录即可。
1
2
3
4
5
6
7
| #进入/sys/fs/cgroup/cpu并创建子cgroup
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu# cd /sys/fs/cgroup/cpu
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu# mkdir test
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu# cd test/
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu/test# ls
cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release
cgroup.procs cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks
|
看起来文件比 memory subsystem 还是少一些。
cpu.cfs_period_us & cpu.cfs_quota_us:两个文件配合起来设置CPU的使用上限,两个文件的单位都是微秒(us)。
- cfs_period_us:用来配置时间周期长度
- cfs_quota_us:用来配置当前cgroup在设置的周期长度内所能使用的CPU时间数
- 取值大于1ms即可
- 默认值为 -1,表示不受cpu时间的限制。
cpu.shares 用来设置 CPU 的相对值(比例),并且是针对所有的 CPU(内核),默认值是1024。
假如系统中有两个 cgroup,分别是 A 和 B,A 的 shares 值是 1024,B 的 shares 值是512,那么 A 将获得 1024/(1204+512)=66% 的 CPU 资源,而 B 将获得 33% 的 CPU 资源。
shares 有两个特点:
- 如果 A 不忙,没有使用到 66% 的 CPU 时间,那么剩余的 CPU 时间将会被系统分配给 B,即 B 的 CPU 使用率可以超过 33%
- 如果添加了一个新的 cgroup C,且它的 shares 值是 1024,那么 A 的限额变成了1024/(1204+512+1024)=40%,B 的变成了 20%
从上面两个特点可以看出:
- 在闲的时候,shares 基本上不起作用,只有在 CPU 忙的时候起作用,这是一个优点。
- 由于 shares 是一个绝对值,需要和其它 cgroup 的值进行比较才能得到自己的相对限额,而在一个部署很多容器的机器上,cgroup 的数量是变化的,所以这个限额也是变化的,自己设置了一个高的值,但别人可能设置了一个更高的值,所以这个功能没法精确的控制CPU 使用率。
cpu.stat 包含了下面三项统计结果:
- nr_periods: 表示过去了多少个 cpu.cfs_period_us 里面配置的时间周期
- nr_throttled: 在上面的这些周期中,有多少次是受到了限制(即 cgroup 中的进程在指定的时间周期中用光了它的配额)
- throttled_time: cgroup 中的进程被限制使用 CPU 持续了多长时间(纳秒)
原理
前面配置的参数都是 cfs_xxx
,这里的 cfs 是 Completely Fair Scheduler 的缩写。
CFS 是 Linux 内核中的调度器,它负责决定哪个进程在给定时间片内运行。CFS 使用 CFS 配额(cpu.cfs_quota_us
)和 CFS 周期(cpu.cfs_period_us
)来限制每个 cgroup 中的 CPU 使用。
CFS 的实现与 cgroups 协同工作,它负责追踪每个 cgroup 中的进程消耗的 CPU 时间,并在每个调度周期结束时根据 cgroup 的 CPU 配额调整进程的运行时间。
如果一个 cgroup 中的进程在调度周期内超过了它的 CPU 配额,它将被调度器限制,从而实现了 CPU 的使用限制。
即:cgroups 中的 subsystem 负责提供配置,cfs 负责记录进程使用的 cpu 时间,达到阈值后就从调度层面进行限制,避免该进程继续使用 cpu。
演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| #继续使用上面创建的子cgroup: test
#设置只能使用1个cpu的20%的时间
dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ sudo sh -c "echo 50000 > cpu.cfs_period_us"
dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ sudo sh -c "echo 10000 > cpu.cfs_quota_us"
#将当前bash加入到该cgroup
dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ echo $$
5456
dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ sudo sh -c "echo 5456 > cgroup.procs"
#在bash中启动一个死循环来消耗cpu,正常情况下应该使用100%的cpu(即消耗一个内核)
dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ while :; do echo test > /dev/null; done
#--------------------------重新打开一个shell窗口----------------------
#通过top命令可以看到5456的CPU使用率为20%左右,说明被限制住了
#不过这时系统的%us+%sy在10%左右,那是因为我测试的机器上cpu是双核的,
#所以系统整体的cpu使用率为10%左右
dev@ubuntu:~$ top
Tasks: 139 total, 2 running, 137 sleeping, 0 stopped, 0 zombie
%Cpu(s): 5.6 us, 6.2 sy, 0.0 ni, 88.2 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 499984 total, 15472 free, 81488 used, 403024 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 383332 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
5456 dev 20 0 22640 5472 3524 R 20.3 1.1 0:04.62 bash
#这时可以看到被限制的统计结果
dev@ubuntu:~$ cat /sys/fs/cgroup/cpu,cpuacct/test/cpu.stat
nr_periods 1436
nr_throttled 1304
throttled_time 51542291833
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| # cfs_period_us 值为 10W
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu/test# cat cpu.cfs_period_us
100000
# 往 cfs_quota_us 写入 20000,即限制只能使用20%cpu
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu/test# echo 20000 > cpu.cfs_quota_us
# 新开一个窗口,运行一个死循环
$ while : ; do : ; done &
[1] 519
# top 看一下 cpu 占用率,果然是100%了
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
519 lixd 25 5 13444 2912 0 R 100.0 0.0 0:05.66 zsh
# 回到第一个shell窗口,限制当前进程的cpu使用率
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu/test# echo 519 >> cgroup.procs
# 再切回第二个窗口,发现519进程的cpu已经降到20%了,说明限制生效了
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
519 lixd 25 5 13444 2912 0 R 20.0 0.0 0:31.86 zsh
# 查看被限制的统计结果
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu/test# cat cpu.stat
nr_periods 2090
nr_throttled 2088
throttled_time 166752684900
|
小结
作用:cpu subsystem 用于限制 cgroups 下进程可以使用的 cpu 上限。
原理:cgroups 中的 subsystem 负责提供配置,cfs 负责记录进程使用的 cpu 时间,达到阈值后就从调度层面进行限制,避免该进程继续使用 cpu。
用法:
- 1)限制为具体值:用 cfs_period_us & cfs_quota_us 两个配置可以严格限制进程 cpu 使用量。
- 2)按比例分配:用 shares 配置,可以使得多个 cgroups 之间按比例分配所有 cpu。
3. memory
memory subsystem 顾名思义,限制 cgroups 中进程的内存使用。
为什么需要内存控制
- 站在一个普通开发者的角度,如果能控制一个或者一组进程所能使用的内存数,那么就算代码有 bug,内存泄漏也不会对系统造成影响,因为可以设置内存使用量的上限,当到达这个值之后可以将进程重启。
- 站在一个系统管理者的角度,如果能限制每组进程所能使用的内存量,那么不管程序的质量如何,都能将它们对系统的影响降到最低,从而保证整个系统的稳定性。
内存控制能控制些什么?
- 限 制cgroup 中所有进程所能使用的物理内存总量
- 限制 cgroup 中所有进程所能使用的物理内存+交换空间总量(CONFIG_MEMCG_SWAP): 一般在 server 上,不太会用到 swap 空间,所以不在这里介绍这部分内容。
- 限制 cgroup 中所有进程所能使用的内核内存总量及其它一些内核资源(CONFIG_MEMCG_KMEM): 限制内核内存有什么用呢?其实限制内核内存就是限制当前cgroup 所能使用的内核资源,比如进程的内核栈空间,socket 所占用的内存空间等,通过限制内核内存,当内存吃紧时,可以阻止当前 cgroup 继续创建进程以及向内核申请分配更多的内核资源。由于这块功能被使用的较少,本篇中也不对它做介绍。
创建子cgroup
在 /sys/fs/cgroup/memory 下创建一个子目录就算是创建了一个子 cgroup
1
2
3
4
5
6
7
8
9
10
11
12
13
| root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory# cd /sys/fs/cgroup/memory
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory# mkdir test
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory# ls test/
cgroup.clone_children memory.kmem.tcp.max_usage_in_bytes memory.oom_control
cgroup.event_control memory.kmem.tcp.usage_in_bytes memory.pressure_level
cgroup.procs memory.kmem.usage_in_bytes memory.soft_limit_in_bytes
memory.failcnt memory.limit_in_bytes memory.stat
memory.force_empty memory.max_usage_in_bytes memory.swappiness
memory.kmem.failcnt memory.memsw.failcnt memory.usage_in_bytes
memory.kmem.limit_in_bytes memory.memsw.limit_in_bytes memory.use_hierarchy
memory.kmem.max_usage_in_bytes memory.memsw.max_usage_in_bytes notify_on_release
memory.kmem.tcp.failcnt memory.memsw.usage_in_bytes tasks
memory.kmem.tcp.limit_in_bytes memory.move_charge_at_immigrate
|
从上面 ls 的输出可以看出,除了每个 cgroup 都有的那几个文件外,和 memory 相关的文件还不少,这里先做个大概介绍(kernel 相关的文件除外),后面会详细介绍每个文件的作用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| cgroup.event_control #用于eventfd的接口
memory.usage_in_bytes #显示当前已用的内存
memory.limit_in_bytes #设置/显示当前限制的内存额度
memory.failcnt #显示内存使用量达到限制值的次数
memory.max_usage_in_bytes #历史内存最大使用量
memory.soft_limit_in_bytes #设置/显示当前限制的内存软额度
memory.stat #显示当前cgroup的内存使用情况
memory.use_hierarchy #设置/显示是否将子cgroup的内存使用情况统计到当前cgroup里面
memory.force_empty #触发系统立即尽可能的回收当前cgroup中可以回收的内存
memory.pressure_level #设置内存压力的通知事件,配合cgroup.event_control一起使用
memory.swappiness #设置和显示当前的swappiness
memory.move_charge_at_immigrate #设置当进程移动到其他cgroup中时,它所占用的内存是否也随着移动过去
memory.oom_control #设置/显示oom controls相关的配置
memory.numa_stat #显示numa相关的内存
|
添加进程
也是往 cgroup 中添加进程只要将进程号写入 cgroup.procs 就可以了。
1
2
3
4
5
6
| #重新打开一个shell窗口,避免相互影响
root@DESKTOP-9K4GB6E:~# cd /sys/fs/cgroup/memory/test/
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo $$ >> cgroup.procs
#运行top命令,这样这个cgroup消耗的内存会多点,便于观察
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# top
# 后续操作不再在这个窗口进行,避免在这个bash中运行进程影响cgropu里面的进程数及相关统计
|
设置限额
设置限额很简单,将阈值写入 memory.limit_in_bytes 文件就可以了,例如:
- echo 1M > memory.limit_in_bytes:限制只能用 1M 内存
- echo -1 > memory.limit_in_bytes:-1则是不限制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #回到第一个shell窗口
#开始设置之前,看看当前使用的内存数量,这里的单位是字节
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.usage_in_bytes
2379776
#设置1M的限额
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 1M > memory.limit_in_bytes
#设置完之后记得要查看一下这个文件,因为内核要考虑页对齐, 所以生效的数量不一定完全等于设置的数量
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.usage_in_bytes
950272
#如果不再需要限制这个cgroup,写-1到文件memory.limit_in_bytes即可
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo -1 > memory.limit_in_bytes
#这时可以看到limit被设置成了一个很大的数字
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.limit_in_bytes
9223372036854771712
|
如果设置的限额比当前已经使用的内存少呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# free -h
total used free shared buff/cache available
Mem: 7.7Gi 253Mi 7.4Gi 0.0Ki 95Mi 7.3Gi
Swap: 2.0Gi 0.0Ki 2.0Gi
# 此时用了 1232K
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.usage_in_bytes
1232896
# 限制成500K
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 500k > memory.limit_in_bytes
# 再次查看发现现在只用了401K
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.usage_in_bytes
401408
# 发现swap多了1M,说明另外的数据被转移到swap上了
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# free -h
total used free shared buff/cache available
Mem: 7.7Gi 254Mi 7.4Gi 0.0Ki 94Mi 7.3Gi
Swap: 2.0Gi 1.0Mi 2.0Gi
#这个时候再来看failcnt,发现有381次之多(隔几秒再看这个文件,发现次数在增长)
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.failcnt
381
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.failcnt
385
#再看看memory.stat(这里只显示部分内容),发现物理内存用了400K,
#但有很多pgmajfault以及pgpgin和pgpgout,说明发生了很多的swap in和swap out
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.stat
swap 946176 # 946K 差不多刚好是内存中少的量
pgpgin 30492
pgpgout 30443
pgfault 23859
pgmajfault 12507
|
从上面的结果可以看出,当物理内存不够时,就会触发 memory.failcnt 里面的数量加 1,但进程不会被 kill 掉,那是因为内核会尝试将物理内存中的数据移动到 swap 空间中,从而让内存分配成功。
如果设置的限额过小,就算 swap out 部分内存后还是不够会怎么样?
1
2
3
4
5
6
7
8
| #--------------------------第一个shell窗口----------------------
# 限制到100k
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 100K > memory.limit_in_bytes
#--------------------------第二个shell窗口----------------------
# 尝试执行 top 发现刚运行就被Kill了
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# top
Killed
|
从上面的这些测试可以看出,一旦设置了内存限制,将立即生效,并且当物理内存使用量达到limit 的时候,memory.failcnt 的内容会加 1,但这时进程不一定就会
被 kill 掉,内核会尽量将物理内存中的数据移到 swap 空间上去,如果实在是没办法移动了(设置的 limit 过小,或者 swap 空间不足),默认情况下,就会 kill 掉 cgroup里面继续申请内存的进程。
行为控制
通过修改memory.oom_control
文件,可以控制 subsystem 在物理内存达到上限时的行为。文件中包含以下3个参数:
oom_kill_disable
:是否启用 oom killunder_oom
:表示当前是否已经进入oom状态,也即是否有进程被暂停了。oom_kill
:oom 后是否执行 kill- 1:启动,oom 后直接 kill 掉对应进程
- 2:关闭:当内核无法给进程分配足够的内存时,将会暂停该进程直到有空余的内存之后再继续运行。同时会更新 under_oom 状态
- 注意:root cgroup 的 oom killer 是不能被禁用的
为了演示 OOM-killer 的功能,创建了下面这样一个程序,用来向系统申请内存,它会每秒消耗1M 的内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MB (1024 * 1024)
int main(int argc, char *argv[])
{
char *p;
int i = 0;
while(1) {
p = (char *)malloc(MB);
memset(p, 0, MB);
printf("%dM memory allocated\n", ++i);
sleep(1);
}
return 0;
}
|
保存上面的程序到文件~/mem-allocate.c
,然后编译并测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| #--------------------------第一个shell窗口----------------------
#编译上面的文件
dev@dev:/sys/fs/cgroup/memory/test$ gcc ~/mem-allocate.c -o ~/mem-allocate
#设置内存限额为5M
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 5M > memory.limit_in_bytes"
#将当前bash加入到test中,这样这个bash创建的所有进程都会自动加入到test中
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo $$ >> cgroup.procs"
#默认情况下,memory.oom_control的值为0,即默认启用oom killer
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.oom_control
oom_kill_disable 0
under_oom 0
#为了避免受swap空间的影响,设置swappiness为0来禁止当前cgroup使用swap
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 > memory.swappiness"
#当分配第5M内存时,由于总内存量超过了5M,所以进程被kill了
dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
Killed
#设置oom_control为1,这样内存达到限额的时候会暂停
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 1 >> memory.oom_control"
#跟预期的一样,程序被暂停了
dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
#--------------------------第二个shell窗口----------------------
#再打开一个窗口
dev@dev:~$ cd /sys/fs/cgroup/memory/test/
#这时候可以看到memory.oom_control里面under_oom的值为1,表示当前已经oom了
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.oom_control
oom_kill_disable 1
under_oom 1
#修改test的额度为7M
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 7M > memory.limit_in_bytes"
#--------------------------第一个shell窗口----------------------
#再回到第一个窗口,会发现进程mem-allocate继续执行了两步,然后暂停在6M那里了
dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
5M memory allocated
6M memory allocated
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| # 创建上面的文件并编译
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# vim ~/mem-allocate.c
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# gcc ~/mem-allocate.c -o ~/mem-allocate
# 限制5M的上限
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 5M > memory.limit_in_bytes
#将当前bash加入到test中,这样这个bash创建的所有进程都会自动加入到test中
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo $$ >> cgroup.procs
#默认情况下,会启用oom killer
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.oom_control
oom_kill_disable 0
under_oom 0
oom_kill 1
#为了避免受swap空间的影响,设置swappiness为0来禁止当前cgroup使用swap
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 0 > memory.swappiness
#当分配第5M内存时,由于总内存量超过了5M,所以进程被kill了
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
Killed
#设置oom_control为1,这样内存达到限额的时候会暂停
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 1 >> memory.oom_control
#跟预期的一样,程序被暂停了
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
#--------------------------第二个shell窗口----------------------
#再打开一个窗口
dev@dev:~$ cd /sys/fs/cgroup/memory/test/
#这时候可以看到memory.oom_control里面under_oom的值为1,表示当前已经oom了
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.oom_control
oom_kill_disable 1
under_oom 1
oom_kill 2
#修改test的额度为7M
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 7M > memory.limit_in_bytes
# 切换会第一个窗口,发送程序又跑了两步,停在了6M
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
5M memory allocated
6M memory allocated
|
其他
进程迁移(migration)
当一个进程从一个 cgroup 移动到另一个 cgroup 时,默认情况下,该进程已经占用的内存还是统计在原来的 cgroup 里面,不会占用新 cgroup 的配额,但新分配的内存会统计到新的cgroup 中(包括 swap out 到交换空间后再 swap in 到物理内存中的部分)。
我们可以通过设置 memory.move_charge_at_immigrate 让进程所占用的内存随着进程的迁移一起迁移到新的 cgroup 中。
1
2
| enable: echo 1 > memory.move_charge_at_immigrate
disable:echo 0 > memory.move_charge_at_immigrate
|
注意: 就算设置为 1,但如果不是 thread group 的 leader,这个 task 占用的内存也不能被迁移过去。
换句话说,如果以线程为单位进行迁移,必须是进程的第一个线程,如果以进程为单位进行迁移,就没有这个问题。
当 memory.move_charge_at_immigrate 被设置成 1 之后,进程占用的内存将会被统计到目的 cgroup 中,如果目的 cgroup 没有足够的内存,系统将尝试回收目的 cgroup 的部分内存(和系统内存紧张时的机制一样,删除不常用的 file backed 的内存或者 swap out 到交换空间上,请参考Linux内存管理),如果回收不成功,那么进程迁移将失败。
注意:迁移内存占用数据是比较耗时的操作。
移除 cgroup
当 memory.move_charge_at_immigrate 为 0 时,就算当前 cgroup 中里面的进程都已经移动到其它 cgropu 中去了,由于进程已经占用的内存没有被统计过去,当前 cgroup 有可能还占用很多内存,当移除该 cgroup 时,占用的内存需要统计到谁头上呢?
答案是依赖memory.use_hierarchy 的值,
- 如果该值为 0,将会统计到 root cgroup 里;
- 如果值为1,将统计到它的父cgroup里面。
force_empty
当向 memory.force_empty 文件写入 0 时(echo 0 > memory.force_empty),将会立即触发系统尽可能的回收该 cgroup 占用的内存。该功能主要使用场景是移除 cgroup 前(cgroup中没有进程),先执行该命令,可以尽可能的回收该 cgropu 占用的内存,这样迁移内存的占用数据到父 cgroup 或者 root cgroup 时会快些。
memory.swappiness
该文件的值默认和全局的 swappiness(/proc/sys/vm/swappiness)一样,修改该文件只对当前 cgroup 生效,其功能和全局的 swappiness 一样,请参考Linux交换空间中关于swappiness 的介绍。
注意:有一点和全局的 swappiness 不同,那就是如果这个文件被设置成 0,就算系统配置的有交换空间,当前 cgroup 也不会使用交换空间。
memory.use_hierarchy
该文件内容为 0 时,表示不使用继承,即父子 cgroup 之间没有关系;当该文件内容为 1 时,子 cgroup 所占用的内存会统计到所有祖先 cgroup 中。
如果该文件内容为 1,当一个 cgroup 内存吃紧时,会触发系统回收它以及它所有子孙 cgroup的内存。
注意: 当该 cgroup 下面有子 cgroup 或者父 cgroup 已经将该文件设置成了 1,那么当前cgroup 中的该文件就不能被修改。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #当前cgroup和父cgroup里都是1
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.use_hierarchy
1
dev@dev:/sys/fs/cgroup/memory/test$ cat ../memory.use_hierarchy
1
#由于父cgroup里面的值为1,所以修改当前cgroup的值失败
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 > ./memory.use_hierarchy"
sh: echo: I/O error
#由于父cgroup里面有子cgroup(至少有当前cgroup这么一个子cgroup),
#修改父cgroup里面的值也失败
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 > ../memory.use_hierarchy"
sh: echo: I/O error
|
memory.soft_limit_in_bytes
有了 hard limit(memory.limit_in_bytes),为什么还要 soft limit 呢?hard limit 是一个硬性标准,绝对不能超过这个值。
而 soft limit 可以被超越,既然能被超越,要这个配置还有啥用?先看看它的特点
- 1)当系统内存充裕时,soft limit 不起任何作用
- 2)当系统内存吃紧时,系统会尽量的将 cgroup 的内存限制在 soft limit 值之下(内核会尽量,但不 100% 保证)
从它的特点可以看出,它的作用主要发生在系统内存吃紧时,如果没有 soft limit,那么所有的cgroup 一起竞争内存资源,占用内存多的 cgroup 不会让着内存占用少的 cgroup,这样就会出现某些 cgroup 内存饥饿的情况。如果配置了 soft limit,那么当系统内存吃紧时,系统会让超过 soft limit 的 cgroup 释放出超过 soft limit 的那部分内存(有可能更多),这样其它cgroup 就有了更多的机会分配到内存。
从上面的分析看出,这其实是系统内存不足时的一种妥协机制,给次等重要的进程设置 soft limit,当系统内存吃紧时,把机会让给其它重要的进程。
注意: 当系统内存吃紧且 cgroup 达到 soft limit 时,系统为了把当前 cgroup 的内存使用量控制在 soft limit 下,在收到当前 cgroup 新的内存分配请求时,就会触发回收内存操作,所以一旦到达这个状态,就会频繁的触发对当前 cgroup 的内存回收操作,会严重影响当前 cgroup 的性能。
memory.pressure_level
这个文件主要用来监控当前 cgroup 的内存压力,当内存压力大时(即已使用内存快达到设置的限额),在分配内存之前需要先回收部分内存,从而影响内存分配速度,影响性能,而通过监控当前 cgroup 的内存压力,可以在有压力的时候采取一定的行动来改善当前 cgroup 的性能,比如关闭当前 cgroup 中不重要的服务等。目前有三种压力水平:
low
- 意味着系统在开始为当前 cgroup 分配内存之前,需要先回收内存中的数据了,这时候回收的是在磁盘上有对应文件的内存数据。
medium
- 意味着系统已经开始频繁为当前 cgroup 使用交换空间了。
critical
- 快撑不住了,系统随时有可能 kill 掉 cgroup 中的进程。
如何配置相关的监听事件呢?和 memory.oom_control 类似,大概步骤如下:
- 利用函数 eventfd(2) 创建一个 event_fd
- 打开文件 memory.pressure_level,得到 pressure_level_fd
- 往 cgroup.event_control 中写入这么一串:
<event_fd> <pressure_level_fd> <level>
- 然后通过读 event_fd 得到通知
注意: 多个 level 可能要创建多个 event_fd,好像没有办法共用一个
Memory thresholds
我们可以通过 cgroup 的事件通知机制来实现对内存的监控,当内存使用量穿过(变得高于或者低于)我们设置的值时,就会收到通知。使用方法和 memory.oom_control 类似,大概步骤如下:
- 利用函数 eventfd(2) 创建一个 event_fd
- 打开文件 memory.usage_in_bytes,得到 usage_in_bytes_fd
- 往 cgroup.event_control 中写入这么一串:
<event_fd> <usage_in_bytes_fd> <threshold>
- 然后通过读 event_fd 得到通知
stat file
这个文件包含的统计项比较细,需要一些内核的内存管理知识才能看懂,这里就不介绍了(怕说错)。详细信息可以参考 Memory Resource Controller中的“5.2 stat file”。这里有几个需要注意的地方:
- 里面 total 开头的统计项包含了子 cgroup的数据(前提条件是 memory.use_hierarchy 等于1)。
- 里面的 ‘rss + file_mapped" 才约等于是我们常说的 RSS(ps aux 命令看到的 RSS)
- 文件(动态库和可执行文件)及共享内存可以在多个进程之间共享,不过它们只会统计到他们的 owner cgroup 中的 file_mapped 去。(不确定是怎么定义 owner 的,但如果看到当前 cgroup 的 file_mapped 值很小,说明共享的数据没有算到它头上,而是其它的cgroup)
小结
作用:限制 cgroups 中的进程占用的内存上限
用法:
- 1)
memory.limit_in_bytes
配置进程可以使用的内存上限(hard limit),当超过该阈值时,一般是尝试使用 swap,如果不行则直接 kill 掉。 - 2)
memory.soft_limit_in_bytes
配置进程可以使用的内存上行(soft limit),当系统内存不足时,cgroups 会优先将使用量超过 soft limit 的进程进行内存回收,腾出内存。 - 3)
memory.oom_control
参数配置内存使用量到达阈值时内核的处理行为,默认为 oom_kill。
原理:当进程使用内存超过memory.limit_in_bytes
之后,系统会根据 memory.oom_control
配置的行为进行处理,一般是尝试使用 swap,如果不行则直接 kill 掉。
本节没有介绍 swap 和 kernel 相关的内容,不过在实际使用过程中一定要留意 swap 空间,如果系统使用了交换空间,那么设置限额时一定要注意一点,那就是当 cgroup 的物理空间不够时,内核会将不常用的内存 swap out 到交换空间上,从而导致一直不触发 oom killer,而是不停的 swap out/in,导致 cgroup 中的进程运行速度很慢。
如果一定要用交换空间,最好的办法是限制 swap+物理内存 的额度,虽然我们在这篇中没有介绍这部分内容,但其使用方法和限制物理内存是一样的,只是换做写文件 memory.memsw.limit_in_bytes 罢了。
4. 小结
本文主要简单介绍了 pid、cpu、memory 这三个 subsystem 的作用和基本使用,具体如下:
subsystem | 功能 | 用法 | 原理 | 备注 |
---|
pid | 限制 cgroups 中进程使用的 pid 数 | 配置 subsystem 中的 pids.max 即可 | 当 cgroups 中的进程调用 fork 或者 clone 系统调用时会判断,subsystem 中配置的 pids.max 和当前 pids.current 的值,来确定是否能够创建新的进程(或线程) | linux 中的 pid 是有限的,通过该 subsystem 可以有效防止 fork 炸弹之类的恶意进程 |
cpu | 限制 cgroups 中进程使用的 cpu 上限 | 1)限制为具体值:用 cfs_period_us & cfs_quota_us 两个配置可以严格限制进程 cpu 使用量。 2)按比例分配:用 shares 配置,可以使得多个 cgroups 之间按比例分配所有 cpu。 | subsystem 负责提供配置,cfs 负责记录进程使用的 cpu 时间,达到阈值后就从调度层面进行限制,避免该进程继续使用 cpu。 | 一般使用 cfs_period_us & cfs_quota_us 方式限制具体值用得比较多。 |
memory | 限制 cgroups 中进程使用的 memory 上限 | 1)memory.limit_in_bytes 配置进程可以使用的内存上限(hard limit),当超过该阈值时,一般是尝试使用 swap,如果不行则直接 kill 掉。 2)memory.soft_limit_in_bytes 配置进程可以使用的内存上行(soft limit),当系统内存不足时,cgroups 会优先将使用量超过 soft limit 的进程进行内存回收,腾出内存。 3)memory.oom_control 参数配置内存使用量到达阈值时内核的处理行为,默认为 oom_kill。 | 当进程使用内存超过memory.limit_in_bytes 之后,系统会根据 memory.oom_control 配置的行为进行处理,一般是尝试使用 swap,如果不行则直接 kill 掉。 | 如果系统使用了交换空间,那么设置限额时一定要注意一点,那就是当 cgroup 的物理空间不够时,内核会将不常用的内存 swap out 到交换空间上,从而导致一直不触发 oom killer,而是不停的 swap out/in,导致 cgroup 中的进程运行速度很慢。 |
5. 参考
cgroups(7) — Linux manual page
美团技术团队—Linux资源管理之cgroups简介
Red Hat—资源管理指南