专栏首页linux&pythonOOM Killer的一点分析
原创

OOM Killer的一点分析

最近线上遇到了好几次由于内存泄漏导致OOM的问题,且大部分都是整个模块被kill掉woker进程,只剩下接入的epoll进程和统计进程的情况,从而导致拨测程序在没有做逻辑拨测的情况下,不会重新拉起程序,导致机器无法服务。

我们的svr的worker进程都有一个用于守护的父进程,在worker子进程挂掉或者运行正常退出之后,会由父进程重新拉起

考虑到线上的内存泄漏都是很缓慢不容易发现的,因此我们希望能够让父进程在OOM的情况下不被os干掉,而是让os kill掉泄漏的子进程,然后由父进程重新拉起子进程,从而让模块可以继续服务。

同时对于在OOM的时候,都是worker进程被kill掉,而epoll进程存活的情况也存有疑问,因此研究了一下OOM Killer的相关机制,这里简单总结一下。

首先说明一下在OOM的时候哪些进程会优先被os干掉。

直观的感觉应该是操作系统会选择内存占用最多的进程将其kill掉,这个大概是对的,但是受很多其他因素的影响。

由于现网出问题的机器的内核版本为2.6.16,所以这里是根据2.6.16的oom killer源码做一下简单分析,具体的源文件为mm/oom_kill.c

需要说明的是,不同版本的内核的oom killer的实现机制有所不同。

在内存爆掉的时候,内核会调用到out_of_memory这个函数,简化之后的代码如下:

void out_of_memory(struct zonelist *zonelist, gfp_t gfp_mask, int order)

{

       struct mm_struct *mm = NULL;

       task_t *p;

       unsigned long points = 0;

       ........

              /*

               * Rambo mode: Shoot down a process and hope it solves whatever

               * issues we may have.

               */

              p = select_bad_process(&points);

              if (PTR_ERR(p) == -1UL)

                     goto out;

              /* Found nothing?!?! Either we hang forever, or we panic. */

              if (!p) {

                     read_unlock(&tasklist_lock);

                     cpuset_unlock();

                     panic("Out of memory and no killable processes...\n");

              }

              mm = oom_kill_process(p, points, "Out of memory");

              if (!mm)

                     goto retry;

              break;

}

基本上这个函数就是调用select_bad_process选择一个进程,然后调用oom_kill_process将其kill掉

而select_bad_process中则是遍历所有进程,逐个调用badness函数,得到一个point,最终返回point最大的进程

但这里有一点需要提及

              /* skip the init task with pid == 1 */

              if (p->pid == 1)

                     continue;

              if (p->oomkilladj == OOM_DISABLE)

                     continue;

可以看到,init进程是会被忽略的,同时,如果进程的oomkilladj == OOM_DISABLE的话,则这个进程也会被跳过

那oomkilladj是什么呢?其实就是每个进程的oom权重

对于2.6.16来说,可以通过设置每个进程在/proc下对应节点的oom_adj节点来调整这个权重

oom_adj的取值范围为[-17, 15],默认值为0,值越大,badness里面计算的时候得到的point就会越大

而如果oom_adj被设置为 -17 的话,则会满足上面的oomkilladj == OOM_DISABLE这个条件,从而使得OOM Killer跳过这个进程。

也就是说,如果我们希望OOM Killer不要干掉某个进程的话,最简单的做法是设置这个进程的oom_adj,或者是进程在启动的时候,自己将/proc/self/oom_adj设置为-17

但是这种做法有个比较明显的缺点,对oom_adj的设置需要root权限。

另外需要提及的是,从Linux 2.6.36开始oom_adj被替换为oom_score_adj。

对oom_adj进行的设置,在内核内部进行变换后的值也是针对oom_score_adj设置的。

oom_score_adj可以设置[-1000, 1000]之间的值。设置为-1000时,等同于上面提及的oom_adj设置为-17的情况。

另外/proc/<pid>/下还有一个oom_score节点,这个节点保存的即是当前进程的point值,值越大被OOM Killer选中的几率越大。

