通过memcg触发oom

简介: 通过memcg触发oom

当应用程序申请内存时,如果系统内存不足或者到达了memory cgroup设置的limit,会触发内存回收,如果还是得不到想要的数量的内存,最后会出发oom,选择一个进程杀死,释放其占用的内存。

下面通过实验来学习一下通过memory cgroup触发oom。

内核:linux-5.14

发行版:ubuntu20.4 (默认使用的是cgroup v1)

发行版:ubuntu22.04 (默认使用的是cgroup v2)

作者: pengdonglin137@163.com

oom对应的源码是mm/oom_kill.c。

cgroup v1

在memory cgroup的挂载目录下创建一个子目录,然后内核会自动在这个目录下创建一些预定义的文件,下面主要用到如下几个节点:

memory.oom_control     # 控制是否启动oom,以及查看当前的oom状态
memory.limit_in_bytes  # 用于设置一个memcg的内存上限
memory.usage_in_bytes  # 用于查看一个memcg当前的内存使用量
cgroup.procs           # 用于将某个进程加入到这个memcg
  • 关闭swap,防止内存回收时匿名内存被交换出去影响测试
swapon -s
swapoff <swap_file>
  • 在memory cgroup顶层目录新建一个oom_test控制组
cd /sys/fs/cgroup/memory
mkdir oom_test
  • 建立三个终端,然后将三个终端进程加入到oom_test控制组中
    分别在三个终端中执行如下命令:
echo $$ > /sys/fs/cgroup/memory/oom_test/cgroup.procs
  • 设置内存限制
echo 80M > memory.limit_in_bytes
  • 分别在三个终端里执行三个不同的命令
  1. 终端1: 执行alloc_memory_1,这个程序每敲一次回车申请10MB的内存
  2. 终端2: 执行alloc_memory_2,这个程序每敲一次回车申请10MB的内存
  3. 终端3: 执行print_counter.sh,每秒打印一行log,表示还活着
  • 在另外一个终端观察内存使用
watch -n1 cat /sys/fs/cgroup/memory/oom_test/memory.usage_in_bytes
  • 在终端1和终端2中交替敲回车
    观察oom_test的变化,看哪个进程被杀死。
  • 结果
    观察到如下现象,当在终端2中敲回车后,内存超过设置的limit时,终端1中的进程alloc_memory_1被kill掉了,然后控制的内存使用量瞬间降了下来。而终端2和终端3中的
    进程还活着。同时内核输出如下的log:

oom的内核log

 

  • 查看节点memory.oom_control
root@ubuntu:/sys/fs/cgroup/memory/oom_test# cat memory.oom_control
oom_kill_disable 0  # 表示当前控制组的oom kill功能处于开启状态
under_oom 0         # 用于计数是否正处于oom,内核提供了两个接口函数mem_cgroup_mark_under_oom和mem_cgroup_unmark_under_oom来操作这个计数
oom_kill 1          # 表示已经杀死了一个进程
  • 关闭oom_test控制组的oom功能
    执行下面的命令关闭oom_test的oom功能:
root@ubuntu:/sys/fs/cgroup/memory/oom_test# echo 1 > memory.oom_control
root@ubuntu:/sys/fs/cgroup/memory/oom_test# cat memory.oom_control
oom_kill_disable 1
under_oom 0
oom_kill 1

然后重新在终端1中运行alloc_memory_1,然后继续在终端1中敲回车,观察控制组的内存占用以及alloc_memory_1进程的状态。

可以看到如下结果:

当控制组的内存使用到达limit后,不再增长,并且alloc_memory_1也不动了,通过ps查看进程状态,发现alloc_memory_1编程了D状态,即不可中断睡眠。

root@ubuntu:/home/pengdl/work/oom_test# ps -aux | grep alloc_me
root      343405  0.0  0.2  43472 42096 pts/4    S+   00:36   0:00 ./alloc_memory_2
root      369713  0.0  0.2  43472 41252 pts/2    D+   01:12   0:00 ./alloc_memory_1

此时查看memory.oom_control的内容:

root@ubuntu:/sys/fs/cgroup/memory/oom_test# cat memory.oom_control
oom_kill_disable 1
under_oom 1
oom_kill 1

