线上内存泄漏引发OOM问题分析和解决

作者:valar
前言长文预警 。该文主要介绍因线上OOM而引发的问题定位、分析问题的原因、以及如何解决问题 。在分析问题原因时候为了能更详细的呈现出引发问题的原因,去翻了hdfs 提供的JAVA Api主要的类FileSystem的部分代码 。由于这部分源代码的分析实在是太太太长了,可以直接跳过看最后的结论,当然有兴趣的可以看下 。
风起一日,突然收到若干线上告警 。于是赶紧查看日志,在日志中大量线程报出OOM错误:
Exception in thread "http-nio-8182-exec-29" java.lang.OutOfMemoryError: Java heap space于是使用jstat命令查看该进程内存使用情况:jstat -gcutil 12492 1000 100
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 0.00 100.00 99.89 96.78 94.41 200 1.272 2925 328.850 330.122 0.00 0.00 99.89 99.89 96.78 94.41 200 1.272 2935 329.908 331.180 0.00 0.00 100.00 99.89 96.78 94.41 200 1.272 2944 330.853 332.125 0.00 0.00 99.89 99.89 96.78 94.41 200 1.272 2955 332.002 333.274 0.00 0.00 100.00 99.89 96.78 94.41 200 1.272 2964 332.940 334.212 0.00 0.00 100.00 99.89 96.78 94.41 200 1.272 2973 333.924 335.196可以看出,该进程老年代内存耗尽,导致OOM,且引发了频繁的FGC 。而在对堆参数配置中是完全能满足项目运行的,于是查看了其他几个节点的内存使用情况,老年代使用率都高达98以上且FGC次数也在增加 。
由于线上环境影响业务,便dump出内存快照,然后临时重启了节点,重启之后查看内存使用情况: jstat -gcutil 18190 1000 10
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT1.04 0.00 50.39 22.87 95.96 93.41 1680 20.542 4 0.136 20.679 1.04 0.00 50.39 22.87 95.96 93.41 1680 20.542 4 0.136 20.679 1.04 0.00 50.39 22.87 95.96 93.41 1680 20.542 4 0.136 20.679虽然暂时业务恢复,但该问题还是需要解决的 。从上能初步分析出问题是由于内存泄漏,导致在运行一段时间之后OOM 。
定位在将dump出的快照导入MAT中查看,并没有找到特别大的对象,但是看见很多个org.Apache.hadoop.conf.Configuration实例 。在代码中使用了hdfs的API操作hdfs,该类为连接hdfs的配置类 。如下:
 

线上内存泄漏引发OOM问题分析和解决

文章插图
 
于是在本地debug启动一个与线上相同代码的进程,并dump出该内存快照 。在MAT中查看该Configuration类的实例,仅一个实例 。到此,差不多能定位是通过Java Api与hdfs交互时,导致某些对象不能回收出现的问题 。
然后在本地编写测试接口,通过测试接口访问hdfs,发现该Configuration类实例在增加,且在执行GC的时候并不能回收 。
至此,内存泄漏的源头可以说找到了,至于为什么会出现问题则需要查看这段代码了 。
原因大致能确认,导致内存泄漏的原因是与hdfs交互时某段代码bug 。于是翻开了项目中与hdfs交互的类,发现了等价于下面的代码的访问hdfs代码:
public Path createDir(String name) throws IOException, InterruptedException { Path path = new Path(name); Configuration configuration = new Configuration(); FileSystem fileSystem = FileSystem.get(URI.create("hdfs://***:8020"), configuration, "hdfs");; if (fileSystem.mkdirs(path)) { return path; } return null; }也就是说,在每次与hdfs交互时,都会与hdfs建立一次连接,并创建一个FileSystem对象 。但在使用完之后并未调用close()方法释放连接 。
此处可能会有疑问,此处的Configuration实例和FileSystem实例都是局部变量,在该方法执行完成之后,这两个对象都应该是会被回收的,怎么会导致内存泄漏呢?
FileSystem是怎样获取的在此,如果想知道该问题,就需要去翻FileSystem类的代码了 。FileSystem的get方法如下:
public static FileSystem get(URI uri, Configuration conf) throws IOException { String scheme = uri.getScheme(); String authority = uri.getAuthority(); if (scheme == null && authority == null) { // use default FS return get(conf); } if (scheme != null && authority == null) { // no authority URI defaultUri = getDefaultUri(conf); if (scheme.equals(defaultUri.getScheme()) // if scheme matches default && defaultUri.getAuthority() != null) { // & default has authority return get(defaultUri, conf); // return default } }String disableCacheName = String.format("fs.%s.impl.disable.cache", scheme); if (conf.getBoolean(disableCacheName, false)) { return createFileSystem(uri, conf); } return CACHE.get(uri, conf); }重点看一下最后的6行代码,其中String.format("fs.%s.impl.disable.cache", scheme)在连接hdfs时候该参数名为fs.hdfs.impl.disable.cache,可以从倒数第5行代码看出该参数默认值为false 。也就是默认情况下会通过CACHE对象返回FileSystem 。
那接下来看一下CACHE.get方法:


推荐阅读