一、背景
最近新服务上线,运行了一段时间都很平稳,没有出现什么大的异常,突然有一天运维同事通知说注册中心上服务掉线了。于是登录了发生异常服务的组件,查看日志信息,关键信息如图:
从上面两个图片可以简单了解到,
1、应该是服务发生了OOM异常
2、Consul链接因为Connection pool shut down而链接失败。
二、问题分析
猜想1:是不是因为Consul服务宕机,导致服务链接不上,并且短时间重试多次造成OOM?
实际:整个错误持续了几个小时,consul服务宕机其他服务也应该收到影响,但是其他服务并没有问题。所以猜想1应该不对。
猜想2:Connection pool shut down 这个问题是因为什么产生的?是不是可以从该地方入手,根据图中的异常位置可以查看代码。查看对应的
继续查看
所以一定是因为 isShutDown 这个状态值变为了true,才导致打印对应的错误信息的。
继续查找shutdown可能赋值为true的地方
最终定位到能够调用shutdown只有finalize()或者close()。
所以猜想一定是发生OOM之后,GC在回收内存的时候调用了finalize()方法,继而引发的isShutDown变为true。
猜想3:我们是否可以通过dump文件,查看内存泄漏的地方,进而在本地模拟这个场景。找到运维下载OOM时候的dump文件,经过MAT工具一番查看,可以很明显的找到内存泄漏的点。
如图所示:据dump文件描述,PoolingHttpClientConnectionManager 这个对象居然占到了整个大对象内存的90%多,并且是被一个数组对象引用着的,引用的对象数量居然有60万。继续查看详细:可以看到这个对象应该是在 com.aliyun.oss.common.comm.IdleConnectionReaper 这个类当中,我们可以看下这个类的源码,可以确认应该就是下面这个静态对象列表。
查看该列表使用的地方,会发现在registerConnectionManager方法内进行填充
registerConnectionManager方法调用的地方,
最终定位,果然是在创建OSSClient的时候调用的
OssClient构造函数
我们看下OssClient是如何创建的,为什么connectionManagers列表会持有这么多对象。最终发现在上传图片的过程中,我们每调用一次getOSSClient()就会生成一个OssClient对象,就会在connectionManagers添加一个PoolingHttpClientConnectionManager对象。
最终问题定位应该就是这里,因为没有及时关闭OssClient导致的内存泄漏,因为不是一瞬间就造成内存暴涨,而是缓慢的增加,所以没有第一时间发现,这也是为什么重启之后就没有报错的原因。
三、问题重现
我们本地启动服务,修改一下jvm参数如下(比线上减少了10倍)
-Xms200M -Xmx200M -Xss256K
顺便增加一些测试代码,打印一下当前jvm的内存信息
启动程序,模拟线上上传图片:
随着请求数量增加,可以发现connectionManagers中PoolingHttpClientConnectionManager对象也在不断上涨。
加大请求次数,通过VirtualGC查看内存分布,也可以发现 老年代一直在上涨,几次GC之后也没有明显下降
最终在我们还可以看到
1、出现了OOM异常
2、Consul的 Connection pool shut down 也被触发了
综上所有的迹象都说明就是因为OSSClient这个类被一直new,导致内存不断泄漏、最终导致程序OOM使得出现了很多异常。
四、问题解决
因为本服务需要提供用户上传图片到阿里云OSS的功能,并且调用频次还算比较高,所以解决方式有两种。
1、每次new OSSClient之后,需要调用shutdown方法,移除PoolingHttpClientConnectionManager对象。
我们都知道new操作是一个比较耗费资源和性能的操作,而且什么时候shutdown不好控制,所以我觉得这样的方式比较适用于操作比较少的场合。
2、使用单例的OSSClient,看了刚刚的代码OssClient本身也封装了一个PoolingHttpClientConnectionManager对象,说明它本身就是支持链接池的,也就是可以并发访问。
修正为单例之后,执行测试:
可以看到经过一段时间的请求,整体老年代内存维持不高。
四、问题总结
经过这个bug的排查,可以有以下几点收获:
1、遇到OOM问题,仅凭借日志输出可能不能马上定位到问题的本质,最好可以通过dump文件进行分析
2、像HttpClient这种需要建立socket链接的资源,在使用的时候尽量能够池化,避免大量创建对象耗费资源,并且使用后要考虑关闭的问题。