这两节课主要讲的是JVM的问题排查和调优。我将结合本身遇到实际的案例和本节课的知识点去总结如何排查项目。本文将从个人实际遇到过的实际问题,从定位问题,问题解决,和最后如何进行压力测试。
问题案例一
项目简介,这个项目是我之前在银行写的一个分布式项目,这是一个实时的任务,作用是从Kafka对列里面拉取数据交易,然后对符合要求的交易调用下游接口,对不符合,或者异常的信息记录到Mysql里。项目是高可用的,同时部署两个,但是同一时间只有一个运行(通过设置group内,consumer数量不能大于partition数),而且当一个停掉时,另外一个自动启动。而项目中使用了一个线程池来执行pull操作。
抽取的伪代码如下:
然后忽然发现原来的项目不再从队列中拉取数据,而且CPU占用很高。下面就讲讲如何排查。
问题定位排查
一般来说,在银行的企业对生产环境要求是很高的,不允许带第三方工具进行调试。所以只能使用命令行。
通过top命令定位问题
1,top 命令是查看Linux的进程问题,定位到是否Java进程耗尽了cpu。
2,执行 top -p + 进程进行单独监控线程
3,在输入监控界面输入H,获取线程信息
4,找到消耗最高的线程
5,对进程进行dump,执行jstack,输出所有线程信息
6,将第 4 步得到的线程编号 11354 转成 16 进制是 0x7b
7, 根据第 6 步得到的 0x7b 在第 5 步的线程信息里面去找对应线程内容
8, 解读线程信息,定位具体代码位置
Jstat(代码中有打印 GC 参数,生产上可以使用这个 jstat –gc 来统计,达到类似的效果)
使用 jmap -histo 查看占用空间的情况,包括什么类占用了多少空间,而可以加上 | head 20 展示前20条
那么可以看到是我们的业务线程生成了大量的对象,并且定位到了具体的类。在上面的这些命令使用中是通过定位进程(top),线程(top h),使用(jstack)对进程进行dump,查看线程信息,再根据线程信息定位到哪个对象对CPU的消耗是最高的。
问题原因(找到问题)
其实出现问题的根本原因就是任务数多于线程数,造成了不断的排队,而队列过长了,占用了很多的内存,GC也处理不了,因为这些都是待处理的任务,暂时不能被回收,这就是典型的内存溢出。而造成这个线上原因是测试环境的测试没考虑到这种问题。所以需要进行压测处理。
问题案例二
秒杀场景下的短时间高并发场景。这种场景一般来说比较少出现,而且就算出现也是短时间内的达到瞬时的如果在金融行业,银行中一般就是双十一。这种情况下,如果企业资金何时,其实是可以通过扩容机器,物理上解决的。但是如果预算也可以通过建议通过物理解决。下面就通过一个简单的demo实现GC调优。
压测工具
Ab(ApacheBench) 测试工具是 Apache 提供的一款测试工具,在测试 Web 服务时非常实用,ab 一般都是在 Linux 上用。我们可以写一个测试的类。
分别使用下面的命令进行压测
-
10 个并发用户/10 万请求量(总)
-
100 个并发用户/10 万请求量(总)
-
1000 个并发用户/10万请求量(总) ab -c 10 -n 100000
http://127.0.0.1:8080/jvm/heap ab -c 100 -n 100000
http://127.0.0.1:8080/jvm/heap ab -c 1000 -n 100000
http://127.0.0.1:8080/jvm/heap
打开GC监控
jstat-gc 8404 5000 20 | awk ‘{print $13,$14,$15,$16,$17}’
压测结果如下:
- 用户的吞吐量大于在 1262/每秒左右
- JVM 服务器平均请求处理时间 0.8ms 左右
- JVM 服务器发生了 2700 多次 YGC,耗时 30 秒 ,还有 56 次 FGC,3 秒左右,加在一起 GC 耗时 33 秒
方案一
调整堆内存空间减少 GC:通过分析,堆内存基本被用完了,而且存在大量 MinorGC 和 FullGC,这意味着我们的堆内存严重不足,这个时候我们需要调 大堆内存空间。 堆空间加大到 1.5G
java -jar -Xms1500m -Xmx1500m jvm-1.0-SNAPSHOT.jar
使用 AB 进行压力测试: ab -c 10 -n 100000 http://127.0.0.1:8080/jvm/heap
方案二
调整内存的新生代和老年代的比例来减少youngGC的次数从而减少STW。
结果如下:
结论如下:在高并发的场景业务中,需要一个大的堆空间,但是不能仅仅调大堆的空间,而是要通过调整新生代和老年代的比例,还有From区和To区的比例,这样才能完好地减少大量对象从新生代到老年代中去。而方案一仅仅是调大了堆空间,这样虽然减少了GC的次数,但是单次GC的扫描会增加很多时间。所以有点得不偿失。而通过调整新生代和From缓冲区的大小可以使得新生对象在新生代被回收而不是被移动到下一个区域。
调优策略
- 降低Minor GC 频率,如果是高响应的秒杀场景,可以通过调整新生代的大小,容纳更多的临时对象,从而减少新生代到老年代的数据从而减少Minor GC 的次数。
- 降低Full GC 频率,如果堆内空间不足或者大对象过多会撑爆老年区从而除非FulGC ,所以代码中尽量减少大对象的创建,例如数据库对象获取,或者调整堆空间的大小。
- 选择合适的回收器,如果内存合适 >8G的可以选择G1回收器。
总结
- 在遇到这种性能问题,先从代码问题入手,看能不能优化
- 通过硬件解决,调大机器集群性能,或者“临时”开挂
- 调优参数,在成本,吞吐量,延时之中寻找一个平衡点。