监测 Linux 内核内存 OOM 事件

监测 Linux 内核内存 OOM 事件

Posted by 陈谭军 on Wednesday, May 1, 2024 | 阅读 |,阅读约 9 分钟

内存 OOM

Linux内核有个机制叫OOMkiller(Out-Of-Memory killer),该机制会监控那些占用内存过大,尤其是瞬间很快消耗大量内存的进程,为了防止内存耗尽而内核会把该进程杀掉。典型的情况是:某天一台机器突然ssh远程登录不了,但能ping通,说明不是网络的故障,原因是sshd进程被OOM killer杀掉了(多次遇到这样的假死状况)。重启机器后查看系统日志 /var/log/messages 会发现 Out of Memory: Kill process 1865(sshd)类似的错误信息。

Linux 内核根据应用程序的要求分配内存,通常来说应用程序分配了内存但是并没有实际全部使用,为了提高性能,这部分没用的内存可以留作它用,这部分内存是属于每个进程的,内核直接回收利用的话比较麻烦,所以内核采用一种过度分配内存(over-commit memory)的办法来间接利用这部分 “空闲” 的内存,提高整体内存的使用效率。一般来说这样做没有问题,但当大多数应用程序都消耗完自己的内存的时候麻烦就来了,因为这些应用程序的内存需求加起来超出了物理内存(包括 swap)的容量,内核(OOM killer)必须杀掉一些进程才能腾出空间保障系统正常运行。

我们可以通过一些内核参数来调整 OOM killer 的行为,避免系统在那里不停的杀进程。

  • Linux下每个进程都有个OOM权重,在/proc//oom_adj里面,取值是-17到+15,取值越高,越容易被干掉;
  • linux内核会通过特定的算法给每个进程计算一个分数来决定杀哪个进程,每个进程的oom分数可以/proc/PID/oom_score中找到(分数越高,越容易被干掉);
  • 我们可以通过调控每个进程的/proc//oom_adj来影响到每个进程的/proc/PID/oom_score;(正比例关系,oom_adj越大,oom_score分数越高,越容易被干掉);

何时触发 OOM killer 机制

Linux下允许程序申请比系统可用内存更多的内存,这个特性叫Overcommit。这样做是出于优化系统考虑,因为不是所有的程序申请了内存就立刻使用的,当你使用的时候说不定系统已经回收了一些资源。 不幸的是,当你用到这个 Overcommit 给你的内存的时候,系统还没有资源的话,OOM killer就跳出来了。参数 /proc/sys/vm/overcommit_memory 可以控制进程对内存过量使用的应对策略。

  • 当overcommit_memory=0 允许进程轻微过量使用内存,但对于大量过载请求则不允许,也就是当内存消耗过大就是触发OOM killer。
  • 当overcommit_memory=1 永远允许进程overcommit,不会触发OOM killer。
  • 当overcommit_memory=2 永远禁止overcommit,不会触发OOM killer。

内核在触发OOM机制时会调用到out_of_memory()函数,此函数的调用顺序以下:

_alloc_pages  //内存分配时调用
    |-->__alloc_pages_nodemask
       |--> __alloc_pages_slowpath
           |--> __alloc_pages_may_oom
              | --> out_of_memory   //触发

OOM Killer选择进程过程如下所示:

当系统内存分配失败时就会调用out_of_memory()函数,该函数中调用了select_bad_process()函数,select_bad_process()通过badness()函数获取目标进程对应的分数,而最终分数最高的进程会被OOM Killer清理,badness()函数内部有一套规则来选择目标进程:

  • 内核为自己保留运行所需最小内存并尝试回收大量的存;
  • 优先剔除内存占用高的进程,尝试杀死最少数量的进程;
  • 一些算法可以调整进程被终止的优先级;

