简单介绍,ydata-server该应用主要的功能是,接收上游kafka的数据,然后进行序列化、归类,再发送到kafka中。部署基于Java8的版本。
优化前
内存的占用
4G的容器内存,压测到了3.7G上下,已经达到了90%以上的使用率。
垃圾回收情况
压测1个多小时,发生了4次的Full GC。吞吐量看着还好。。
各项配置堆内存参数:
JVM虚拟机,除了堆内存之外,还有虚拟机栈、方法区、直接内存、容器内存等。所以将容器的所有内存全部分配给堆空间,可能会导致容器内存使用率超出限制。
也确实监测到了超出容器内存限制的情况。。。
第一次优化
优化思路
ydata-server的所有功能,只是简单的消费数据,序列化等等,无使用直接内存的逻辑。
可以这么理解,ydata-server所有的功能,都在堆内存上进行。
而堆空间的分配,新生代占比为1/3,老年代占比为2/3,但是在观测垃圾回收的数据,老年代并没有发生很多次的垃圾回收,而且占用内存很少。
很显然,高频使用的新生代内存,仅占堆内存的1/3,而占2/3的老年代内存,却长期闲置,造成内存使用率过高。
所以,可以适当提高新生代内存的比例。通过-XX:NewSize
固定新生代内存的大小。
在观测垃圾回收的数据时,还发现了ydata-server发生了4次的Full GC,虽然4次 Full GC,任然是一个很小的数值,但是发生的原因,值得深入分析。
ydata-server主要的逻辑,是从kafka接收数据,并序列化等等,这些操作很快,而且操作完,相关的对象就应该被回收。理论上,ydata-server应该仅仅会引起新生代的垃圾回收,不会引起老年代的垃圾回收(Parallel 的垃圾回收器,老年代进行并行化的Full GC)。
那是什么原因引起了老年代的垃圾回收呢?
直接原因是,老年代的对象发生了堆积。(废话)
那是什么原因,造成了老年代的对象发生了堆积呢?
下图是对象的一般分配过程,可以看到,当S区的内存不足以放下Eden区存活的对象时,会直接放进老年代
再看一下之前的垃圾回收情况,S区的大小仅为3M,也就是说,如果有一批3M左右的数据在Eden区存活时,会直接进入老年代,从而造成老年代的堆积。
ydata-server还提供一定的rest接口,而如果接口返回的数据在3M左右的话,那么这些数据可能会直接进入老年代。
对前端页面的接口进行查看,发现确实有一些接口,返回的数据比较大。
对其重新发送请求,并对堆内存进行实时监控,最终发现,老年代内存轻微上涨。 积少成多,多次的页面查询,造成老年代的堆积,但总体上页面查询不频繁(还处于测试阶段),所以垃圾回收也仅有几次。
为此,可以加大堆内存中S区的大小,使其能够充分发挥作用。S区的大小比例,为新生代的2/10,但是Parallel会自动调整S区的大小,以达到最大的系统吞吐量,调着调着,S区就越来越小了。。。可以通过禁用自动调整(-XX:-UseAdaptiveSizePolicy)或者使用CMS GC解决。
优化参数
-Xmx2g-Xms2g-XX:NewSize=1g-XX:-UseAdaptiveSizePolicy
内存占用
内存占用稳定在了1.4G上下,较之前下降2G。内存占用率为35%。
垃圾回收情况
// 现场的截图已经找不到了,只能文字描述
仍然发生了3次的Full GC ,但这3次的Full GC,是在压测前就已经发生了。在压测的过程中,再也没有发送过Full GC。
新生代垃圾回收频繁,基本上每秒就会有一次Young GC,每次耗时在130毫秒上下,也就是说,JVM垃圾回收的吞吐量在87%上下,并不是十分优秀。
新生代垃圾回收频繁,没办法了,数据量过来就这么多,所以,只能够增大新生代的内存大小,而老年代,内存稳定,不用再进行增加。
第二次优化
优化思路
启动后垃圾回收的情况:
可以看到,进行了3次的Full GC ,但是老年代的占用很小。所以,应该是其他地方触发了Full GC。
除了堆内存之外,当方法区不够,也会触发Full GC 。而在刚启动时,方法区内存太小,会进行扩容,在扩容时,就发生了Full GC。
所以,启动时的3次Full GC,可能是元空间水位线上涨引起的。
可以通过指定元空间的大小,进行规避,如-XX:MetaspaceSize=256m
,Java8之前为设置永久代相关的参数。
由于-Xmx2g内存太小,导致频繁的 Young GC,所以,适当调大内存,使JVM吞吐量达到95%上下。当然如果内存大小的优先级比较高,也可以适当牺牲吞吐量,来换取更小的内存占用。
Parallel Old GC在老年代满时,势必会引起Full GC,为了避免一些考虑不到位的情况导致的Full GC,所以,应该尽量让老年代的内存维持在一个较低的值。可以使用 CMS GC,通过他的Majar GC的机制,让老年代的内存占用保持在一个较低的值。
优化参数
-Xmx3g-Xms3g-XX:NewSize=2g-XX:+UseConcMarkSweepGC-XX:MetaspaceSize=256m
内存占用
内存占用较之前-Xmx2g的设置,略有上升,但仍然保持在了60%上下,且非常稳定。
垃圾回收
经过长时间的压测,在也没有看到Full GC,并且新生代的垃圾回收次数和耗时都明显减少,JVM吞吐量已经达到了95%以上。
总结
1、需要明确调优的目的,如吞吐量,内存占用
2、调优的前提是监控,在此次调优的过程中,使用到了java自带的jstat命令,k8s中部署的普罗米修斯。并且对内存、CPU、网络流量(预估从Kafka接收到的数据规模)、JVM垃圾回收等进行监控。需要熟练使用相关的监控工具。
3、需要了解各Java版本默认的垃圾回收器,及其原理和特点,了解对应GC的相关参数
4、需要对业务功能和代码有一定的理解阅读能力,如果都不知道这个服务是干什么的,那根本无从上手。必要时要阅读项目源码,了解其实现的相关细节。