今天抽时间解决了一个升级Elasticsearch后带来的一个问题。

起源是最初开发的一个用于维护搜索的项目infiniti,主要功能是重建索引,创建备份,维护分词,排序控制等。其中重建索引的部分,原先是基于elasticsearch-jdbc来做的,为了方便起见,从数据库导入数据的部分需要执行shell脚本。在前面一篇文章中也有说到,ES升级到5.0之后,数据库同步的部分也切换到了logstash,同样是通过执行shell脚本的方式同步数据。

原先执行shell脚本的姿势是:

1
Runtime.getRuntime().exec()

一直稳定运行了大半年。

但是更换到logstash的shell脚本之后,线上环境在执行shell的时候都会抛出OOM异常:

1
2
3
java.lang.OutOfMemoryError: Java heap space
Dumping heap to /heapdump.hprof ...
Heap dump file created [4328855 bytes in 0.024 secs]

因为daily环境没有出现过问题,上线当晚顿时吓出一身冷汗,因为ES的相关的搜索代码已经更新了,配套的logstash也是箭在弦上不得不发,第一件事(勿喷):加jvm内存 -> 增加机器内存。 通过free命令和top命令可以看到机器的可用内存还非常非常多:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ free
total        used        free      shared  buff/cache   available
Mem:       16268236     2229004     7143792      737712     6895440    12946664
Swap:             0           0           0

$ top  
 load average: 0.04, 0.04, 0.05
Tasks:  86 total,   1 running,  85 sleeping,   0 stopped,   0 zombie
%Cpu(s):  1.0 us,  0.5 sy,  0.0 ni, 98.5 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 16268236 total,  7143296 free,  2229468 used,  6895472 buff/cache
KiB Swap:        0 total,        0 free,        0 used. 12946204 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                                                             
23250 admin     20   0 4738164 1.097g  15420 S   1.3  7.1  54:58.24 java  

而logstash的jvm参数配置是:

1
2
-Xms2g
-Xmx2g

初始和最大堆内存都是2G,显然可用内存是远远多余2G的。

再观察OOM留下的dump文件: 整个dump文件只有1.4M,怎么可能耗尽内存?

百思不得其解,观察了一下线上机器和daily机器的区别:daily机器的内存(4G)只有线上机器增加内存前(8G)一半的内存,但是存在swap交换区。 因为线上使用的阿里云ECS默认是不配置swap的,原因是OOS的读写性能比较差:

而daily机器是SSD,所以读写性能较优,存在swap分区:

1
2
3
4
free
              total        used        free      shared  buff/cache   available
Mem:        8011064     3873528     2529872        8568     1607664     3868544
Swap:       4194300           0     4194300

是不是因为daily的swap分区为执行shell瞬间膨胀的内存用量提供了缓冲所以使得daily机器未出现问题? 因而尝试了给阿里云ECS配置了swap分区,参考云服务器 ECS Linux SWAP 配置概要说明。 然而,依然没有起任何作用。

仔细想了想,在JAVA中执行Shell脚本,除了Runtime.getRuntime().exec(),还有ProcessBuilder.start(),遂改写代码为ProcessBuilder实现。 然而,依然没有起任何作用。研究了一下,发现Runtime.getRuntime().exec()底层其实就是使用ProcessBuilder.start(),这两个是一家,都依赖于:

1
java.lang.UNIXProcess.forkAndExec

后来搜到了stackoverflow上的一个问答How to solve “java.io.IOException: error=12, Cannot allocate memory” calling Runtime#exec()?,跟我遇到的问题一模一样。

原来UNIXProcess.forkAndExec()在新建进程的时候,会将现有进程占用的内存大小完全fork一份出来,不管新进程使用的内存或大或小。

fork() call actually duplicates the entire memory of the currently running process. If you have a java program with 1.2 GB memory and 2GB total,it will fail。 fork()的调用实际上复制了当前进程的整个内存。如果你当前程序声明了1.2G内存,而内存共有2G,这个命令就会失败(因为剩余的0.8G不足存放复制出来的1.2G)

这是一个多么操蛋的设定。。 好吧,定位了问题,下面我们想办法解决。 首先,尝试命令:

1
echo 1 > /proc/sys/vm/overcommit_memory

这个命令的含义是:

overcommit_memory set to 1 every malloc() will succeed. Linux will start randomly killing processes when you’re running out of memory 将overcommit_memory设置为1,每次通过malloc()命令来划用内存都会返回成功,如果可用内存不足,系统会开始随机停止一些耗大量内存砸进程(OOM KILL)

然而,依然没有起任何作用。

再认真想了一想,原来增加jvm的直觉恰恰是与真相背道而驰了。我一再增加现有进程的内存,就会导致fork的时候,需要的内存越多,就越要出现OOM!所以正确的姿势是,减少现有进程声明的内存:

1
 -Xms96m -Xmx384m -XX:PermSize=96m -XX:MaxPermSize=384m

终于,整个世界清静了。


PS.阿里云,能退我的内存钱吗。。 PPS.再也不在紧急关头先加配置了。。