OOM 系统参数

  • 参数 /proc/sys/vm/overcommit_memory 可以控制进程对内存过量使用的应对策略。
    • 当overcommit_memory=0 允许进程轻微过量使用内存,但对于大量过载请求则不允许,也就是当内存消耗过大就是触发OOM killer。
    • 当overcommit_memory=1 永远允许进程overcommit,不会触发OOM killer。
    • 当overcommit_memory=2 永远禁止overcommit,不会触发OOM killer。
  • 参数 /proc/sys/vm/panic_on_oom 用来控制当内存不足时该如何做。
    • 值为0:内存不足时,启动 OOM killer。
    • 值为1:内存不足时,有可能会触发 kernel panic(系统重启),也有可能启动 OOM killer。
    • 值为2:内存不足时,表示强制触发 kernel panic,内核崩溃GG(系统重启)。
  • 参数 /proc/sys/vm/oom_kill_allocating_task 用来决定触发OOM时先杀掉哪种进程
    • 值为0:会 kill 掉得分最高的进程。
    • 值为非0:会kill 掉当前申请内存而触发OOM的进程。
    • 当然,一些系统进程(如init)或者被用户设置了oom_score_adj的进程等可不是说杀就杀的。
  • 参数 /proc/sys/vm/oom_dump_tasks 用来记录触发OOM时记录哪些日志。oom_dump_tasks参数可以记录进程标识信息、该进程使用的虚拟内存总量、物理内存、进程的页表信息等。值为0:关闭打印上述日志。在大型系统中,可能存在上千进程,逐一打印使用内存信息可能会造成性能问题。值为非0:有三种情况会打印进程内存使用情况。
    • 由 OOM 导致 kernel panic 时。
    • 没有找到符合条件的进程 kill 时。
    • 找到符合条件的进程并 kill 时。
  • 参数 oom_adj、oom_score_adj 和 oom_score 用来控制进程打分(分数越高,就先杀谁)。这三个参数的关联性比较紧密,都和具体的进程相关,位置都是在 /proc/进程PID/ 目录下。内核会对进程打分(oom_score),主要包括两部分,系统打分和用户打分。系统打分就是根据进程的物理内存消耗量(进程自身的空间、swap空间、页缓存空间);用户打分就是 oom_score_adj 的值。如果用户指定 oom_score_adj 的值为 -1000,也就是表示禁止 OOM killer 杀死该进程。

内存 OOM 原因

内存溢出(OOM)通常是由以下几个原因引起的:

  • 内存使用超过了物理硬件限制:如果程序或系统尝试使用超过其物理内存的内存,就会发生内存溢出。
  • 内存泄漏:内存泄漏是指程序中已经结束生命周期的对象却不能被垃圾回收机制回收的情况,从而导致这部分内存无法再次使用。随着时间的推移,内存泄漏可能会消耗掉所有可用的内存,从而导致OOM。
  • 不适当的内存分配:有时,程序可能会错误地请求过多的内存,导致系统无法满足这种需求。例如,程序可能会尝试一次性分配超过系统可用内存的数组。
  • 资源竞争:在集群环境中,如果多个进程或节点竞争同一内存资源,可能会导致内存不足。
  • 缺乏有效的内存管理:在一些情况下,操作系统或应用程序的内存管理策略可能不够有效,导致内存的使用不够高效,从而引发OOM。

内存 OOM 排查

定位内存溢出(OOM)问题通常需要以下步骤:

  • 查看系统日志:系统日志通常是查找OOM问题的第一步。在Linux系统中,可以查看/var/log/messages(如果配置了syslog,日志可能在/var/log/syslog)或者使用dmesg命令来查看内核日志,这些日志中通常会记录OOM发生的时间和被杀死的进程。
  • 分析进程的内存使用情况:可以使用top,ps,free等命令来实时查看进程的内存使用情况。特别是,top命令可以按内存使用量对进程排序,帮助我们找到消耗内存最多的进程。
  • 使用调试工具:有一些工具可以帮助我们更详细地分析内存使用情况。例如,valgrind可以帮助我们检测内存泄漏,gdb可以让我们在运行时检查进程的内存使用情况。
  • 审查代码:如果确定是某个进程导致的OOM,那么需要审查这个进程的代码,查找可能导致内存使用过多的部分。这可能包括查找内存泄漏,或者查找不必要的大内存分配。
  • 使用性能分析工具:一些性能分析工具,如perf,gprof,JProfiler等,可以提供关于程序运行时内存使用情况的详细信息,帮助我们找到内存使用过多的代码部分。

eBPF 排查 OOM

oom的触发就是在内存无法分配的时候,选择一个最“差”的进程发送kill信号。oom_kill_process这个函数是主要入口,定义是static void oom_kill_process(struct oom_control *oc, const char *message),其中oc->victim是要被杀掉的进程,如果有cgroup会把相同内存cgroup的进程都杀掉。

/*
 * setup_per_zone_lowmem_reserve - called whenever
 *	sysctl_lowmem_reserve_ratio changes.  Ensures that each zone
 *	has a correct pages reserved value, so an adequate number of
 *	pages are left in the zone after a successful __alloc_pages().
 */

static void setup_per_zone_lowmem_reserve(void) 我们使用 eBPF 程序(跟踪 oom_kill_process 内核函数)来统计内存 OOM 的情况。内核源码如下所示:

static void oom_kill_process(struct oom_control *oc, const char *message)
/*
 * Kill provided task unless it's secured by setting
 * oom_score_adj to OOM_SCORE_ADJ_MIN.
 */
