早上6点,我不得不开始处理“叫醒”我的一些问题。因为当这些问题发生的时候,我的手机铃声响了。昏睡中的我非常不情愿地拿起了手机,检查我是否疯狂到将叫醒闹钟设在了早上5点。原来是监控系统发现一个Plumbr服务死掉了。
作为一名该领域经验丰富的高手,我首先来到了咖啡机旁。我需要用一杯咖啡开始工作。第一个问题,在应用崩溃之前看起来一切运行正常。日志中没有错误,没有告警,也没有其他任何异常。
我们的监控系统已经察觉到进程死掉了,并且已经重启了崩溃的服务。因为血液中已经有了咖啡因,我开始收集更多的证据。30分钟后,在/var/log/kern.log文件中发现了以下内容:
Jun 4 07:41:59 plumbr kernel: [70667120.897649] Out of memory: Kill process 29957 (java) score 366 or sacrifice child Jun 4 07:41:59 plumbr kernel: [70667120.897701] Killed process 29957 (java) total-vm:2532680kB, anon-rss:1416508kB, file-rss:0kB
很显然,我们成了Linux内核的受害者。大家都知道,Linux建立在一些守护进程之上。这些守护进程被几个看起来糟透了的内核任务看管。所有现代Linux内核都内置了一个被称为“内存不足杀手”的机制,它在内存不足的情况下会杀掉用户进程。当检测到内存不足时,杀手会被激活并选择一个进程杀死。选择机制是用启发式算法对所有进程进行打分,最后选择得分最低的进程杀死。
理解“内存不足杀手”
默认情况下,Linux内核允许进程请求比当前系统可用内存更多的内存。这是有道理的,因为大部分进程从来不会用掉它们请求的所有内存。就像有线网络运营商,他们承诺每个用户100Mbit的下载速度,这远远超出了运营商网络的真实带宽。因为他们认为所有用户不会同时达到带宽的上限。所以,一个10Gbit的链路能够很好地为100个用户提供服务超。
这种机制的一个副作用是,一些程序会消耗系统内存。这将导致内存不足,使得没有内存页面可以分配给进程。你可能遇到过这种情况,只有root账号才能杀掉offending任务。为了避免这种情况发生,杀手进程会被启动,识别进程并杀死它。
更多关于“内存不足杀手”的内容请参见这篇RedHad的文档。
内存不足杀手由谁触发?
现在,我们知道了一些背景知识,但是内存不足杀手由谁触发?究竟什么原因让我在早上5点被叫醒?一些调查显示:
- /proc/sys/vm/overcommit_memory中的配置允许过量使用内存,它被设置为1,意味着每一次malloc都能够成功申请到内存。
- 应用运行在一个EC2 m1.small实例上。EC2实例默认是不支持交换区的。
这两点再加上突然增加的访问导致了我们的应用会申请越来越多的内存以支持这些用户。过量使用内存配置也允许为这些进程申请越来越多的内存,最后触发了“内存不足杀手”,就像它的名字那样,杀死我们的应用然后在半夜把我叫醒。
示例
当我向工程师们描述这个问题时,有一个很有兴趣的工程师用一个小测试程序来复现这个问题。当在Linux(最新稳定版Ubuntu)上编译和加载下面的Java代码片段时,
package eu.plumbr.demo; public class OOM { public static void main(String[] args){ java.util.List l = new java.util.ArrayList(); for (int i = 10000; i < 100000; i++) { try { l.add(new int[100_000_000]); } catch (Throwable t) { t.printStackTrace(); } } } }
你会发现类似下面的消息:Kill process (java) score 或牺牲子进程的消息。
注意:你可能需要修改交换区和堆大小。在我的测试程序中,将堆大小通过-Xmx2g设置成2G,通过如下配置设置交换区大小:
swapoff -a dd if=/dev/zero of=swapfile bs=1024 count=655360 mkswap swapfile swapon swapfile
解决方案?
有很多种方法可以解决这个问题。在我们的示例中,我们只是把系统迁移到一个有更大内存的实例中。并且我还建议允许交换,但是当咨询过工程人员后,我意识到Java虚拟机中的垃圾回收进程在交换时表现不是很好,所以这个选项最后没有被采用。
其他可能有用的方案包括微调内存不足杀手,在几个实例间进行负载均衡或者降低应用的内存需求。
转载请注明:爱开源 » 内存不足:杀死进程还是牺牲子进程