记一次物理机 OOM 的处理过程

1. 情景描述

1.1 技术背景

在之前开发的一个作业调度系统中,采用了一种最简单作业运行机制:

  • 物理机上启动称为 worker 的进程。

  • worker 进程与其他模块交互启动任务、停止任务、保存日志。

  • worker 运行作业的命令由调度模块生成派发。

  • 作业进程以 worker 进程的子进程的方式启动。

这种架构的优点是轻量级,任务的具体运行策略在运行时由调度中心决定,因此大量的特性可以在调度模块进行开发,worker 模块不需要频繁重启和更新。

但是缺点也同样明显,主要包括两个方面。

首先,因为物理机上没有做资源的隔离,所以所有作业共享物理机的资源,单个任务可能无限制地使用资源导致 worker 进程或者物理机的不稳定。

其次,作业都是由 worker 进程创建子进程的方式启动。一旦 worker 进程退出,原来作业进程的 ppid 都会变成 1。如果不对作业进程的信息进行持久化,那么重启之前的在运行的作业状态就会丢失,但是贸然重启又会导致同一个作业有两个实例同时运行。但即使持久化了作业进程的信息,在 worker 进程重启过程中运行结束的作业信息还是会丢失。

鉴于上述原因,针对当前的架构最重要的就是保证 worker 进程不会被意外终止。

1.2 问题描述

但是,就像墨菲定律所说:

如果事情有变坏的可能,不管这种可能性有多小,它总会发生。

最近频繁在半夜被报警电话打醒,worker 进程被 supervisor 重新拉起。只能先来一波令人窒息的运维操作,恢复这台物理机的状态,然后再排查问题的原因。

查看监控页面可知物理机的内存使用率和 CPU load 在短时间内一路走高,操作系统达到了 OOM 的边缘。

之前还发生过操作系统在类似的情况下直接重启的后果。

2. 排查手段

咨询了一下公司负责 SLA 的同事,初步判断时内核的 OOM-killer 将进程杀死导致了 worker 进程的重启。

2.1 OOM 的日志

首先 OOM-killer 发生作用时在内核的日志里是会有记录的:

  • /var/log/kern.log

  • /var/log/dmesg

    查找 Out of memory: Kill process 或者 Killed process 字样。

     sudo grep --color -n -B 30 'Killed process' /var/log/kern.log

    日志中会提示 OOM-killer 杀死进程前的内存使用信息、终止进程的 pid 以及 cmd 等。由于时间比较久远,相关日志被清除了,因此就不再这里贴出样例了。

2.2 OOM-killer 机制

根据下面几篇文章,可以简单了解一下 OOM-killer 机制的作用原理:

简单来说几个相关参数:

  • /proc/sys/vm/panic_on_oom

  • /proc/sys/vm/oom_dump_tasks

  • /proc/sys/vm/oom_kill_allocating_task

  • /proc/sys/vm/overcommit_memory

  • /proc/sys/vm/overcommit_ratio

  • /proc/[pid]/oom_score

  • /proc/[pid]/oom_adj

2.2.1 /proc/sys/vm/panic_on_oom

默认值是 0。

如果设置为 0,内核会杀死内存占用过多的进程。通常杀死内存占用最多的进程,系统就会恢复。

如果设置为 1,在发生 OOM 时,内核会 panic 。然而,如果一个进程通过内存策略或进程绑定限制了可以使用的节点,并且这些节点的内存已经耗尽,OOM-killer 可能会杀死一个进程来释放内存。在这种情况下,内核不会 panic,因为其他节点的内存可能还有空闲,这意味着整个系统的内存状况还没有处于崩溃状态。

如果设置为 2,在发生 OOM 时总是会强制 panic,即使在上面讨论的情况下也一样。即使在 memory cgroup 限制下发生的 OOM,整个系统也会 panic。

2.2.2 /proc/sys/vm/oom_dump_tasks

默认值是 1(启用),在内核执行OOM-killing时会打印系统内进程的信息(不包括内核线程),信息包括 pid、uid、tgid、vm size、rss、nr_ptes,swapents,oom_score_adj 和进程名称。

2.2.3 /proc/sys/vm/oom_kill_allocating_task

默认值是 0。如果设置为 0,OOM killer 会扫描进程列表,选择一个进程来杀死。通常都会选择消耗内存内存最多的进程,杀死这样的进程后可以释放大量的内存。如果设置为非零值,OOM killer 只会简单地将触发 OOM 的进程杀死,避免遍历进程列表(代价比较大)。如果 panic_on_oom 被设置,则会忽略 oom_kill_allocating_task 的值。

2.2.4 /proc/sys/vm/overcommit_memory

默认值为 0 。该参数有三个值,分别是:

  • 0:当用户空间请求更多的的内存时,内核尝试估算出剩余可用的内存。

  • 1:当设这个参数值为 1 时,内核允许超量使用内存直到用完为止。

  • 2:当设这个参数值为 2 时,内核会使用一个决不过量使用内存的算法,即系统整个内存地址空间不能超过 swap + 50% 的 RAM 值,50% 参数的设定是在 overcommit_ratio 中设定。

2.2.5 /proc/sys/vm/overcommit_ratio

默认值为:50。这个参数值只有在 vm.overcommit_memory=2 的情况下,这个参数才会生效。该值为物理内存比率,当 overcommit_memory=2 时,进程可使用的 swap 空间不可超过 PM * overcommit_ratio / 100

2.2.6 /proc/[pid]/oom_score

每一个进程都会有的一个值,在 OOM-killer 选择终止进程的时候,会首先终止 oom_score 最高的进程。

2.2.7 /proc/[pid]/oom_adj

/proc/[pid]/oom_adj ,该 pid 进程被 OOM-killer 杀掉的权重,介于 [-17,15] 之间,越高的权重,意味着更可能被 OOM-killer 选中,-17 表示禁止被 kill 掉。

oom_score_adj 的作用和 oom_adj 作用类似,如果设置为 -1000 则表示禁止被 kill 掉。

3. 解决方案

在当前场景下,其实可以接受单个作业被内核杀死而保全 worker 进程。因此要做的很简单,把 worker 进程的 oom_adj 调整为 -17 即可。

 sudo su - root
 echo -17 > /proc/[pid]/oom_adj

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注