static int oom_kill_memcg_member(struct task_struct *task, void *message)
{
	if (task->signal->oom_score_adj != OOM_SCORE_ADJ_MIN &&
	    !is_global_init(task)) {
		get_task_struct(task);
		__oom_kill_process(task, message);
	}
	return 0;
}

static void oom_kill_process(struct oom_control *oc, const char *message)
{
	struct task_struct *victim = oc->chosen;
	struct mem_cgroup *oom_group;
	static DEFINE_RATELIMIT_STATE(oom_rs, DEFAULT_RATELIMIT_INTERVAL,
					      DEFAULT_RATELIMIT_BURST);

	/*
	 * If the task is already exiting, don't alarm the sysadmin or kill
	 * its children or threads, just give it access to memory reserves
	 * so it can die quickly
	 */
	task_lock(victim);
	if (task_will_free_mem(victim)) {
		mark_oom_victim(victim);
		wake_oom_reaper(victim);
		task_unlock(victim);
		put_task_struct(victim);
		return;
	}
	task_unlock(victim);

	if (__ratelimit(&oom_rs))
		dump_header(oc, victim);

	/*
	 * Do we need to kill the entire memory cgroup?
	 * Or even one of the ancestor memory cgroups?
	 * Check this out before killing the victim task.
	 */
	oom_group = mem_cgroup_get_oom_group(victim, oc->memcg);

	__oom_kill_process(victim, message);

	/*
	 * If necessary, kill all tasks in the selected memory cgroup.
	 */
	if (oom_group) {
		mem_cgroup_print_oom_group(oom_group);
		mem_cgroup_scan_tasks(oom_group, oom_kill_memcg_member,
				      (void*)message);
		mem_cgroup_put(oom_group);
	}
}

我们可以使用 bpftrace 程序来跟踪 oom_kill_process 内核函数,如下所示:

bpftrace -e 'kprobe:oom_kill_process { printf("%s called oom_kill_process\n", comm); }'

手动触发 OOM

方式一

/proc/sysrq-trigger 是一个特殊的接口,通过这个接口,可以直接发送信号给内核,触发一些特殊的操作。这个接口通常用于系统调试或者紧急恢复。/proc/sysrq-trigger 就是其中的一个接口,通过向这个文件写入特定的字符,可以触发内核执行一些特定的动作。

这个功能主要用于系统的调试或者救援,比如在系统死锁时强制重启系统,或者在内存耗尽时手动触发OOM Killer。执行 echo f > /proc/sysrq-trigger 时,向内核发送了一个f信号,这个信号的含义是“调用OOM Killer”。也就是说,这个操作会直接触发内核去执行OOM Killer,不需要等到内存真正不足。

最简单的测试触发OOM的方法,可以把某个进程的oom_adj设置到15(最大值),最容易触发。然后执行以下命令:

# echo f > /proc/sysrq-trigger   // 'f' - Will call oom_kill to kill a memory hog process.
root@instance-kncj9zve:~/tanjunchen/tracer/function/oom# pidof nginx
624728 624727 624726 624725 624724 624723 624722 624721 624719
root@instance-kncj9zve:~/tanjunchen/tracer/function/oom# echo 15 > /proc/624728/oom_score_adj
root@instance-kncj9zve:~/tanjunchen/tracer/function/oom# echo f > /proc/sysrq-trigger

方式二

用 C 写一个叫 bigmem 程序,分配大内存,主动触发 OOM。代码如下所示:

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

#define PAGE_SZ (1<<12)

int main() {
    int i;
    int gb = 36; //以GB为单位分配内存大小

    for (i = 0; i < ((unsigned long)gb<<30)/PAGE_SZ ; ++i) {
        void *m = malloc(PAGE_SZ);
        if (!m)
            break;
        memset(m, 0, 1);
    }
    printf("allocated %lu MB\n", ((unsigned long)i*PAGE_SZ)>>20);
    getchar();
    return 0;
}

使用如下方式进行编译与运行:

gcc -o bigmem bigmem.c
./bigmem

代码示例

通过上述方式主动触发 OOM,示例如下所示:

root@instance-kncj9zve:~/tanjunchen/tracer/function/oom# ./run.sh 
libbpf: loading object 'oom_bpf' from buffer
libbpf: elf: section(3) kprobe/oom_kill_process, size 264, link 0, flags 6, type=1
libbpf: sec 'kprobe/oom_kill_process': found program 'kprobe_oom_kill_process' at insn offset 0 (0 bytes), code size 33 insns (264 bytes)
libbpf: elf: section(4) .relkprobe/oom_kill_process, size 16, link 27, flags 40, type=9
libbpf: elf: section(5) license, size 13, link 0, flags 3, type=1
......

