背景

服务器的某个模块的进程莫名其妙的不见了,查看MQ的连接情况,该模块的连接数直接为0,但是其他模块的进程还存在。很是纳闷该模块的进程为什么进程突然不见了。

oom-killer-mq-consumer-connnection

查看进程的log文件,没有发现有stop信号收到。平时停止会收到kill信号。

[12:29:37.290]:[executor.py:_fun: 53]: Info: proc[28269] recv signal[15]
[12:29:37.314]:[mq.py:_consuming:181]: service received STOP signal, stop consume

一下子没有反应过来会被kill 9。想到平时大家都会去/var/log/message查看,打开文件一看:

Dec  2 10:10:00 localhost kernel: [53533657.927030] python invoked oom-killer: gfp_mask=0x200da, order=0, oom_adj=0
Dec  2 10:10:00 localhost kernel: [53533657.927033] Pid: 19679, comm: python Not tainted  #1
Dec  2 10:10:00 localhost kernel: [53533657.927035] Call Trace:
Dec  2 10:10:00 localhost kernel: [53533657.927041]  [<ffffffff81089f9a>] oom_kill_process.clone.0+0xaa/0x270
Dec  2 10:10:00 localhost kernel: [53533657.927043]  [<ffffffff8108a278>] __out_of_memory+0x118/0x180
Dec  2 10:10:00 localhost kernel: [53533657.927045]  [<ffffffff8108a3ba>] out_of_memory+0xda/0x160
Dec  2 10:10:00 localhost kernel: [53533657.927048]  [<ffffffff8108e9cd>] __alloc_pages_nodemask+0x61d/0x630
Dec  2 10:10:00 localhost kernel: [53533657.927052]  [<ffffffff810b8ce3>] alloc_page_vma+0x93/0x150
Dec  2 10:10:00 localhost kernel: [53533657.927054]  [<ffffffff810af10e>] read_swap_cache_async+0xde/0x130
Dec  2 10:10:00 localhost kernel: [53533657.927061]  [<ffffffff810af868>] ? valid_swaphandles+0x68/0x160
Dec  2 10:10:00 localhost kernel: [53533657.927063]  [<ffffffff810af1df>] swapin_readahead+0x7f/0xb0
Dec  2 10:10:00 localhost kernel: [53533657.927067]  [<ffffffff810a09e2>] handle_mm_fault+0x392/0x9d0
Dec  2 10:10:00 localhost kernel: [53533657.927071]  [<ffffffff8102eaa0>] do_page_fault+0x110/0x2e0
Dec  2 10:10:00 localhost kernel: [53533657.927076]  [<ffffffff817de855>] page_fault+0x25/0x30
...
Dec  2 10:10:00 localhost kernel: [53533657.964521] Out of memory: kill process 21270 (python) score 1508 or a child
Dec  2 10:10:00 localhost kernel: [53533657.964523] Killed process 21360 (python)

原来是OOM,给杀死进程了。以为OOM会随便杀进程,之前也出现过一次,也是同一模块的进程都被杀了。于是上网搜索了一次OOM挑选进程的算法。

Linux 如何选择要kill掉的进程

从网上的找了一个比较全面的如下:

OOM Killer在内存耗尽时,会查看所有进程,并分别为每个进程计算分数。将信号发送给分数最高的进程。 
计算分数的方法 
在OOM Killer计算分数时要考虑很多方面。首先要针对每个进程确认下列1~9个事项再计算分数。 
1. 首先,计算分数时是以进程的虚拟内存大小为基准的,虚拟内存大小可以使用ps命令的VSZ或/proc/<PID>/status的 VmSize来确认。对于正在消耗虚拟内存的进程,其最初的得分较高,单位是将1KB作为1个得分,消耗1GB内存的进程,得分约为1024*1024。 
2. 如果进程正在执行swapoff系统调用,则得分设置为最大值(unsigned long的最大值)。这是因为禁用swap的行为与消除内存不足是相反的,会立刻将其作为OOM Killer的对象进程。 
3. 如果是母进程,则将所有子进程内存大小的一半作为分数。 
4. 根据进程的CPU使用时间和进程启动时间调整得分,这是因为在这里认为越是长时间运行或从事越多工作的进程越重要,需保持得分较低。 
5. 对于通过nice命令等将优先级设置得较低的进程,要将得分翻倍。nice-n中设置为1~19的命令的得分翻倍。 
6. 特权进程普遍较为重要,因此将其得分设置为1/4。 
7. 通过capset(3)等设置了功能(capability)CAP_SYS_RAWIO注3的进程,其得分为1/4,将直接对硬件进行操作的进程判断为重要进程。 
8. 关于Cgroup,如果进程只允许与促使OOM Killer运行的进程所允许的内存节点完全不同的内存节点,则其得分为1/8。 
9. 最后通过proc文件系统oom_adj的值调整得分。 

