Java后端问题排查经验

By | 2023年 12月 18日

线上出现问题首先应该做什么,不是解决问题,而是先恢复系统,把损失降到最小,有机会的话保留日志等数据用于后期问题复盘分析。解决问题可以后期慢慢复现排查,而线上用户的体验则不能多耽误一分一秒,任何线上问题的解决都应以用户为主。

出现问题的解决方法:重启,扩容,回滚,降级,限流

高并发高流量系统的设计处理:缓存,降级,限流

1. GC问题

  1. 收到预警后查看JVM监控,确定问题的时间点,内存,FGC频率,CPU使用率,线程数等。 如果频率规律,内存起伏正常,初步排除内存泄漏。
  2. 了解该时间点之前有没有上线,升级等操作,初步确定范围。
  3. 了解JVM的参数设置是否合理。
  4. 导出需要的堆栈文件,条件可以的话,也可以使用jmap,jstack等基本命令。
  5. 针对大对象或者长生命周期对象导致的FGC,可通过 jmap -histo 命令并结合dump堆内存文件作分析, 定位到可疑对象,通过可疑对象定位到具体代码再次分析。
  6. CPU使用率高 , top命令查询cpu使用率高的进程, top -Hp pid 定位进程中高使用率线程, jstack tid获 取栈信息,如果垃圾回收线程(VM Thread) ,说明GC导致cpu使用率高,问题在GC上,如果不是可以定位runnable中业务线程是否有死循环出现或者耗时计算(正常情况会出现我们工程相关代码)
  7. Full GC 次数过多,如果没有大对象产生,判断一下是不是有显示调用System.gc()的记录。
  8. Full GC 不多,CPU使用率不高,系统慢,分析jstack获取栈信息中如下关键字( Deadlock 死锁, waiting for monitor entry 等待获取锁,waiting on condition 等待某个资源或条件,in Object.wait),定位到具体代码行优化处理。
  9. 对于一些阻塞性的操作,这种操作不一定每次都能复现,可以通过压力测试的方式,放大阻塞效果, 定位阻塞点

2. 慢查询优化

  1. 先运行看看是否真的很慢,注意设置SQL_NO_CACHE
  2. where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高
  3. explain查看执行计划,是否与2预期一致(从锁定记录较少的表开始查询)
  4. order by limit 形式的sql语句让排序的表优先查
  5. 了解业务方使用场景
  6. 加索引时参照建索引的几大原则
  7. 观察结果,不符合预期继续从1分析

3. 内存使用率报警

3.1 内存泄漏问题-fastJson使用不当导致的内存泄漏

2020-05-31 20:40左右我这边收到任务服务告警:内存使用率超过75%

同时查看任务服务ump监控,tp99出现极值,高达17秒

紧接着查看JVM监控,发现在20:40触发了一次Full GC,而这次Full GC执行长达十几秒,故tp99出现极值是FullGC导致。

继续观察GC后堆内存使用情况,发现无论Young GC还是Full GC执行后仍让留有3个G的堆内存,而且每次都是堆内存即将打满后触发的GC,可以说GC执行的“很不顺利”。问题暴露后,为不影响接口性能及线上服务,暂时先将容器重启了,堆内存被重置。

持续观察线上服务内存

观察近期内存使用率情况(时间跨度为一个月),发现随着时间的推移,即使有GC,内存使用率依然逐步增高,早晚会触发下一次内存告警

观察近期GC情况,发现随着时间的推移,Yong GC后剩余堆内存在逐步递增,如下图

6月10左右,GC后剩余堆内存在300M左右

10天后,6月20日GC后剩余堆内存普遍在1.7个G,即随着时间推移GC后剩余的堆内存越来越多

问题定位

  1. 查看JVM参数配置

年轻代:Eden区(1879M) + S0(85M) + S1(84M) = 2048M

老年代:3072M

垃圾收集器用的是Java8默认的注重吞吐量的并行收集器

  1. 利用MAT工具查看堆内存对象使用情况

(a)首先找到java进程ID

(b)利用jmap命令,打印dump二进制日志

jmap -dump:format=b,file=/export/Logs/Domains/union-content- api.jd.local/server1/logs/dump.log

将dump日志输出到日志目录,再利用jdos的容器目录查询将日志下载下来(日志比较大,压缩后再下载)

(c)利用MAT工具查看堆内存使用情况

MAT(Memory Analyzer Tool),是一个java堆内存分析工具,可有效帮助我们排查内存泄漏问题

前面dump日志有4个G。。。 因日志过大,需要修改MAT的MemoryAnalyzer.ini配置文件:-Xmx5120m,否则无法分析

MAT工具导入dump日志分析后发现:fastJson的IdentityHashMap的Entry数组占用了绝大部分堆内存。

查看IdentityHashMap源码,是一个线性探测模式一个简单的Hash表,只不过他的key是由System.identityHashCode(key)生成,即一个对象对应一个唯一key且与对象的HashCode方法不同,无法被重写。

百度查fastJosn的IdentityHashMap内存泄漏相关资料,发现有很多类似问题

比如这篇文章:https://www.cnblogs.com/liqipeng/p/11665889.html

通过博客及源码发现,fastjson在处理泛型的反序列化时,ParserConfig类会反对序列化的目标类的泛型对象做缓存,而缓存容器正是IdentityHashMap,key则是反射包里的type对象.

说到fastjson处理泛型的反解析,一下子就想到任务服务的redis缓存优化,正是4月份上线的功能

下面这段代码是程序中触发内存泄漏的代码,它主要是将从redis取出的json字符串反解析成带泛型的java对象。

每次反解析都会实例化ParameterizedType对象,ParameterizedType继承自Type接口,

在JSONObject.parseObject方法中,ParameterizedType对象会被作为Map的key缓存在IdentityHashMap中,故随着这段代码调用次数的增加,越来越多的对象被实例化并被IdentityHashMap缓存起来而不被GC释放,故内存使用率越来越高。

参考https://github.com/alibaba/fastjson/issues/1418以及TypeReference的用法

修改为,或者将ParameterizedType对象做缓存也行

参考资料

【MAT工具使用介绍】https://blog.csdn.net/bohu83/article/details/51124060

【fastjson反序列化使用不当导致内存泄露】 https://www.cnblogs.com/liqipeng/p/11665889.html

【“com.alibaba.fastjson”遇到的内存泄漏问题】https://www.jianshu.com/p/adfde1a318b0

【GitHub-Issue:parseObject是否存在内存泄漏情况】https://github.com/alibaba/fastjson/issues/1418

【GitHub-Wiki:TypeReference使用】https://github.com/alibaba/fastjson/wiki/TypeReference

netty在recyler使用FastThreadLocalThread内存溢出

由于内存的 allocate 和 release 不再同一个线程,造成的内存泄漏。

https://www.cnblogs.com/405845829qq/p/6255931.html

3.2 内存泄漏问题-从ThreadLocalMap分析ThreadLocal使用不当导致内存泄漏

首先我们先看看ThreadLocalMap的类图,在前面的介绍中,我们知道ThreadLocal只是一个工具类,他为用户提供get、set、remove接口操作实际存放本地变量的threadLocals(调用线程的成员变量),也知道threadLocals是一个ThreadLocalMap类型的变量,下面我们来看看ThreadLocalMap这个类。在此之前,我们回忆一下Java中的四种引用类型,相关GC只是参考前面系列的文章(JVM相关)

①强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。

②软引用:简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中

③弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null

④虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)

分析ThreadLocalMap内部实现

上面我们知道ThreadLocalMap内部实际上是一个Entry数组,我们先看看Entry的这个内部类

当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

总结:THreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注