问题背景&现象
ExitCode:137 OOMkill
线上环境的后台服务频繁重启,重启的原因日志是ExitCode:137 OOMkill 。OOMkill是服务器的一种保护行为,服务器会定时的扫描服务器中的各个进程,每个进程根据一种机制得到一个分数,如果这个分数最高则会被服务器主动kill掉。OOMkill的具体机制在此不做过多赘述。而进程得到高分的原因一般情况是内存资源占用过高导致的。被OOMkill后总是难于保留当时具体情况,每次都是服务被重启了才发现问题。还好运维平台可以支持OOMkill保护,发现资源超限情况不让直接OOMkill而是先保护现场并发出告警。
Memory hit original limit
果然设置了保护之后,告警就接踵而至了。而且确实是内存超限导致的,如果没有保护又事一次烦人的OOMkill 。知道是内存的问题接下来问题定位的方向就更明了了。
问题定位&分析
先dump堆内存再说
第一个首先怀疑的是应用服务的堆内存。但是dump堆内存下来分析后,发现占用的并不高。总内存只有200M左右。
看来堆内存并不是罪魁祸首。
总结:dump命令,jmap -dump:format=b,file= /文件路径/dump文件名称.hprof 进程ID ,如:jmap -dump:format=b,file=/home/heap.hprof 1
是堆外内存的原因吗?
不是堆内存,那应该就是堆外内存的原因了。项目中有些开源框架使用到Netty,Netty是有使用堆外内存的。而且JDK8的元空间也是属于堆外。
JDK8元空间观测
通过查看JVM的配置,发现元空间的大小并没有限制。
原因是项目以前使用的是JDK8以前的版本,升级到了JDK8后对应的JVM配置没有对应更新。运维平台观测到的非堆内存占用确实也是不小。
给JVM参数加元空间的限制,-XX:MetaspaceSize=xxxm -XX:MaxMetaspaceSize=xxxm 。
本以为万事大吉,但过了几天烦人的内存超限告警还是来了。
netty堆外内存占用观测
不是元空间占用的大量内存,那会不会是项目其他地方使用到了堆内存呢? 查看了整个项目的业务代码和相关的依赖,只有Netty使用到堆外内存。那么就想办法观测一下Netty使用堆外内存的情况。关于Netty的堆外内存如何使用,因篇幅有限此处不做扩展;只是通过以下工具可以监控到堆外内存的大小:
public static List<String> startReport() {
Field field = ReflectionUtils.findField(PlatformDependent.class, "DIRECT_MEMORY_COUNTER");
field.setAccessible(true);
try {
AtomicLong directMemory = (AtomicLong) field.get(PlatformDependent.class);
int memoryInKb = (int) (directMemory.get() / 1024);
log.info("堆外内存大小:{}",memoryInKb);
if (list.size() <=100) {
list.add("时间:" + LocalDateTime.now()
.toString() + " 堆外内存:" + memoryInKb + "k ");
}else {
list.clear();
}
return list;
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return list;
}
并通过一个上帝接口持续观测:
但是经过一段时间的观测,占用的内存并没有异常增长。
page cache占用总内存情况
无奈之下,只能进一步的通过运维监控平台继续找一些观测数据。经过长时间的观测对比。发现page cache的变化对总内存变化影响很明显,而且发出内存超限提醒的服务节点page cache都没法正常回收。
page cache是linux内核为了提高磁盘文件操作而设计的一种机制。它会将一些磁盘的文件数据写入到内存中,并设计一套淘汰机制回收内存。根据上图观测到的情况,page cache占用的内存并没法正常回收。
vmtouch观测文件缓存情况
梳理了整个业务项目的代码,并没有发现有大量读写文件的地方。怀疑是日志文件的读写,导致一直在产生page cache。为了验证个问题需要使用到vmtouch命令,关于此命令的介绍可以参见官方文档:https://hoytech.com/vmtouch
此命令可以查看磁盘文件被加载到内存中的情况。
查看日志文件被加载到内存的情况:
整个日志文件900M左右,全部被加载到内存中。
将被加载到内存中的缓冲驱逐掉:
观测到的page cache和总内存对应马上降低下来:
终于问题原因变得明朗了,接下来就要看看page cache为什么不能正常回收了。
page cache数据观测
为什么page cache这么多?
page cache为什么这么多,是因为日志文件太大了吗?为了验证这个问题,通过修改log4j的配置,将每个日志文件分割成500M以内,但是问题依旧。而且日志文件被切割后,不再写入的日志文件在依然被加载到内存中,说明是读日志文件的原因。
日志文件的读场景,是运维平台做日志采集的情况才会触发的。于是找了运维平台的同事反馈。
为什么page cache没正常回收?
反馈问题后自己也持续观察,发现有问题的节点。发现page cache的扫描速度很慢,而且脏页阈值配置的也不一样。和运维同事沟通后,得知服务器节点是被随机分配的,可能节点属于不同的物理机,而脏页阈值的配置使用的应该是物理机的配置。
问题解决&防范
曲线救国
既然问题原因是读日志文件导致的,那就不让监控程序主动来读取日志,而是主动将数据通过消息队列推送给监控系统。公司基于log4j自研封装了日志工具,可以根据配置将日志直接通过kafka推送给监控系统,这样就能避免读日志文件,page cache的问题也可以避免了。
问题根治
主动推送日志的方法只能是避免问题的发生,要根治问题还是得从page cache的相关配置入手。page cache难回收的原因一般有三个(具体可以参见:Page Cache难以回收问题):
1.直接内存回收导致
2.系统脏页内存积压过多导致
3.系统NUMA策略配置不当导致
其中情况1 通过sar -B命令观测可以排插,因为pgscand并没有发生
情况3通过命令numactl --hardware确认也可以排除了。
情况2需要不断配置系统参数并调试,因是线上环境,而且初始环境没法模拟验证,只能反馈给运维团队,让他们持续优化。