转载

Linux内核分析: OOM杀掉nginx后导致的系统hang问题

概述

KVM虚拟机在遇到OOM后然后就hang住了,长时间无反应,不得不重启进行恢复。这篇文档总结了hang住的原因以及背后的机制,供以后遇到类似问题做参考。主要是涉及到OOM的机制,进程组,以及nginx的进程模型。

问题现象

系统在内存不足的情况下,就无法通过ssh登陆进去这是可以理解的;无法理解的是,内存不足时,杀掉进程就可以释放出来一部分可用内存,从而让系统能够继续运行下去,可是这个时候为什么会一直hang在这里呢? 这是内核的bug还是什么原因?

系统日志

由于系统长时间hang在那里,而且事先也没有打开hungtask,softlockup之类的检测,所以不得不去重启恢复,现场也无法保留了。

这也给了我们一个教训:现有的Linux调试手段,如果不影响性能,我们还是要部署到服务器上。

在系统重启后,去查看系统日志/var/log/messages, 发现了在重启前的一段时间内核一直在打印如下一段信息:

Jan 18 09:50:59 kernel: [ 8202] 0 8202 27050 55 3 0 0 bash

Jan 18 09:50:59 kernel: [ 8234] 0 8234 27600 455 3 0 0 rsync

Jan 18 09:50:59 kernel: [ 8253] 0 8253 27542 474 0 0 0 rsync

Jan 18 09:50:59 kernel: [ 8273] 2156 8273 25227 18 1 0 0 sleep

Jan 18 09:50:59 kernel: [ 8304] 600 8304 25227 19 1 0 0 sleep

Jan 18 09:50:59 kernel: [ 8353] 2188 8353 29284 4840 0 0 0 nginx

Jan 18 09:50:59 kernel: [ 8354] 2188 8354 29284 5714 2 0 0 nginx

Jan 18 09:50:59 kernel: [ 8356] 2188 8356 29284 11348 1 0 0 nginx

Jan 18 09:50:59 kernel: [ 8357] 2188 8357 18681 1240 2 0 0 nginx

Jan 18 09:50:59 kernel: Out of memory: Kill process 8356 (nginx) score 5 or sacrifice child

Jan 18 09:50:59 kernel: Killed process 8356, UID 2188, (nginx) total-vm:117136kB, anon-rss:45388kB, file-rss:4kB

Jan 18 09:50:59 kernel: Out of memory: Kill process 8353 (nginx) score 5 or sacrifice child

Jan 18 09:50:59 kernel: Killed process 8353, UID 2188, (nginx) total-vm:117136kB, anon-rss:41224kB, file-rss:4kB

Jan 18 09:50:59 kernel: Out of memory: Kill process 8354 (nginx) score 6 or sacrifice child

Jan 18 09:50:59 kernel: Killed process 8354, UID 2188, (nginx) total-vm:121304kB, anon-rss:49488kB, file-rss:8kB

Jan 18 09:50:59 kernel: Out of memory: Kill process 8359 (nginx) score 5 or sacrifice child

Jan 18 09:50:59 kernel: Killed process 8359, UID 2188, (nginx) total-vm:117136kB, anon-rss:45932kB, file-rss:8kB

Jan 18 09:50:59 kernel: Out of memory: Kill process 8358 (nginx) score 6 or sacrifice child

Jan 18 09:50:59 kernel: Killed process 8358, UID 2188, (nginx) total-vm:121456kB, anon-rss:49608kB, file-rss:52kB

Jan 18 09:50:59 kernel: Out of memory: Kill process 8357 (nginx) score 6 or sacrifice child

Jan 18 09:50:59 kernel: Killed process 8357, UID 2188, (nginx) total-vm:121304kB, anon-rss:49488kB, file-rss:40kB

Jan 18 09:51:00 kernel: Out of memory: Kill process 8363 (nginx) score 5 or sacrifice child

Jan 18 09:51:00 kernel: Killed process 8363, UID 2188, (nginx) total-vm:117136kB, anon-rss:47364kB, file-rss:40kB

Jan 18 09:51:00 kernel: Out of memory: Kill process 8361 (nginx) score 6 or sacrifice child

Jan 18 09:51:00 kernel: Killed process 8361, UID 2188, (nginx) total-vm:121304kB, anon-rss:49052kB, file-rss:48kB

