1.查看GC日志
grep 'Full' /data/logs/gc_log/gc.log
红框内 回收大小->回收后大小(总大小),耗费时间 secs。可以看到full gc 一分钟就执行了3次,STW每次接近20秒,已经严重影响业务。
也可以使用命令 jstat -gc 1535 2000 10 查看
jstat -gc 1535 2000 10意味每隔2000毫秒输出 jvm进程1535 的GC信息 打印10次。
2.导出jvm 堆文件分析
此时只能进行对堆文件进行分析哪些对象导致频繁Full GC
使用sz命令进行下载但是当时heapDump.hprof 文件大小是6G ,linux上sc命令最大支持4G故无法使用sz命令进行下载。
所以需要对文件进行切割。
命令:tar cvf heap.tar heapDump.hprof
生产环境出问题上线导致的问题先回滚,不确定的问题先重启。怎么恢复快怎么来搞,哪怕是临时性的。不然再紧张的情况下去修复可能会补丁上打补丁出新问题,引发连锁问题。
漫长的等待后再windows下使用copy 命令将heap.tar.aa,heap.tar.ab,heap.tar.ac,heap.tar.ad,合并为最终的heap.tar文件。
命令:copy /b heap.tar.aa+heap.tar.ab+heap.tar.ac+heap.tar.ad heap.tar
下载Memory Analyzer进行对dump文件分析。
Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation
下载对应安装包,由于最新版本必须jdk11,我是jdk8所以我下载了比较老的版本。
运行MemoryAnalyzer.exe
ParseHeapDump.bat 是可以再linux系统中直接使用的,比较适合那些生产环境中对应dump后的文件十几个G或者几十个G不好下载的情况下去根据dump文件生成报表可以直接分析。网上有对应教程(jvm内存快照dump文件太大,怎么分析 - 神风炼 - 博客园)
导入文件后分析完看到占用最多的是AutomicLong类,这个类代码中没用到,所以基本可以断定是其他依赖包里的,再继续往下看com.netflix.servo.monitor.MonitorConfig ,com.netflix.servo.monitor.BasicTimer。字面意思可以知道是来自netflix的监控什么东西的。
3.定位问题类
直接搜下ServoMonitorCache类是干嘛的。
4.定位问题类引用
类中只有一个map并且名字是timerCache 问题出现的地方应该就是这里,但是具体干什么的不知道。
spring boot 自动装配配置文件(spring.factories)中找到来自spring cloud 的netflix core包,
然后找到ServoMetricsAutoConfiguration (别问为什么因为只有他是metrics包下的)
ServoMetricsAutoConfiguration类中import了MetricsInterceptorConfiguration 看似拦截器的配置类
拦截器中声明了一个MetricsHandlerInterceptor 拦截器
MetricsInterceptorConfiguration 类中初始化的拦截器MetricsHandlerInterceptor 这个拦截器没看懂干什么的先跳过
@ConditionalOnBean spring自动装配就是各种conditionOn xxx 这里意思是如果容器中有RestTemplate 就会执行,并且吧ServoMonitorCache构造注入,ServoMonitorCache正是dump文件分析出来的hashMap对象过多的根源线程,进去详细了解。
MetricsClientHttpRequestInterceptor 显然是个过滤器的名字到底拦截什么的我也不知道,那就看注释,该类的头部注释中是这样写道的。
/** * Intercepts RestTemplate requests and records metrics about execution time and results. * * @author Jon Schneider */
大意是拦截RestTemplate 的request,record,time,result 。所以是不是我们凡是使用restTemplate的请求都会被拦截到?
在过滤器intercept方法内打了断点启动debug 然后发起restTemplate请求果然打到了断点上。
但是执行完以后不管是否异常或者超时都有一个肯定会执行的finally代码块,
然后servoMonitorCache 有一个getTimer方法 传入的是monitorConfigBuilder
monitorConfigBuilder来自MetricsTagProvider.clientHttpRequestTags 方法。进去看下干什么的。
DefaultMetricsTagProvider.clientHttpRequestTags 内部就算记录了http请求的method,uri,status,clientName 然后组装为一个不可以更改的map
然后构造为monitorConfigBuilder,此时可以进入servoMonitorCache的getTimer方法内查看做了什么,这里就跟分析dump文件结果中出现问题根源的类ServoMonitorCache对应上了,刚开始不知道为什么会调用到该类。
原来就是如果每次的monitorConfig对象不存在timerCache这个Map中的话,就put进去。
那如果是重复的url则不会有问题,如果不重复的话就会一直增加。而该项目再使用restTemplate的场景主要是基于feign调用其他业务的情况,而对于其他业务的调用为了防止篡改都使用了动态字符串签名加密,而签名加密的数据都是直接跟在真实请求地址后面的。例如baidu.com?time=2021101015&sign=123abc.这样上面图1中的uri永远都不可能重复,也就导致map中不可能有该数据,从而导致map越来越大。占用JVM内存区域越来越高,前期数据量比较小,后面过不了多久内存就无法满足GC就会一直再工作。
虽然可以看到图中3标有如果timerCache到达一定容量大小后(1000)log.warn的输出,但是该项目QPS一直比较大上线根本看不到。所以metrics认为系统中最多对接1000个外部接口,不可能再多了。。。。
再回过头来看这个metrics到底干什么的,
5.问题解决
spring boot依赖的组件都是通过自动装配的,各种@Condition @import @ConditionalOnProperty matchIfMissing 等,所以回到metrics的装配类中看下对于metrics的装配是否可以进行配置。
然后再配置文件中配置spring.cloud.netflix.metrics.enabled = false 即可。禁用metrics的请求监控,因为我们项目有自己的监控工具。
然后启动应用再次发起restTemplate的请求则不会执行先前打断点的拦截器了,也就不会再有重复uri添加到map中了。
TODO:后续应该考虑是否对于生产JVM的频繁full gc进行监控,例如某一时间段进行了频繁的full gc发送告警。
最后我是没整明白为什么netflix的这个metrics监控为什么会把url后面如果有拼接的参数就视为一个新的url.好歹你识别到url中有?切割下再识别也可以。而且是默认开启监控 - -!我吐了。
关注我的公众号 LearnMoreDoMore 可以及时看到更多技术分享文章(不会该死的卖课广告)。