案例背景

image.png
在日常在某宝买东西支付完成之后都会有一个订单产生,在实际的处理流程中是分为生成订单、进行支付两件事情来做。

支付核心业务流程

image.png

分析支付系统中的压力都在哪

在核心业务流程中,可以看出每次完成一单支付的时候首先都要创建一个订单对象,然后再将这个订单放入数据库中。那么当每天有百万个请求过来,是不是就会存在百万个订单对象在jvm中做停留。因此,支付系统最大的压力,就是每天JVM内存里会频繁的创建和销毁百万个订单对象。
image.png
在这里需要考虑一下几个问题
我们的支付系统需要部署多少台机器?
每台机器需要多大的内存空间?
每台机器上启动的JVM需要分配多大的堆内存空间?
给JVM多大的内存空间才能保证可以支撑这么多的支付订单在内存里的创建,而不会导致内存不够直接崩溃?

堆内存的使用分析&设置

支付系统中每秒要处理多少笔订单

假设每天的订单量达到100万,按高峰期的几个小时来平均一下,那么每秒也就要处理大概100笔的订单。再如果我们使用了3台服务器做处理,那么每台机器实际处理也就是在30笔左右。
image.png

单个订单处理需要耗时多久

所谓处理,就是指订单对象从new到数据库insert完这么个过程,假如这个过程是1秒钟。那么也就是说,每台机器一秒钟接收到30笔订单,也就是在jvm的新生代中创建了30个订单对象。随后,经过1秒钟,这30个对象做完处理后就不再被引用到了,这些对象就会变为垃圾对象。

每个订单需要多大的内存空间

按每个对象中每个变量的大小的总和来进行计算即可,例如图中的这个订单也就占十几个字节,姑且算大一点,扩大十倍也不过几百个字节,达不到1kb
image.png

每秒发起的支付请求对内存的占用

按照之前设定的,每台服务器每秒要处理30个订单,30*500(几百字节) = 15000字节,也就15kb多也是很小的
image.png

实际运行时状态

业务系统的运行模型:每秒有30个订单对象在jvm中创建,下一秒后对象不再被使用了,那么就会存在30个垃圾对象。往复循环之后,直到有一刻,存在了几十万个对象,新生代空间可能快要满了,那么这时候就会触发Minor GC,也就把新生代中的垃圾对象都回收掉了。

堆内存如何设置

支付系统内存占用预估

可以把之前的计算结果扩大10倍~20倍。也就是说,每秒钟除了在内存里创建支付订单对象,还会创
建其他数十种对象。
那么每秒钟创建出来的被栈内存的局部变量引用的对象大致占据的内存空间就在几百KB~1MB之间。

设置内存

常见的机器配置是2核4G,或者是4核8G。
如果选用2核4G,就会比较紧凑,4g中还有机器本身运行的时候要占一部分,算下来jvm最多可使用的也就是2G,然后这2G还得分配给方法区、栈内存、堆内存几块区域,那么堆内存可能最多就是个1G多的内存空间。而且堆内存中还分年轻代和老年代,平摊下来使用最平凡的年轻代区域就没有多少内存可以用了。
但是实际上如果扩大10倍~20倍换成对完整系统的预估之后,我们看到,大致每秒会占据1MB左右的内存空间。
如果新生代只有几百MB,那么运行几百秒年轻代就满了,要触发GC了,按照这种情况GC的频率太高,会导致整个系统的吞吐量会降低,影响线上系统的稳定性。
因此,可以考虑采用4核8G的机器来部署支付系统,那么你的JVM进程至少可以给4G以上内存,新生代在里面至少可以分配到2G内存空间这样子就可以做到可能新生代每秒多1MB左右的内存,但是需要将近半小时到1小时才会让新生代触发Minor GC,这就大大降低了GC的频率。
举个例子:机器采用4核8G,然后-Xms和-Xmx设置为3G,给整个堆内存3G内存空间,-Xmn设置为2G,给新生代2G内存空间。

总结

优化和分析遵循下面流程
1.在不同的机器配置下,新生代大致会需要多大内存。一定要做全局预估,预估范围大概是你推测的10-20倍
2.不同新生代大小下,多久会执行一次MinorGC
3.为了增加程序吞吐量,应该选用怎么样的服务器配置,应用几台服务器,并且设置多大的堆内存和新生代占比。

栈内存和永久代(1.8为元空间)分析&设置

永久代(元空间):一般永久代刚开始上线一个系统,没太多可以参考的规范,但是一般你设置个几百MB,大体上都是够用的
因为里面主要就是存放一些类的信息。
栈内存:一般默认就是比如512KB到1MB,就差不多够了。
这就是每个线程自己的栈内存空间,用来存放线程执行方法期间的各种布局变量的。

反面线上案例情况

大促期间,瞬时访问量增加十倍

一般搞大促活动,很可能导致你的压力瞬间增大10倍,因为平时不来你网站的人,今天都来了。
此时可能会发现,每秒钟你的支付系统不是100笔订单了,可能是每秒钟上千笔订单。
这个时候你的系统压力本身就会很大了,不光是内存,尤其是线程资源、CPU资源,都会几乎打满。内存就更是岌岌可危了。

少数请求需要几十秒处理,导致老年代内存占用变大

当有些请求处理响应很慢(突发情况导致压力剧增,性能下降),GC的时候并不会一次就把这些对象给回收了,如果仍有对象放入年轻代的时候空间不足也会触发GC。反复经历了许多次GC,那么这些请求慢对象由于经历了很多轮GC都没被回收,这些对象就被移动进了老年代,导致老年代的空间迅速被填满。
当老年代被填满的时候,就会触发老年代的GC,而且这个情况发生的频率还是很快的,就会频繁触发老年代的GC(老年代的GC是很慢的)。