pid: 624728, comm: nginx
pid: 625669, comm: hosteye
pid: 625697, comm: hosteye
pid: 625774, comm: bigmem
root@instance-kncj9zve:~/tanjunchen/tracer/function/oom# echo f > /proc/sysrq-trigger 
root@instance-kncj9zve:~/tanjunchen/tracer/function/oom# echo f > /proc/sysrq-trigger 
root@instance-kncj9zve:~/tanjunchen/tracer/function/oom# ./bigmem 
Killed

系统自身查看 OOMEvent 事件,如下所示:

root@instance-kncj9zve:~/tanjunchen/tracer/function/oom# tail -f /var/log/syslog
Jan 18 20:08:17 instance-kncj9zve kernel: [789677.638843] [ 625306]     0 625306     4291     1348    69632        0             0 sshd
Jan 18 20:08:17 instance-kncj9zve kernel: [789677.638844] [ 625366]     0 625366     3075     1100    57344        0             0 bash
Jan 18 20:08:17 instance-kncj9zve kernel: [789677.638846] [ 625724]     0 625724    47501     9206   430080        0             0 hosteye
Jan 18 20:08:17 instance-kncj9zve kernel: [789677.638847] [ 625747]     0 625747     2507      628    53248        0             0 cron
Jan 18 20:08:17 instance-kncj9zve kernel: [789677.638848] [ 625774]     0 625774  8026776  8026366 64376832        0             0 bigmem
Jan 18 20:08:17 instance-kncj9zve kernel: [789677.638850] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=containerd.service,mems_allowed=0,global_oom,task_memcg=/user.slice/user-0.slice/session-3104.scope,task=bigmem,pid=625774,uid=0
Jan 18 20:08:17 instance-kncj9zve kernel: [789677.638861] Out of memory: Killed process 625774 (bigmem) total-vm:32107104kB, anon-rss:32104292kB, file-rss:1172kB, shmem-rss:0kB, UID:0 pgtables:62868kB oom_score_adj:0
Jan 18 20:08:17 instance-kncj9zve systemd[1]: session-3104.scope: A process of this unit has been killed by the OOM killer.
Jan 18 20:10:01 instance-kncj9zve CRON[625839]: (root) CMD (/opt/hosteye/bin/upgrade --upgrade_mode=8>/dev/null 2>&1)
Jan 18 20:15:01 instance-kncj9zve CRON[625981]: (root) CMD (/opt/hosteye/bin/upgrade --upgrade_mode=8>/dev/null 2>&1)
root@instance-kncj9zve:~/tanjunchen/tracer/function/oom# tail -f /var/log/kern.log
Jan 18 20:08:17 instance-kncj9zve kernel: [789677.638839] [ 625290]     0 625290     2659      601    53248        0             0 run.sh
Jan 18 20:08:17 instance-kncj9zve kernel: [789677.638840] [ 625304]     0 625304     9205      502   110592        0             0 oom
Jan 18 20:08:17 instance-kncj9zve kernel: [789677.638842] [ 625305]     0 625305     2233      184    45056        0             0 sleep
Jan 18 20:08:17 instance-kncj9zve kernel: [789677.638843] [ 625306]     0 625306     4291     1348    69632        0             0 sshd
Jan 18 20:08:17 instance-kncj9zve kernel: [789677.638844] [ 625366]     0 625366     3075     1100    57344        0             0 bash
Jan 18 20:08:17 instance-kncj9zve kernel: [789677.638846] [ 625724]     0 625724    47501     9206   430080        0             0 hosteye
Jan 18 20:08:17 instance-kncj9zve kernel: [789677.638847] [ 625747]     0 625747     2507      628    53248        0             0 cron
Jan 18 20:08:17 instance-kncj9zve kernel: [789677.638848] [ 625774]     0 625774  8026776  8026366 64376832        0             0 bigmem
Jan 18 20:08:17 instance-kncj9zve kernel: [789677.638850] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=containerd.service,mems_allowed=0,global_oom,task_memcg=/user.slice/user-0.slice/session-3104.scope,task=bigmem,pid=625774,uid=0
Jan 18 20:08:17 instance-kncj9zve kernel: [789677.638861] Out of memory: Killed process 625774 (bigmem) total-vm:32107104kB, anon-rss:32104292kB, file-rss:1172kB, shmem-rss:0kB, UID:0 pgtables:62868kB oom_score_adj:0

参考

  1. https://blog.51cto.com/laoxu/1267097
  2. https://www.cnblogs.com/MrLiuZF/p/15229868.html