这个问题困扰了我2个多月,现在终于找到了原因了。现在将分析过程以及原因记录下来。
背景
部署在容器中的一些Java程序每个月都会有1次由于超过容器的限制内存大小,被cgroup杀掉。 容器的内存限制是3G,Java程序的堆内存的最大大小是2G,内存多出了几百M。
初步分析
非堆内存大小调整
一开始JVM参数配置是这样子的:
ruby
-Xmx2G -Xms2G
-XX:+UseG1GC -XX:G1HeapRegionSize=16m
由于不是JVM层面的内存溢出,所以堆内存配置是没问题。除了堆内存外,还有Metaspace、CodeCache、CompressedClassSpace、DirectByteBuffer这几块非堆内存,没有做限制。所以增加了非堆内存的限制:
ini
-XX:CompressedClassSpaceSize=128m
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=128m
增加了限制后,容器的内存依旧不断增大。查看非堆内存的监控,非堆内存并不会一直增大。所以也不是非堆内存的问题。
分析非JVM管理内存
分别打印内存增大之前和之后的pmap的内容,对比两次的内存情况。发现Java进程的RSS内存只差了7M左右,和实际情况不符合。其他部分的内存差异也不大。分析到这里,还是不能确定问题原因。 获取pmap的命令:
shell
pmap -X ${pid} > pmap.txt
问题定位
发现规律
通过监控平台发现不出现这个问题的容器WSS内存和RSS内存差异不大,而经常OOM的容器WSS内存和RSS内存差异会不断增大,如下图: 那监控中的WSS和RSS指标分别代表什么呢?
分析原因
指标解析
RSS的指标是:container_memory_rss WSS的指标是:container_memory_working_set_bytes container_memory_rss是指不包括任何cache的进程自己的内存,取的是memory.stat文件中的anno的值。 container_memory_working_set_bytes是包含RSS、文件缓存内存、内存脏页、内核内存、slab内存等内存,简单计算就是(总内存-inactive_files内存),并且容器触发OOM Killer是由这个指标决定。当WSS内存超过容器内存限制大小后,就会触发OOM Killer。 那RSS和WSS是差了哪部分内存呢?
差异内存
进入容器执行命令,查看memory.stat:
shell
# 没有这个文件,则打开下面的文件
cat /sys/fs/cgroup/memory.stat
如果没找到上面的文件,则查看下面的文件:
shell
cat /sys/fs/cgroup/memory/memory.stat
内容如下:
yaml
anon 5413322752
file 7618560
kernel 1010552832
kernel_stack 2965504
pagetables 12705792
percpu 0
sock 16384
vmalloc 0
shmem 0
zswap 0
zswapped 0
file_mapped 2818048
file_dirty 49152
file_writeback 0
swapcached 0
anon_thp 5188354048
file_thp 0
shmem_thp 0
inactive_anon 5423308800
active_anon 24576
inactive_file 5468160
active_file 2146304
unevictable 0
slab_reclaimable 992354280
slab_unreclaimable 2309568
slab 994663848
workingset_refault_anon 0
workingset_refault_file 33846
workingset_activate_anon 0
workingset_activate_file 817
workingset_restore_anon 0
workingset_restore_file 627
workingset_nodereclaim 0
pgscan 1517152
pgsteal 1515495
pgscan_kswapd 81139
pgscan_direct 1436013
pgsteal_kswapd 81139
pgsteal_direct 1434356
pgfault 2014894
pgmajfault 209
pgrefill 29369
pgactivate 27688
pgdeactivate 28594
pglazyfree 0
pglazyfreed 0
zswpin 0
zswpout 0
thp_fault_alloc 2162
thp_collapse_alloc 382
发现其中的slab_reclaimable占用的大小刚好跟WSS和RSS的差值差不多。 那slab_reclaimable又是什么含义呢?
SLAB
Linux有两种内存分配算法:伙伴系统(buddy system)和slab。在Linux中,伙伴系统是以页为单位管理和分配内存。slab分配器就应运而生了,专为小内存分配而生。slab分配器分配内存以Byte为单位。但是slab分配器并没有脱离伙伴系统,而是基于伙伴系统分配的大内存进一步细分成小内存分配。 slab专用缓冲区主要用于内核频繁使用的一些数据结构,例如task_struct、mm_struct、vm_area_struct、file、dentry、inode等。 而slab_reclaimable就是slab分配的可被回收的内存,这部分内存在内存不足的时候,可以被系统回收。 但是现在在容器里,却导致了OOM Killer。 那是什么导致了slab_reclaimable使用增加呢?
xxl-job执行器的日志文件
由于slab是分配小内存的,既然能增长到这么大,说明有数量很多的小内存被分配。想到程序中使用了xxl-job,而xxl-job每次调度后,执行器executor都会生成一个单独的日志文件,记录当次的定时任务执行情况。 所以可能是因为频繁创建文件导致slab内存使用过大。
验证猜想
关闭xxl-job执行器日志
修改xxl-job-core的代码,注释掉原来创建文件的代码,改为logger.info方式打印内容: 修改xxl-job日志后,发现WSS和RSS的内存差值,运行一天依然稳定在20M左右。看来问题的确是出在xxl-job不断生产日志文件上。
程序验证
为了更全面的验证创建文件和slab的关系,编写一个程序不断创建小文件,每个文件大小固定在574字节。 场景一:写入50万文件,堆内存:1G,容器最大内存:1500M WSS和RSS差值到了330M 堆内存:1G,非堆:50M左右
场景二:写入500万文件,不删文件,缩小容器内存大小,模拟内存超过容器内存的场景。堆内存:1G,容器最大内存:1500M WSS和RSS差值最大到了317M,达到最高值后,在23点41分触发oom,被cgroup杀掉。
堆内存是1G,非堆保持在50m左右,和场景一差不多,所以OOM与JVM管理的内存无关
cgroup oom kill的系统日志如下,被杀掉时,java进程的RSS内存为1207198K(1178.9M),和容器最大内存1500M,相差322M。符合上图WSS-RSS监控的的差值。java进程的内存基本固定的情况下,场景一50万文件没有OOM,当把写文件数提高到了500万,就出现了OOM。说明不断增加文件数量,非java进程的内存一直在增加。
场景三:写入50万文件,然后再删除50万文件。堆内存:1G,容器最大内存:1500M WSS和RSS差值先涨到299M,文件删除后,内存差值降到3M 堆内存:1G,非堆:50M左右
对比删除文件前和删除文件后的memory.stat文件。可以看到文件删除前slab_reclaimable的大小和(WSS-RSS)的差值基本一致,删除文件后slab_reclaimable也下降,下降到2M左右。
总结
结合第三个场景的数据,基本可以确认,容器的非RSS内存增加和文件创建的文件数量有关系。创建文件产生的内存则是slab_reclaimable。 再结合场景二的情况,说明slab_reclaimable在容器中无法被回收,从而导致了OOM。 用相同代码直接在虚拟机上运行,限制内存大小,发现是slab_reclaimable可以被回收的,slab_reclaimable大小会维持在50M左右,不会不断增长。而在容器中,slab_reclaimable无法回收,会一直增长最终导致容器被杀。