其中,under_oom是1,因为此时alloc_memory_1在处理oom时被阻塞了,没有处理完oom。

内核源码流程:

handle_mm_fault
  -> __set_current_state(TASK_RUNNING)
  -> __handle_mm_fault(vma, address, flags)
    -> handle_pte_fault
      -> do_anonymous_page(vmf)
        -> alloc_zeroed_user_highpage_movable
        -> mem_cgroup_charge
          -> __mem_cgroup_charge
            -> try_charge
              -> try_charge_memcg
                -> mem_cgroup_oom
                  -> 如果memcg->oom_kill_disable是1的话:
                    if (memcg->oom_kill_disable) {
                        if (!current->in_user_fault)
                          return OOM_SKIPPED;
                        css_get(&memcg->css);
                        current->memcg_in_oom = memcg;
                        current->memcg_oom_gfp_mask = mask;
                        current->memcg_oom_order = order;
                        return OOM_ASYNC;
                      }
                -> return -ENOMEM;
        -> return VM_FAULT_OOM
  -> mem_cgroup_oom_synchronize
    -> prepare_to_wait(&memcg_oom_waitq, &owait.wait, TASK_KILLABLE)

在mem_cgroup_oom_synchronize会将当前进程设置为TASK_KILLABLE状态,被称为中度睡眠,是(TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)两种状态的组合,即还不完全是D,还可以响应kill信号。

此时,我们可以在终端1中用ctrl c杀死alloc_memory_1进程。

  • 接着上一步,重新开启oom_test的oom功能
    执行下面的命令开启oom_test的oom,然后观察现象,注此时alloc_memory_1处于D
root@ubuntu:/sys/fs/cgroup/memory/oom_test# echo 0 > memory.oom_control
root@ubuntu:/sys/fs/cgroup/memory/oom_test# cat memory.oom_control
oom_kill_disable 0
under_oom 0
oom_kill 2

可以看到,此时oom被打开,然后终端2中的alloc_memory_2进程被杀死了,节点memory.oom_controlunder_oom变成了0,表示oom处理完了,而终端1中的进程alloc_memory_1的状态变成了S:

root@ubuntu:/home/pengdl/work/oom_test# ps -aux | grep alloc_me
root      369713  0.0  0.3  63960 62632 pts/2    S+   01:12   0:00 ./alloc_memory_1

原因是,当开启oom时,会将memcg_oom_waitq中睡眠的进程唤醒,当alloc_memory_1唤醒后,继续运行,因为上次没有处理成功缺页,所以mmu中的pte的还不是有效的映射,所以还是会触发跟上一次相同的缺页

终端,由于此时开启了oom,会选中alloc_memory_2并且杀掉以释放出内存。

mem_cgroup_oom_control_write
  -> memcg->oom_kill_disable = val
  -> memcg_oom_recover
    -> __wake_up(&memcg_oom_waitq, TASK_NORMAL, 0, memcg)

alloc_memory_2被杀死的内核log

cgroup v2

cgroup v2默认也被挂载在/sys/fs/cgroup/下,用到的文件:

cgroup.procs     # 用于将进程加入到控制组
memory.max       # 用于设置控制组的内存上限
memory.current   # 用于查看控制组的内存消耗
memory.oom.group # 用于控制oom发生时,是否回收控制组中的所有进程

cgroupv2中删除了控制oom是否开启的文件,即oom始终开启。并且多了一个memory.oom.group的文件,表示当发生oom时,是否杀死这个控制组下的所有进程。

  • 创建oom_test控制组
  • 上前面同样的操作步骤,关闭全局swap,创建三个终端,将终端进程加入到oom_test控制组,然后分别运行alloc_memory_1、alloc_memory_2以及print_counter.sh
  • 设置内存上限
root@ubuntu2204:/sys/fs/cgroup/oom_test# echo 80M > memory.max
root@ubuntu2204:/sys/fs/cgroup/oom_test# cat memory.max
83886080
  • 在终端1和终端2中交替敲回车
    可以看到跟之前的一样,当在终端2中输入回车后,导致内存超过max,从而触发了内存回收,由于没有swap,并且页缓存也不够,这样会触发oom,选中了终端1中的进程alloc_memory_1,杀死后,
    控制组的内存占用降了下来,终端2和终端3终端的进程还在运行。