badness函数的主要功能如上所述,即根据各种条件计算进程的point值。

point的初始值为进程占用的内存大小。

       /*

        * The memory size of the process is the basis for the badness.

        */

       points = p->mm->total_vm;

接着,会遍历当前进程的所有子进程,将与父进程不共享内存的子进程占用的一半内存大小加到父进程的point里面

       /*

        * Processes which fork a lot of child processes are likely

        * a good choice. We add half the vmsize of the children if they

        * have an own mm. This prevents forking servers to flood the

        * machine with an endless amount of children. In case a single

        * child is eating the vast majority of memory, adding only half

        * to the parents will make the child our kill candidate of choice.

        */

       list_for_each(tsk, &p->children) {

              struct task_struct *chld;

              chld = list_entry(tsk, struct task_struct, sibling);

              if (chld->mm != p->mm && chld->mm)

                     points += chld->mm->total_vm/2 + 1;

       }

也就是说,如果一个进程fork了一堆子进程,每个子进程又分配了大量内存,则即使这个父进程本身没有分配什么内存,父进程的point值还是可能大于子进程。

从而导致父进程被OOM Killer选中。

举个例子,以下是两个进程的oom_score值,其中 4865 是 25266 的守护父进程,可以看到,父进程的omm_score明显大于子进程的oom_score。

接着代码会计算进程占用的cpu时间和运行时间,并point除与这两个时间的开平方值,也就是说,如果进程占用的cpu时间、存活时间越长,其point值会越小。

同时还会考虑进程的nice值,如果nice值大于0,则将point×2,这个基于被设置了nice值的进程重要性低这一点。

对于超级用户的进程或者直接对硬件进行操作的进程,其point值会被除与4,以减少被选中的可能性。

最后,会根据oom_adj的值进行修正。

       /*

        * Adjust the score by oomkilladj.

        */

       if (p->oomkilladj) {

              if (p->oomkilladj > 0)

                     points <<= p->oomkilladj;

              else

                     points >>= -(p->oomkilladj);

       }

至此,一个进程的point值已经被计算完毕,select_bad_process最终会返回一个point值最大的进程,然后在oom_kill_process中kill掉这个进程。

这里需要提及的是,如果这个进程有子进程,则会优先kill掉其一个不与父进程共享内存的子进程。

       /* Try to kill a child first */

       list_for_each(tsk, &p->children) {

              c = list_entry(tsk, struct task_struct, sibling);

              if (c->mm == p->mm)

                     continue;

              mm = oom_kill_task(c, message);

              if (mm)

                     return mm;

       }

只要kill掉一个子进程,则这个函数就会返回,于是父进程可能不会被kill掉。

这也是在OOM的时候,我们可能会在dmesg中看到类似如下的日志的原因

[1911080.461584] Out of Memory: Kill process 23009 (qspoint) score 1660489 and children.

[1911080.461642] Out of memory: Killed process 23012 (qspoint).

即dmesg显示要kill某个进程(上例中为23009)和其子进程,最终却只kill了另外一个进程(23012)的原因。

对OOM Killer的分析大概如上,回到一开始提到的问题上来,如何避免worker的父进程不被OOM Killer干掉呢?

经过以上的分析之后,可以知道一种解决方案是设置父进程的oom_adj为-17。

经过测试,这种方案是可以解决问题。

不过最终我们并没有采用这种方式,原因在于这个操作需要root权限,但是我们并不想用root来起这个模块,当然也可以通过脚本在外部设置或者在程序中切换用户,但是最终我们还是选择了通过在epoll进程中起一个线程定期检查worker父进程是否存活,发现不存活就kill掉整个进程组的方式来解决这个问题。

检查进程是否存活的方式也很简单,直接kill(pid, 0),如果返回-1,则可以认为进程已经不存在,具体请man kill。