依据以上规则,为所有进程打分,向得分最高的进程发送信号SIGKILL(到Linux 2.6.10为止,在设置了功能CAP_SYS_RAWIO的情况下,发送SIGTERM,在没有设置的情况下,发送SIGKILL)。 
各进程的得分可以使用/proc/<PID>/oom_score来确认。 


来自: https://blog.csdn.net/JeffreyNicole/article/details/47263235 

上面的算法看着有点复杂,另外 Linux 的系统选择的策略不断在演进,还是得看正在用的版本的算法。

大概的算法的意思是:通过设置一些值来影响OOM killer做出决策。Linux下每个进程都有个OOM权重,在 /proc/<pid>/oom_adj里面,取值是-17+15,取值越高,越容易被干掉。

最终OOM killer是通过 /proc/<pid>/oom_score 这个值来决定哪个进程被干掉的。这个值是系统综合进程的内存消耗量、CPU时间(utime + stime)、存活时间(uptime – start time)和oom_adj计算出的,消耗内存越多分越高,存活时间越长分越低。

总之,总的策略是:损失最少的工作,释放最大的内存同时不伤及无辜的用了很大内存的进程,并且杀掉的进程数尽量少。 另外,Linux在计算进程的内存消耗的时候,会将子进程所耗内存的一半同时算到父进程中。

这也说明了,为什么有内存用的很多的模块,却没有被kill了,而是kill了一个cpu使用时间少的一个模块。因为内存大的模块,CPU的运行时间长,所以分数比较低。

top - 17:23:31 up 619 days, 21:41, ? users,  load average: 1.59, 1.88, 1.62
Tasks: 651 total,   1 running, 650 sleeping,   0 stopped,   0 zombie
Cpu(s): 15.7%us,  1.6%sy,  0.0%ni, 74.5%id,  8.1%wa,  0.0%hi,  0.1%si,  0.0%st
Mem:  24738212k total, 24577344k used,   160868k free,     2068k buffers
Swap:  2097144k total,  2097144k used,        0k free,    79416k cached

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
17880 root      20   0 1075m 895m 1632 S  0.0  3.7 123:55.47 python
17897 root      20   0 1076m 894m 1632 S  0.0  3.7 122:55.16 python
17840 root      20   0 1064m 893m 1632 S  0.0  3.7 125:09.59 python

/proc/$pid/oom_score_adj

The value of /proc/<pid>/oom_score_adj is added to the badness score before it
is used to determine which task to kill.  Acceptable values range from -1000
(OOM_SCORE_ADJ_MIN) to +1000 (OOM_SCORE_ADJ_MAX).  This allows userspace to
polarize the preference for oom killing either by always preferring a certain
task or completely disabling it.  The lowest possible value, -1000, is
equivalent to disabling oom killing entirely for that task since it will always
report a badness score of 0.

在计算最终的 badness score 时,会在计算结果是中加上 oom_score_adj ,这样用户就可以通过该在值来保护某个进程不被杀死或者每次都杀某个进程。其取值范围为-1000到1000 。

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

/proc/$pid/oom_adj

The value of /proc/<pid>/oom_score_adj is added to the badness score before it

For backwards compatibility with previous kernels, /proc/<pid>/oom_adj may also
be used to tune the badness score.  Its acceptable values range from -16
(OOM_ADJUST_MIN) to +15 (OOM_ADJUST_MAX) and a special value of -17
(OOM_DISABLE) to disable oom killing entirely for that task.  Its value is
scaled linearly with /proc/<pid>/oom_score_adj.

