Contents

Docker教程(七)---深入剖析 Linux Cgroups 子系统:资源精细管理

https://img.lixueduan.com/docker/03-dark-blue-docker-logo.png

本章主要演示以下 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

#进入目录/sys/fs/cgroup/pids/并新建一个目录,即创建了一个子cgroup
lixd  /home/lixd $ cd /sys/fs/cgroup/pids
lixd  /sys/fs/cgroup/pids $ sudo mkdir test

再来看看 test 目录下的文件

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:

#--------------------------第一个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 中已经无法创建新的进程了:

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 查看

 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.currentpids.max 代表了当前 cgroup 及所有子孙 cgroup 的所有进程,所以子孙 cgroup 中的 pids.max 大小不能超过父 cgroup。

如果子 cgroup 中的 pids.max 设置的大于父 cgroup 里的值,会怎么样?

答案是子 cgroup 中的进程不光受子 cgroup 限制,还要受其父 cgroup 的限制。

#继续使用上面的两个窗口
#--------------------------第二个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 相关的子系统有 cpusetscpuacct cpu

  • 其中 cpuset 主要用于设置 CPU 的亲和性,可以限制 cgroup 中的进程只能在指定的 CPU 上运行,或者不能在指定的 CPU上运行,同时 cpuset 还能设置内存的亲和性。设置亲和性一般只在比较特殊的情况才用得着,所以这里不做介绍。

  • cpuacct 包含当前 cgroup 所使用的 CPU 的统计信息,信息量较少,有兴趣可以去看看它的文档,这里不做介绍。

本节只介绍 cpu 子系统,包括怎么限制 cgroup 的 CPU 使用上限及相对于其它 cgroup 的相对值。

创建子 cgroup

通用是创建子目录即可。

#进入/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:用来配置时间周期长度
    • 取值范围为1毫秒(ms)到1秒(s)
  • 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。

演示

#继续使用上面创建的子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
# 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

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 相关的文件除外),后面会详细介绍每个文件的作用:

 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 就可以了。

#重新打开一个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则是不限制
#回到第一个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

如果设置的限额比当前已经使用的内存少呢?

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 部分内存后还是不够会怎么样?

#--------------------------第一个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 kill
    • 0:关闭
    • 1:开启
  • under_oom:表示当前是否已经进入oom状态,也即是否有进程被暂停了。
  • oom_kill:oom 后是否执行 kill
    • 1:启动,oom 后直接 kill 掉对应进程
    • 2:关闭:当内核无法给进程分配足够的内存时,将会暂停该进程直到有空余的内存之后再继续运行。同时会更新 under_oom 状态
    • 注意:root cgroup 的 oom killer 是不能被禁用的

为了演示 OOM-killer 的功能,创建了下面这样一个程序,用来向系统申请内存,它会每秒消耗1M 的内存。

#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,然后编译并测试

#--------------------------第一个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
# 创建上面的文件并编译
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 中。

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 中的该文件就不能被修改。

#当前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 类似,大概步骤如下:

  1. 利用函数 eventfd(2) 创建一个 event_fd
  2. 打开文件 memory.pressure_level,得到 pressure_level_fd
  3. 往 cgroup.event_control 中写入这么一串:<event_fd> <pressure_level_fd> <level>
  4. 然后通过读 event_fd 得到通知

注意: 多个 level 可能要创建多个 event_fd,好像没有办法共用一个

Memory thresholds

我们可以通过 cgroup 的事件通知机制来实现对内存的监控,当内存使用量穿过(变得高于或者低于)我们设置的值时,就会收到通知。使用方法和 memory.oom_control 类似,大概步骤如下:

  1. 利用函数 eventfd(2) 创建一个 event_fd
  2. 打开文件 memory.usage_in_bytes,得到 usage_in_bytes_fd
  3. 往 cgroup.event_control 中写入这么一串:<event_fd> <usage_in_bytes_fd> <threshold>
  4. 然后通过读 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—资源管理指南