生产环境 OOM 定位
2025-01-22 08:19:30    894 字   
This post is also available in English and alternative languages.

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 阶段)。

yangGC耗时

YoungGC详情:G1 Eden Space(伊甸区)回收率100%。

yangGC详情


3. hprof分析

根据JVM配置,从指定路径下获取dump文件(-XX:HeapDumpPath)。

期间24号凌晨,进行了生产环境压测,又出现了同样的OOM,而且还是同一台机器。这时候就怀疑是不是这台机器的JVM配置有问题,然后对比了一下集群中所有机器的JVM配置,配置一摸一样,没有任何区别(最大4G)。

由于生产机器,直到24号下午才拿到dump文件(流程审批)。

拿到 hprof 文件后,用 Eclipse Memory Analyzer 打开,进行分析。

先看 Top_consumers,最大的的五个Object对象,占用了百分之七十的内存。从线程名称来看,这五个线程应该是一个线程池中的。

然后扒了一下代码中所有用到线程池的地方,都进行了命名。不会用这个默认的名字。排除代码中的线程池。

MemoryAnalyzer-1

转换成dominator tree结构,进入thread线程里面查看;

从左边的 attributes 已经可以看到了,是 io.prometheus.client.exporter.HTTPServer 这个类。

MemoryAnalyzer-2

线程就是这个类中线程池里的,线程池大小正好是5个,破案。

异常代码


4. 定位

出问题的地方是找到里,但原因没有找到。捋了一遍这个类代码,没有毛病。

突然灵光一闪,想到一个点:以前听监控服务的人说过,监控数据的维度,量级不能过多(用过该监控的可能知道)。然后去扒了一下业务代码中的监控代码,果不其然,监控的维度达到了用户级别。

举个例子,你可以监控缓存key的前缀,因为key前缀并不会有很多;但是,不能监控拼装完整的key。

为啥?因为拼装完整key的数量是很多的,如果是用户维度:key前缀+用户编码 ,这种维度的key,量级是很可怕的。

prometheus client会将监控的数据,临时写在应用的内存中,服务端通过定期扫描,取走数据,然后再图表中展现。

监控的数据,必须是有穷的,不能是无穷的。用户维度数据相对来说就是无穷的了。内存和存储都吃不消。


5. 整改

排查代码中所有使用prometheus监控的数据的维度,进行粗化(有穷)处理。


6. 后话

后来在网上乱翻,看到一个工具,很好用:Jprofiler

Jprofiler-analyzer

Jprofiler-analyzer-1

Jprofiler-analyzer-2