oom kill原理讲解

简介:

OOM就是out of memory的缩写,虽然linux kernel有很多的内存管理技巧(从cache中回收、swap out等)来满足各种应用空间的vm内存需求,但是,当你的系统配置不合理,让一匹小马拉大车的时候,linux kernel会运行非常缓慢并且在某个时间点分配page frame的时候遇到内存耗尽、无法分配的状况。

应对这种状况首先应该是系统管理员,他需要首先给系统增加内存,不过对于kernel而言,当面对OOM的时候,要根据OOM参数来进行相应的处理。

oom kiiler会在内存紧张的时候,会依次kill内存占用较高的进程,发送Sig15(SIGTERM)或Sig9(SIGKILL),取决于内核版本(可见uname -a,>= 2.6.32只会发送sigkill,并在/var/log/message中进行记录。里面会记录一些如pid,process name,cpu mask,trace等信息,通过监控可以发现类似问题

1.什么是oom killer

他是Linux内核设计的一种机制,在内存不足的会后,选择一个占用内存较大的进程并kill掉这个进程,以满足内存申请的需求(内存不足的时候该怎么办,其实是个两难的事情,oom killer算是提供了一种方案吧)

2.在什么时候触发?

前面说了,在内存不足的时候触发,再往细节看,主要牵涉到【linux的物理内存结构】和【overcommit机制】

2.1 内存结构 node、zone、page、order

对于物理内存内存,linux会把它分很多区(zone),zone上还有node的概念,zone下面是很多page,这些page是根据buddy分配算法组织的,看下面两张图:

上面的概念做下简单的介绍,对后面分析oom killer日志很有必要:

  • Node:每个CPU下的本地内存节点就是一个Node,如果是UMA架构下,就只有一个Node0,在NUMA架构下,会有多个Node

  • Zone:每个Node会划分很多域Zone,大概有下面这些:

    • 1) ZONE_DMA:定义适合DMA的内存域,该区域的长度依赖于处理器类型。比如ARM所有地址都可以进行DMA,所以该值可以很大,或者干脆不定义DMA类型的内存域。而在IA-32的处理器上,一般定义为16M。

    • 2) ZONE_DMA32:只在64位系统上有效,为一些32位外设DMA时分配内存。如果物理内存大于4G,该值为4G,否则与实际的物理内存大小相同。

    • 3) ZONE_NORMAL:定义可直接映射到内核空间的普通内存域。在64位系统上,如果物理内存小于4G,该内存域为空。而在32位系统上,该值最大为896M。

    • 4) ZONE_HIGHMEM:只在32位系统上有效,标记超过896M范围的内存。在64位系统上,由于地址空间巨大,超过4G的内存都分布在ZONE_NORMA内存域。

    • 5) ZONE_MOVABLE:伪内存域,为了实现减小内存碎片的机制。

    • 分配价值链

      • 除了只能在某个区域分配的内存(比如ZONE_DMA),普通的内存分配会有一个“价值”的层次结构,按分配的“廉价度”依次为:ZONE_HIGHMEM > ZONE_NORMAL > ZONE_DMA。

      • 即内核在进行内存分配时,优先从高端内存进行分配,其次是普通内存,最后才是DMA内存

  • Page:zone下面就是真正的内存页了,每个页基础大小是4K,他们维护在一个叫free_area的数组结构中

    • order:数组的index,也叫order,实际对应的是page的大小,比如order为0,那么就是一堆1个空闲页(4K)组成的链表,order为1,就是一堆2个空闲页(8K)组成的链表,order为2,就是一堆4个空闲页(16K)组成的链表

2.2 overcommit

根据2.1,已经知道物理内存的大概结构以及分配的规则,不过实际上还有虚拟内存的存在,他的overcommit机制和oom killer有很大关系:

在实际申请内存的时候,比如申请1G,并不会在物理区域中分配1G的真实物理内存,而是分配1G的虚拟内存,等到需要的时候才去真正申请物理内存,也就是说申请不等于分配

所以说,可以申请比物理内存实际大的内存,也就是overcommit,这样会面临一个问题,就是当真的需要这么多内存的时候怎么办—>oom killer!

vm.overcommit_memory 接受三种值:

  • 0 – Heuristic overcommit handling. 这是缺省值,它允许overcommit,但过于明目张胆的overcommit会被拒绝,比如malloc一次性申请的内存大小就超过了系统总内存

  • 1 – Always overcommit. 允许overcommit,对内存申请来者不拒。

  • 2 – Don’t overcommit. 禁止overcommit。

3.oom killer 怎么挑选进程?