Jan 18 09:51:00 kernel: Out of memory: Kill process 8360 (nginx) score 5 or sacrifice child

Jan 18 09:51:00 kernel: Killed process 8360, UID 2188, (nginx) total-vm:117136kB, anon-rss:47364kB, file-rss:4kB

Jan 18 09:51:00 kernel: Out of memory: Kill process 8364 (nginx) score 6 or sacrifice child

Jan 18 09:51:00 guomai131030 kernel: Killed process 8364, UID 2188, (nginx) total-vm:121304kB, anon-rss:49484kB, file-rss:12kB

Jan 18 09:51:00 kernel: Out of memory: Kill process 8369 (nginx) score 6 or sacrifice child

Jan 18 09:51:00 kernel: Killed process 8369, UID 2188, (nginx) total-vm:121304kB, anon-rss:49488kB, file-rss:12kB

Jan 18 09:51:00 kernel: Out of memory: Kill process 8368 (nginx) score 5 or sacrifice child

Jan 18 09:51:00 kernel: Killed process 8368, UID 2188, (nginx) total-vm:117136kB, anon-rss:43268kB, file-rss:4kB

Jan 18 09:51:00 kernel: Out of memory: Kill process 8377 (nginx) score 6 or sacrifice child

Jan 18 09:51:00 kernel: Killed process 8377, UID 2188, (nginx) total-vm:121304kB, anon-rss:49484kB, file-rss:8kB

Jan 18 09:51:00 kernel: Out of memory: Kill process 8367 (nginx) score 5 or sacrifice child

Jan 18 09:51:00 kernel: Killed process 8367, UID 2188, (nginx) total-vm:117136kB, anon-rss:47364kB, file-rss:4kB

Jan 18 09:51:00 kernel: Out of memory: Kill process 8379 (nginx) score 6 or sacrifice child

Jan 18 09:51:00 kernel: Killed process 8379, UID 2188, (nginx) total-vm:121308kB, anon-rss:49492kB, file-rss:64kB

Jan 18 09:51:00 kernel: Out of memory: Kill process 8380 (nginx) score 5 or sacrifice child

Jan 18 09:51:00 kernel: Killed process 8380, UID 2188, (nginx) total-vm:117136kB, anon-rss:47364kB, file-rss:4kB

Jan 18 09:51:00 kernel: Out of memory: Kill process 8378 (nginx) score 6 or sacrifice child

Jan 18 09:51:00 kernel: Killed process 8378, UID 2188, (nginx) total-vm:121304kB, anon-rss:49484kB, file-rss:8kB

Jan 18 09:51:00 kernel: Out of memory: Kill process 8382 (nginx) score 6 or sacrifice child

Jan 18 09:51:00 kernel: Killed process 8382, UID 2188, (nginx) total-vm:121304kB, anon-rss:49484kB, file-rss:16kB

Jan 18 09:51:00 kernel: Out of memory: Kill process 8381 (nginx) score 5 or sacrifice child

Jan 18 09:51:00 kernel: Killed process 8381, UID 2188, (nginx) total-vm:117136kB, anon-rss:46808kB, file-rss:8kB

Jan 18 09:51:00 kernel: Out of memory: Kill process 8384 (nginx) score 5 or sacrifice child

...

看起来似乎是系统有杀不完的nginx进程(或者线程)。PS:内核是不区分线程与进程的,线程在内核看来也是一个进程。接着来看下,在开始杀nginx进程时,系统的进程信息,我们只看nginx进程即可:

Jan 18 09:50:59 kernel: [ pid ] uid tgid total_vm rss cpu oom_adj oom_score_adj name

Jan 18 09:50:59 kernel: [29640] 0 29640 12621 178 2 0 0 nginx

Jan 18 09:50:59 kernel: [29641] 0 29641 13282 318 1 0 0 nginx

Jan 18 09:50:59 kernel: [29643] 0 29643 13289 321 0 0 0 nginx

Jan 18 09:50:59 kernel: [29644] 0 29644 13288 323 2 0 0 nginx

Jan 18 09:50:59 kernel: [29645] 0 29645 13283 319 3 0 0 nginx

...

Jan 18 09:50:59 kernel: [30747] 2188 30747 18681 1242 0 0 0 nginx

...

Jan 18 09:50:59 kernel: [ 8353] 2188 8353 29284 4840 0 0 0 nginx