对于另一个问题,为什么epoll进程没有被kill掉,而总是worker进程被kill掉,从上面的分析也可以得到大概的解释,对于worker子进程来说,被kill掉是因为本身有内存泄漏,确实占用了大量内存导致,而对于worker的守护进程来说,则是由于其fork了worker子进程,导致在计算point的时候,子进程的一半内存大小被计算到守护进程的point中,使得守护进程在本身没有泄漏和占用大量内存的情况下,也仍然被OOM Killer选中。

不过这里还有一点存疑,按照上面的分析,即使是在选中父进程的情况下,只要能够kill掉一个子进程,则OOM Killer就会退出,简单的测试程序测试的结果也的确如此,那为什么现网会出现父进程也被kill掉的情况呢?

OOM Killer在kill掉某个进程的时候,是通过发送SIGKILL信号的方式来实现的,如下:

       force_sig(SIGKILL, p);

这里一种可能性是,线上内存爆掉的时候情况远比测试环境复杂,虽然OOM Killer发送了信号给子进程,但并不能立刻kill掉子进程,从而使得OOM Killer多次被触发,最终把父进程也kill掉,而我们的worker子进程是有运行次数限制的,即处理的请求数达到一定程度之后就会退出,然后由父进程重新拉起,而由于父进程已经被kill掉,最终导致所有worker全部挂掉。

参考资料:

http://lwn.net/Articles/317814/

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 详解Linux中的守护进程

    Linux系统启动时会启动很多系统服务进程,这些系统服 务进程没有控制终端,不能直接和用户交互。其它进程都是在用户登录或运行程序时创建,在运⾏结束或⽤户注销时终...

    砸漏
  • 如何在nodejs中实现兄弟进程通信

    在nodejs主进程中,开启一个额外的子进程A,进程A负责和线程池通信,完成cpu密集型的任务。通过nodejs主进程创建出来的多个nodejs工作进程可以把任...

    theanarkh
  • Linux 阻碍国产操作系统进程?

    简单来讲,进程就是运行中的程序。更进一步,在用户空间中,进程是加载器根据程序头提供的信息将程序加载到内存并运行的实体。

    CSDN技术头条
  • Python学习,多进程了解一下!学爬虫不会用多进程能行吗?

    首先我们先做一个小脚本,就用turtle画4个同心圆吧!这样在演示多进程的时候比较直观。代码如下:

    云飞
  • 深度好文|面试官:进程和线程,我只问这19个问题

    标准定义:进程是一个具有一定独立功能的程序在一个数据集合上依次动态执行的过程。进程是一个正在执行程序的实例,包括程序计数器、寄存器和程序变量的当前值。

    cxuan
  • 深度好文|面试官:进程和线程,我只问这19个问题

    标准定义:进程是一个具有一定独立功能的程序在一个数据集合上依次动态执行的过程。进程是一个正在执行程序的实例,包括程序计数器、寄存器和程序变量的当前值。

    C语言与CPP编程
  • 进程组、会话、控制终端概念,如何创建守护进程?

    守护进程,也就是通常所说的Daemon进程,是Linux中的后台服务进程。周期性的执行某种任务或等待处理某些发生的事件。

    睡魔的谎言
  • 操作系统复习笔记——第三章 进程

    进程可看做是正在执行的程序。进程需要一定的资源(如CPU时间、内存、文件和I/O设备)来完成其任务。这些资源在创建进程或执行进程时被分配。

    种花家的奋斗兔
  • Android内存管理(六)Android对Linux系统的内存管理机制进行的优化

    Android对内存的使用方式同样是“尽最大限度的使用”,这一点继承了Linux的优点。只不过有所不同的是,Linux侧重于尽可能多的缓存磁盘数据以降低磁盘IO...

    Anymarvel
  • LNMP架构下的进程模型分析

    如果已经在LNMP架构下工作2-3年时间,这个阶段我们对自己常用的技术栈的工作原理一定需要有一个基本的认识。一方面,可以去学习这些优秀软件的设计思路,另一方面,...

    用户1093396

扫码关注云+社区

领取腾讯云代金券