linux会为每个进程算一个分数,最终他会将分数最高的进程kill

  • /proc/<pid>/oom_score_adj

    • 在计算最终的 badness score 时,会在计算结果是中加上 oom_score_adj,取值范围为-1000到1000

    • 如果将该值设置为-1000,则进程永远不会被杀死,因为此时 badness score 永远返回0。

  • /proc/<pid>/oom_adj

    • 取值是-17到+15,取值越高,越容易被干掉。如果是-17,则表示不能被kill

    • 该设置参数的存在是为了和旧版本的内核兼容

  • /proc/<pid>/oom_score

    • 这个值是系统综合进程的内存消耗量、CPU时间(utime + stime)、存活时间(uptime - start time)和oom_adj计算出的,消耗内存越多分越高,存活时间越长分越低

  • 子进程内存:Linux在计算进程的内存消耗的时候,会将子进程所耗内存的一半同时算到父进程中。这样,那些子进程比较多的进程就要小心了。

  • 其他参数(不是很关键,不解释了)

    • /proc/sys/vm/oom_dump_tasks

    • /proc/sys/vm/oom_kill_allocating_task

    • /proc/sys/vm/panic_on_oom

  • 关闭 OOM killer

    • sysctl -w vm.overcommit_memory=2

    • echo “vm.overcommit_memory=2” >> /etc/sysctl.conf

3.1 找出最优可能被杀掉的进程

vi oomscore.sh

#!/bin/bash
for proc in $(find /proc -maxdepth 1 -regex '/proc/[0-9]+'); do
    printf "%2d %5d %s\n" \
        "$(cat $proc/oom_score)" \
        "$(basename $proc)" \
        "$(cat $proc/cmdline | tr '\0' ' ' | head -c 50)"
done 2>/dev/null | sort -nr | head -n 10

chmod +x oomscore.sh
./oomscore.sh
18   981 /usr/sbin/mysqld
 4 31359 -bash
 4 31056 -bash
 1 31358 sshd: root@pts/6
 1 31244 sshd: vpsee [priv]
 1 31159 -bash
 1 31158 sudo -i
 1 31055 sshd: root@pts/3
 1 30912 sshd: vpsee [priv]
 1 29547 /usr/sbin/sshd -D

3.2 避免的oom killer的方案

  • 直接修改/proc/PID/oom_adj文件,将其置位-17

  • 修改/proc/sys/vm/lowmem_reserve_ratio

  • 直接关闭oom-killer

参考:

如果您memory为该容器设置了4GiB 的限制,则kubelet(和 容器运行时)执行限制。运行时可防止容器使用超出配置的资源限制的容器。例如:当容器中的进程尝试消耗的内存超过允许的数量时,系统内核将终止尝试分配的进程,并显示内存不足(OOM)错误

每个cgroup维护每个cgroup LRU,其结构与
全局VM。当cgroup超过其限制时,我们首先尝试
从cgroup回收内存,以便为新的磁盘空间
cgroup触摸过的页面。如果回收失败,
调用OOM例程以选择并杀死该任务中最大的任务
cgroup

补充-图解kubernetes资源QOS机制实现原理

总结说明:

k8s中所说的oom分两种:1是node节点oom kill,2是pod内部容器oom kill

前者可以通过脚本查看,可用来打印当前系统上 oom_score 分数最高(最容易被 OOM Killer 杀掉)的进程

# vi oomscore.sh
#!/bin/bash
for proc in $(find /proc -maxdepth 1 -regex '/proc/[0-9]+'); do
    printf "%2d %5d %s\n" \
        "$(cat $proc/oom_score)" \
        "$(basename $proc)" \
        "$(cat $proc/cmdline | tr '\0' ' ' | head -c 50)"
done 2>/dev/null | sort -nr | head -n 10

kubelet根据pod的Qos为每个容器设置一个oom_score_adj,如果kubelet无法在系统OOM之前回收足够的内存。则oom_killer会根据内存使用比例来计算oom_score,最后结果和oom_score_adj相加,得分最高的pod将会首先被驱逐。

OOM-KILL介绍

oom_killer(out of memory killer)是Linux内核的一种内存管理机制,在系统可用内存较少的情况下,内核为保证系统还能够继续运行下去,会选择杀掉一些进程释放掉一些内存。

通常oom_killer的触发流程是:进程A想要分配物理内存(通常是当进程真正去读写一块内核已经“分配”给它的内存)->触发缺页异常->内核去分配物理内存->物理内存不够了,触发OOM

