1. 现象、背景
22号晚上十一点多,系统监控告警,集群中一台机器,出现OOM(OutOfMemeoryError);保护现场后,联系运维紧急重启系统。
查阅日志,OutOfMemeoryError 异常信息,是从一个线程中抛出来的。
(当时看到 io.prometheus.client…信息就在怀疑是不是prometheus监控的问题,但要继续分析,需要更多的证据)
2. YoungGC
查看系统监控,发现在OOM之前,发现YoungGC的频次、耗时,陡然升高。
JVM用的是G1垃圾收集器,一次YoungGC的过程主要包含两个过程:
- 从GC Root扫描对象,对存活对象进行标注。
- 将存活对象复制到空的survivor区或晋升到old区。
监控显示的GC原因:G1 Evacuation Pause ,也就是上面的第二步。
观察图表,期间触发了多次 Old Gen 的GC,而且Young Gen的GC耗时很长。
大致可以得出结论:长生命周期的对象增加,导致标注和复制过程的耗时增加(G1 Evacuation Pause 阶段)。
YoungGC详情:G1 Eden Space(伊甸区)回收率100%。
3. hprof分析
根据JVM配置,从指定路径下获取dump文件(-XX:HeapDumpPath
)。
期间24号凌晨,进行了生产环境压测,又出现了同样的OOM,而且还是同一台机器。这时候就怀疑是不是这台机器的JVM配置有问题,然后对比了一下集群中所有机器的JVM配置,配置一摸一样,没有任何区别(最大4G)。
由于生产机器,直到24号下午才拿到dump文件(流程审批)。
拿到 hprof 文件后,用 Eclipse Memory Analyzer 打开,进行分析。
先看 Top_consumers
,最大的的五个Object对象,占用了百分之七十的内存。从线程名称来看,这五个线程应该是一个线程池中的。
然后扒了一下代码中所有用到线程池的地方,都进行了命名。不会用这个默认的名字。排除代码中的线程池。
转换成dominator tree结构,进入thread线程里面查看;
从左边的 attributes
已经可以看到了,是 io.prometheus.client.exporter.HTTPServer
这个类。
线程就是这个类中线程池里的,线程池大小正好是5个,破案。
4. 定位
出问题的地方是找到里,但原因没有找到。捋了一遍这个类代码,没有毛病。
突然灵光一闪,想到一个点:以前听监控服务的人说过,监控数据的维度,量级不能过多(用过该监控的可能知道)。然后去扒了一下业务代码中的监控代码,果不其然,监控的维度达到了用户级别。
举个例子,你可以监控缓存key的前缀,因为key前缀并不会有很多;但是,不能监控拼装完整的key。
为啥?因为拼装完整key的数量是很多的,如果是用户维度:key前缀+用户编码 ,这种维度的key,量级是很可怕的。
prometheus client会将监控的数据,临时写在应用的内存中,服务端通过定期扫描,取走数据,然后再图表中展现。
监控的数据,必须是有穷的,不能是无穷的。用户维度数据相对来说就是无穷的了。内存和存储都吃不消。
5. 整改
排查代码中所有使用prometheus监控的数据的维度,进行粗化(有穷)处理。
6. 后话
后来在网上乱翻,看到一个工具,很好用:Jprofiler