写点什么

Java 后端问题排查经验

作者:WizInfo
  • 2023-12-18
    内蒙古
  • 本文字数:3731 字

    阅读完需:约 12 分钟

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

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

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

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 <pid>

将 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 方法避免内存泄漏。


发布于: 刚刚阅读数: 4
用户头像

WizInfo

关注

还未添加个人签名 2020-04-06 加入

还未添加个人简介

评论

发布
暂无评论
Java后端问题排查经验_WizInfo_InfoQ写作社区