回收alloc_memory_1的内核log

 

  • 开启oom.group,观察是否会杀死控制组中的所有进程

在终端1中重新启动alloc_memory_1,然后执行下面的命令开启oom.group

root@ubuntu2204:/sys/fs/cgroup/oom_test# cat memory.oom.group
0
root@ubuntu2204:/sys/fs/cgroup/oom_test# echo 1 > memory.oom.group
root@ubuntu2204:/sys/fs/cgroup/oom_test# cat memory.oom.group
1
  • 在终端1中连续敲回车,观察现象

可以看到,当控制组的内存占用超过max后,会将控制组中的进程全部杀死:

root@ubuntu2204:/sys/fs/cgroup/oom_test# cat cgroup.procs

可以看到,这个控制组下一个进程也没有了。在看看内核log:

控制组中的所有进程全部被杀死的log

代码流程:

out_of_memory
  -> select_bad_process
  -> oom_kill_process
    -> mem_cgroup_get_oom_group
    -> __oom_kill_process
    -> mem_cgroup_scan_tasks
C 复制 全屏

完。

相关文章
|
17天前
|
Java Python
设置垃圾收集的触发条件
设置垃圾收集的触发条件
|
4月前
|
Java Linux 容器
JVM内存问题之什么是OOM-Killer,它通常会在什么情况下触发
JVM内存问题之什么是OOM-Killer,它通常会在什么情况下触发
115 2
|
缓存 安全 Java
JVM中垃圾回收相关参数介绍:大页和NUMA参数+GC日志相关参数
大页和NUMA参数 本节介绍JVM为使用OS而提供的大页和NUMA特性相关的参数。 该参数控制JVM向OS请求内存时使用大页的粒度。使用该参数时需要对OS进行配置,只有OS允许时才能真正启动。参数的默认值与平台相关,一般为false。 在允许使用大页方式向OS请求内存时,如果堆空间小于该阈值,则强制禁止大页使用。该参数的默认值为128MB。 在允许使用大页方式向OS请求内存时,优先在本地节点进行分配。该参数仅适用于Windows系统。 在允许使用大页方式向OS请求内存时,如果OS提供了多种大页的设置,可通过该参数选择其中的大页设置。参数的默认值为0,表示使用OS默认的大页设置。
177 0
|
存储 算法 Java
CMS 触发GC(Allocation Failure)解析
针对GC中发生的"Allocation Failure"
824 0
|
数据可视化 Java
利用jstat命令排查OOM和内存泄漏
利用jstat命令排查OOM和内存泄漏
460 0
利用jstat命令排查OOM和内存泄漏
|
算法 Java
一文搞懂Y-GC和Full GC的触发条件
1 Young GC触发时机 一般在新生代Eden区满后触发,采用复制算法回收新生代垃圾。
1023 0
|
Java
四、理解GC日志、内存分配与回收策略
最前面的数字“33.125:”和“100.667”代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数 GC日志开头的“[GC” "[FullGC"说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的,如果有“Full”,说明这次GC是发生了stop-the-world的。 接下来“[DefNew”、"[Tenured" “[Perm”标识GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的,例如上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,所以显示为“DefNew”。如果是ParNew收
103 0
|
监控 Java 数据安全/隐私保护
JVM频繁GC内存溢出排查
GC(Garbage collection)频繁和堆内存溢出原因简单来说是对象占用堆空间难以回收,新对象无法分配触发GC或者直接导致内存溢出,最终进程结束。
465 0
|
Java C#
jvm调优【减少GC频率和Full GC次数】中Gc是什么
1. Java中为什么会有GC机制呢 2. 对于Java的GC哪些内存需要回收 内存运行时 JVM 会有一个运行时数据区来管理内存。它主要包括 5 大部分:程序计数器(Program Counter Register)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap). 3. Java的GC什么时候回收垃圾 在 Java,C#等语言中,比较主流的判定一个对象已死的方法是:可达性分析(Reachability Analysis).
256 0
|
运维 监控 Kubernetes
JVM 输出 GC 日志导致 JVM 卡住
JVM 输出 GC 日志导致 JVM 卡住
JVM 输出 GC 日志导致 JVM 卡住