oom kiiler会在内存紧张的时候,会依次kill内存占用较高的进程,发送Sig15(SIGTERM)或Sig9(SIGKILL),取决于内核版本(可见uname -a,>= 2.6.32只会发送sigkill,并在/var/log/message中进行记录

一句话说明oom_killer的功能:

当系统物理内存不足时,oom_killer遍历当前所有进程,根据进程的内存使用情况进行打分,然后从中选择一个分数最高的进程,杀之取内存

函数解析:

oom_killer的处理主要集中在mm/oom_kill.c

核心函数为out_of_memory,函数处理流程:

  1. 通知系统中注册了oom_notify_list的模块释放一些内存,如果从这些模块中释放出了一些内存,那么皆大欢喜,直接结束oom killer流程,回收失败, 那只有进入下一步开始oom_killer了;

  2. 触发oom killer通常是由当前进程进行内存分配所引起,而如果当前进程已经挂起了一个SIG_KILL信号,直接选中当前进程,否则进入下一步;

  3. check_panic_on_oom检查系统管理员的态度,看oom时是进行oom killer还是直接panic掉,如果进行oom killer,则进入下一步;

  4. 如果系统管理员规定,谁引起oom,杀掉谁,那就杀掉正在尝试分配内存的进程,oom killer结束,否则进入下一步;

  5. 调用select_bad_process选中合适进程,然后调用oom_kill_process杀死选中进程,如果不幸select_bad_process没有选出任何进程,那么内核走投无路,只有panic了。

详情见:Linux OOM机制分析

系统相关配置

  • /proc/sys/vm/panic_on_oom:配置系统产生oom时的动作

  • /proc/sys/vm/oom_kill_allocating_task:为true的时候,直接kill掉当前想要分配内存的进程(此进程能够被kill时)

  • /proc/<pid>/oom_score_adj(或/proc/<pid>/oom_adj):配置PID指定进程的oom权重,子进程继承该权重值

  • /proc/sys/vm/would_have_oomkilled:为true时并不会真正杀死oom killer选中进程,只是打印一条警告信息

  • echo f >/proc/sysrq-trigger模拟oom

  • cat /proc/<PID>/oom_score:查看PID进程的oom分数

k8s中oom说明

如果节点在 kubelet 回收内存之前经历了系统 OOM(内存不足)事件,它将基于 oom-killer 做出响应。

Kubernetes会根据QoS设置oom的评分调整参数oom_score_adj,oom_killer 根据 内存使用情况算出oom_score, 并且和oom_score_adj综合评价,进程的评分越高,当发生oom时越优先被kill。

kubelet 基于 pod 的 service 质量为每个容器设置一个 oom_score_adj 值。

Service 质量

oom_score_adj

Guaranteed

-998

BestEffort

1000

Burstable

min(max(2, 1000 - (1000 * memoryRequestBytes) / machineMemoryCapacityBytes), 999)

如果 kubelet 在节点经历系统 OOM 之前无法回收内存,oom_killer将基于它在节点上使用的内存百分比算出一个 oom_score,并加上 oom_score_adj 得到容器的有效 oom_score,然后结束得分最高的容器。

预期的行为应该是拥有最低 service 质量并消耗和调度请求相关内存量最多的容器第一个被结束,以回收内存。

和 pod 驱逐不同,如果一个 pod 的容器是被 OOM 结束的,基于其 RestartPolicy,它可能会被 kubelet 重新启动。

例子单个节点可分配内存为8010196Ki, 大约7822.45Mi。 根据Burstable 的计算方式:


request 200Mi: (1000 - 1000*200/7822.45) 约为975

request 400Mi: (1000 - 1000*400/7822.45) 约为950

设置oom 规则代码: https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/qos/policy.go

QOS是k8s中一种资源保护机制,其主要是针对不可压缩资源比如的内存的一种控制技术,比如在内存中其通过为不同的Pod和容器构造OOM评分,并且通过内核的策略的辅助,从而实现当节点内存资源不足的时候,内核可以按照策略的优先级,优先kill掉哪些优先级比较低(分值越高优先级越低)的Pod,今天来分析下背后的实现

1.关键基础特性

一切皆文件

在Linux中一切皆文件,控制CGroup本身也是通过配置文件进行的,这是我创建的一个内存Lmits为200M的Pod的容器的配置

# pwd
/sys/fs/cgroup
# cat ./memory/kubepods/pod8e172a5c-57f5-493d-a93d-b0b64bca26df/f2fe67dc90cbfd57d873cd8a81a972213822f3f146ec4458adbe54d868cf410c/memory.limit_in_bytes
209715200

oom score

在遇到较高内存使用压力时,Linux 内核会杀掉一些不太重要的进程,腾出空间保障系统正常运行。它会给每个进程(/proc/$ pid / oom_score)分配一个得分(oom_score),分数越高,被 OOM 的概率就越大。

这个参数本身只反映该进程的可用资源在系统中所占的百分比,并没有“该进程有多重要”的概念

oomScoreAdj

手动调整 oom_score 可以通过 oom_score_adj 来实现,它允许开发者在内存不足的情况下杀死指定进程

具体做法是把可调参数 /proc/pid/oom_score_adj 直接添加到 badness() 分数中,范围从 -1000(OOM_SCORE_ADJ_MIN)到 +1000(OOM_SCORE_ADJ_MAX),使某些任务总是会被考虑 OOM,某些任务则永远不会被 OOM。

如果我们调整了 PROC1 的 oom_score_adj (echo "-1000" > /proc/$(pidof PROC1)/oom_score_adj),系统在 OOM 时就会先杀死其他进程。

参考文章

Last updated