Jan 18 09:50:59 kernel: [ 8354] 2188 8354 29284 5714 2 0 0 nginx

Jan 18 09:50:59 kernel: [ 8356] 2188 8356 29284 11348 1 0 0 nginx

Jan 18 09:50:59 kernel: [ 8357] 2188 8357 18681 1240 2 0 0 nginx

可以看到,有两组nginx进程(或线程),分别是uid=0(即root)和uid=2188(即普通用户)创建的nginx。

然后oom killer杀的进程pid都是83XX,是属于uid=2188用户创建的,其中8353/8354/8356/8357是系统已存在的进程,而oom killer后面杀的进程,比如pid为8359/8363等都是系统先前不存在的进程。

我们接着来分析下这到底是怎么一回事。

底层机制:线程组,线程与进程

在Linux 2.6的内核开始,引入了一个线程组(thread group)的概念,它是在task_struct结构体中增加了一个tgid(thread group id)字段,

struct task_struct {     ...     pid_t tgid;     ... } 

如果这个task是一个“主线程”(即thread group leader),则它的tgid等于pid,若是子线程则tgid等于进程的pid(即主线程的pid)。在clone系统调用中, 传递CLONE_THREAD参数就可以把新进程的tgid设置为父进程的tgid(否则新进程的tgid会设为其自身的pid)。

有了tgid,就可以区分某个tast_struct是代表一个进程还是代表一个线程(tgid不等于pid就是线程)了。与此有关的一些系统调用: getpid(2)系统调用返回的就是tast_struct中的tgid,而tast_struct中的pid则由gettid(2)系统调用来返回。

前面的系统日志显示,nginx的pid与tgid相等,说明这些nginx都是进程,而不是线程。即他们都是单线程的。

##底层机制:UID(用户ID)

内核用UID来标示用户,root用户的uid是0,非root用户的uid为非零。

通过前面的日志信息,我们可以发现,这10个nginx进程属于两个用户,一个是uid=0,即root;另一个是uid=2188,即普通用户。

我们还可以发现,oom killer杀掉的只是普通用户创建的nginx,而不会杀root用户创建的nginx进程。这是怎么回事呢?

底层机制:oom killer与root

我们需要来看下oom选择杀进程的策略:

unsigned int oom_badness(...) {     ...         /*        * Root processes get 3% bonus, just like the __vm_enough_memory()      * implementation used by LSMs.      */     if (has_capability_noaudit(p, CAP_SYS_ADMIN))         points -= 30;      ... } 

root用户创建的进程,相比非root用户,得分会低一些,所以oom killer会优先杀非root用户创建的进程。

然后我们接着分析,为什么uid=2188的nginx进程明明只有5个,可是oom killer却啥不完呢?为什么会有新的nginx进程产生呢?

这跟nginx的进程模型有关。

应用程序的机制:nginx的进程模型

我们服务器上nginx的进程模型如下:

Linux内核分析: OOM杀掉nginx后导致的系统hang问题

它工作在master-worker模式,由一个master进程fork出来N(N的数目是在nginx.conf里面配置的,我们的系统配置的是4)个worker进程,master进程会周期性的检查worker进程,如果不足N个,它就会重新fork,直至数目达到N为止。

于是,这就解释了,为什么会不断的有新的nginx进程产生了,它是由master进程fork出来的。

不过有一点点遗憾的是,由于系统日志里面没有纪录ppid(parent PID)这一项,所以不知道8353/8354/8356/8357这四个进程的父进程,不过根据理论分析,它应该是PID 30747(因为还只有这一个nginx进程属于uid=2188),即30747进程是master进程。

分析到这里,就可以有一个合理的推测了:在内存不足的时候,由于uid=2188的nginx worker的score较高,所以oom killer就杀掉nginx worker进程以便于释放出足够的内存空间,于此同时,nginx master进程发现worker进程不足4了,然后就fork出来新的worker进程。 于是就这样,oom killer杀worker进程,nginx master进程fork出来新的worker进程,就这样形成了死循环了

##底层机制:oom killer为什么死循环了?

如下这部分代码印证了我们的合理推测:

restart:     page = get_page_from_freelist();     // 如果还是没办法申请到内存,那就去杀进程       if (page)          goto got_pg;          // 在发生oom杀进程的情况下,返回值page为NULL      page = __alloc_pages_may_oom();      // page is NULL now.         if (page)         goto got_pg;              // 既然杀掉了进程,那就可以重新再去尝试申请内存了。         goto restart; 

大致的意思就是,进程在申请内存的时候,如果申请不到足够的内存,就会触发oom选择一个最惨的进程给杀掉它以便于释放出来一部分内存,然后在杀掉这个进程后,就尝试继续申请内存,如果还不够,就继续选择一个进程杀,即在这里循环,直到申请出来足够的内存,或者当前把当前这个进程给杀掉,或者实在没有进程给杀了而panic。

底层机制: 为什么需要oom killer?

Linux内存管理模块有一个overcommit机制,意思是说,进程申请的内存可以大于当前系统free的内存,这可以通过/proc/sys/vm/overcommit_memory这个proc接口来配置。

它的默认值是0,即启发式策略,尽量减少swap的使用,root可以分配比一般用户略多的内存;

为1表示总是会允许overcommit,这一般适用于科学计算程序;

为2则表示不允许overcommit,系统申请的内存不能超过CommitLimit,在这种情况下,是进程申请内存返回错误,而不是去杀死进程。

Linux之所以这么设计,是出于这么一个考虑:进程申请的内存不会马上就被用到,并且,在进程的整个生命周期内,它也不会用到它申请的所有内存。如果没有overcommit,系统就不能够充分的利用它的内存,这样就会导致内存的浪费。overcommit就可以让系统更加高效的使用它的内存,但是与此同时也带来了一个风险:oom。memory-hogging程序能够耗尽整个系统的内存,从而导致整个系统处于halt的状态,在这种情况下,用户程序甚至连一个page的内存都无法申请,于是oom killer就出现了,它会识别出来可以为整个系统作出牺牲的进程,然后杀掉它,释放出来一些内存。

底层机制: 控制oom killer

oom killer可以杀死哪些进程,而不应该杀死哪些进程,这确实是个难题,所以kernel就导出了一些接口给用户,让用户来控制,于是就把这个难题抛给了用户。

这个接口就是/proc/ /oom_adj, 它的范围是-17~+15,值越高,就越容易被杀掉,如果把该值设置为-17,oom就永远也不会考虑杀它。

底层机制: oom杀进程的策略是怎么样的?

oom killer选择杀哪个进程,是基于它的badness score,该值体现在/proc/

/oom_score里面。它的原则是,尽可能少杀进程来尽可能释放出足够多的内存,同时不去杀那些耗费内存很多的无辜进程。badness score的计算会用到进程的内存大小,CPU时间(user time + system time), 运行时间,以及oom_adj值。进程消耗的内存越多,得分就越高;进程运行的时间越长,得分就越低。

这也解释了,为什么新fork出来的进程容易被杀死,因为它的运行时间短,得分高

oom killer选择victim进程的策略大致如下:

  1. 它必须拥有大量的页框
  2. 杀掉这个进程只会损失少量的工作
  3. 它的静态优先级必须低(可以通过nice来给不重要的进程设置低的优先级)
  4. 它不能够拥有root权限
  5. 它不能直接访问硬件
  6. 它不能够是0号进程(swapper),1号进程(init),以及内核线程

分析清楚了oom的机制后,我们就可以采取合理的手段来解决该问题了。

我们这个问题的解决方法

由于罪魁祸首在于master进程不停的fork出来子进程,所以我们可以采取的手段是先杀掉master,再杀掉worker。master被杀掉后,流量将不会再被导入进来,这也强于流量被导入进来了但是却没有办法处理。这样业务将不会受到影响。

社区对于OOM的一些讨论

oom这一块总来的来看还是有很多缺陷,不然也不会导致这种问题发生。

社区里面对oom的处理也提出了一些很好的建议,比如增加一个oom cgroup,这样痛过cgroup来控制oom。具体参考 Taming the OOM killer

总结与思考

这个问题给我们的一个教训是,对于可用的Linux定位手段,在不影响系统性能和稳定性的前提下,我们还是都需要部署到服务器上去。

PS:以上讨论,基于的OS版本是CentOS-6(Kernel 2.6.32),文档中的内核代码均摘自CentOS-6的kernel-2.6.32。

Contact me: yafang@mogujie.com

原文  http://mogu.io/159-159
正文到此结束
Loading...