该设置参数的存在是为了和旧版本的内核兼容。其设置范围为-17到15。从Linux 2.6.36开始都安装了/proc/<pid>/oom_score_adj,此后将替换掉/proc/<pid>/oom_adj。即使当前是对/proc/<pid>/oom_adj进行的设置,在内核内部进行变换后的值也是针对/proc/<pid>/oom_score_adj设置的。可以参见 feature-removal-schedule 这里 171行。

/proc/$pid/oom_score

This file can be used to check the current score used by the oom-killer is for
any given <pid>. Use it together with /proc/<pid>/oom_score_adj to tune which
process should be killed in an out-of-memory situation.

OOM killer机制主要根据该值和 /proc/<pid>/oom_score_adj 来决定杀死哪一个进程的。分数越高,越先被kill。

通过如下命令可以查看进程 oom_score 分数情况。最后一列是分数。

# ps -eo pid,command,pmem --sort -rss | awk '{"cat /proc/"$1"/oom_score" | getline oom; print $0"\t"oom}'
cat: /proc/PID/oom_score: No such file or directory
  PID COMMAND                     %MEM
 7663 python ./executor.py vsSche  2.2  794
 7675 python ./executor.py vsSche  2.1  683
 7671 python ./executor.py vsSche  2.1  780
 7811 python ./executor.py vsreso  0.1  1873
 7816 python ./executor.py vsreso  0.1  1877
 7465 python ./dispatcher.py host  0.1  15
 7540 python ./executor.py vnc_au  0.0  1914

其中有一个很低的进程才 15 分,看了下是一个常驻周期性的任务进程,执行的CPU时间非常长,所以算法把该进程的分数设置的非常低。 而经常被kill的进程分数有1900+分,所以这类进程老是被kill。

防止进程不被kill

通过上文说的,一般通过 /proc/<pid>/oom_score_adj设置一个很低的分数,例如 -1000 分永远不被杀死。

当系统认为进程都无法被杀死后,内核就会panic,然后整个机器挂了。

sigkill 信号无法被捕获

日志中记录了 SIGNTERM(15),但是在 SIGNKILL(9)时却没有打印,如果有一个 SIGNKILL 的信号,那么看日志的时候也会有一个更直观的线索。

两者区别:

  • SIGNKILL(9) 的效果是立即杀死进程. 该信号不能被阻塞, 处理和忽略。

  • SIGNTERM(15) 的效果是正常退出进程,退出前可以被阻塞或回调处理。并且它是Linux缺省的程序中断信号。

The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.

不过SIGKILL无法在进程里面被捕获。在Python中会直接跑出一个异常,所以无法打印这个信号。

>>> import signal
>>> def trycache(*args):
...     print args
...
>>> signal.signal(signal.SIGKILL,trycache)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: (22, 'Invalid argument')

阻止内存泄漏方法

很多程序会跑着跑着然后内存就泄漏了,然后内存就OOM。

特别的,像Python 虚拟机这样的程序,就申请了内存后,就不会释放内存给系统,会一直在虚拟机内部进行使用。那么对待这类程序的内存泄漏最好的方法是什么呢?

周期性reload进程。

是的,就是这么暴力,这么直接。

之前看 uWSGI里有个参数max-requests在处理了一定次数的请求后,会自动reload该进程。这样可以防止该进程的内存泄漏。

像平台类的软件,能做的就是在外部周期性reload进程。而业务的编写者,就得控制好自己进程的内存管理了。

不过要支持一个能reload进程的服务(注意不是restart),这个是一个要求需要比较高能力。幸好大部分的CGI进程都支持reload,例如nginx、php-fpm、uWSGI等。

而一个后台批量型任务,做到restart就行了,毕竟对进程重启对间隙不太敏感。做负载均衡的话,推荐用MQ可以推送给还能处理任务的进程。

自动补充进程

发现一些OOM的被kill的进程,比如 nslcd 进程,不断的被kill,但是会不断给补充进来。因此我们的进程管理设置,也需要增加一些进程自动补充,这样防止进程被oom几天都不知道。这样可以达到变相reload。

参考

关于python捕获内核发出的sigkill信号问题

https://learning-kernel.readthedocs.io/en/latest/mem-management.html

声明:未经允许禁止转载 东东东 陈煜东的博客 文章,谢谢。如经授权,转载请注明: 转载自东东东 陈煜东的博客

本文链接地址: 进程不见了,Linux 的OOM Killer – https://www.chenyudong.com/archives/linux-oom-killer.html