跳到主要内容

09、JVM 调优实战 - 案例:每日百万交易的支付系统,如何设置JVM堆内存大小

1.支付系统背景

在一个电商系统里,一个支付系统如下图所示:

 

网购的大致流程是,在APP或网站上购买东西,一般是将一些商品添加到购物车里,然后下订单,接着对订单进行支付,钱从我们的账户划拨到网站的账户里去。

在上述的过程中,有个支付系统,它是网站或APP后台系统的核心环节,负责管理公司的资金流。

它负责对接用户支付的请求,然后根据用户的扣款方式,跟第三方的支付渠道对接起来,比如微信、支付宝,等等。

2. 支付的核心业务流程

 

上图的业务流程如下:

首先用户在商城系统提交支付一个订单的请求,接着商城系统把这个请求提交给支付系统,支付系统就会生成一个支付订单,此时订单状态可能是 “待支付” 的状态。

然后支付系统指引用户跳转到付款页面,选择一个付款方式。

然后用户发起实际支付请求,支付系统把实际支付请求转交给第三方支付渠道,比如微信或者支付宝,他们回去处理支付请求进行资金转移。

如果微信或者支付宝处理完支付之后,就会返回支付结果给支付系统,支付系统可以更新自己本地的支付订单的状态变成 “已完成” 。

一个完整的支付系统还包含很多东西,这里不去关注。重点关注支付流程。

3. 每日百万交易的支付系统的压力来源

假如基于该核心支付流程,系统每日要发生百万次交易。那么每次用户发起支付请求的时候,都会生成一个支付订单。

此时支付订单需要记录是谁发起支付?对哪个商品的支付?通过哪个渠道进行支付?还有发起支付的时间?等等,诸如此类的消息。

如果每日百万交易,在JVM的角度来看,就是每天会在JVM中创建上百万个支付订单对象

 

所以,支付系统,在系统架构层面会有包括:高并发访问、高性能处理请求、大量的支付订单数据需要存储,等等技术难点。

而在JVM层面,支付系统最大的压力是,每天JVM内存里会频繁的创建和销毁100万个支付订单,而这就牵扯到一个核心的问题。

  • 我们的支付系统需要部署多少台机器?
  • 每台机器需要多大的内存空间?
  • 每台机器上启动的JVM需要分配多大的堆内存空间?
  • 给JVM多大的内存空间才能保证可以支撑这么多的支付订单在内存里的创建,而不会导致内存不够直接崩溃?

4. 支付系统每秒钟需要处理多少笔支付订单?

假设每天100万个支付订单,而交易发生的高峰期一般为中午或者晚上,那么100万平均分配到几个小时里,那么大概是 每秒100笔订单左右

假设支付系统部署了 3台机器,每台机器实际上每秒大概处理了 30笔订单。

 

如上图,反映了支付系统每秒钟的订单处理压力。

5. 每个支付订单处理要耗时多久?

如果用户发起一次支付请求,那么支付需要在JVM中创建一个支付订单对象,填充进去数据,然后把这个支付订单写入数据库,还可能会处理一些其他的事情。

假设一次支付请求的处理,包含一个支付订单的创建,大概需要 1秒钟的时间。

那么一个整体的流程,是每台机器一秒钟接收到 30笔支付订单的请求,然后在 JVM的新生代里创建了 30个支付订单的对象,做了写入数据库等处理。

接着1秒之后,这30个支付订单就处理完毕,然后对这些支付订单对象的引用就回收了,这些订单在 JVM的新生代里就是没人引用的垃圾对象了。

接着再是下一秒来 30个支付订单,重复这个步骤。

6.每个支付订单大概需要多大的内存空间?

该内存空间,可以根据支付订单类中的实例变量的类型来计算。

比如,如下支付订单的代码中,只要记住一个Integer类型的变量数据是 4个字节,Long类型的变量数据是 8 个字节,还有别的类型的变量数据占据多少字节?从而计算出每个支付订单对象大致占据多少字节。

 

比如支付订单这种核心类,可以按照 20个实例变量来计算,大概一个对象就在 几百字节左右。往大了去算,可以算一个支付订单对象占据 500字节的内存空间,不到 1kb。

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

假设有3台机器,每秒处理30笔支付订单的请求,那么在这1秒内,肯定是有方法的局部变量在引用这些支付订单的,如下图:

 

那么30个支付订单,大概占据的内存空间是 30 * 500 字节 = 15000 字节,大概就是 15kb。

8. 让支付系统运行起来分析一下

整个支付系统运行的关键环节的数据,为每秒30个支付请求,创建 30个支付订单对象,最多占据 kb级别的内存空间。

然后接着 1秒之后,这30个对象就没有人引用了,就成了新生代里的垃圾了。

下一秒请求过来,系统持续去创建支付订单对象,不停在新生代里放入30个支付订单,然后新生代里的对象会持续的累积和增加。

直到发现新生代里都有几十万个对象,并且占据了几百MB的内存空间,可能导致新生代空间快满了。

然后就会触发 Minor GC,就把新生代里的垃圾对象都给回收掉了,腾出内存空间,然后继续来在内存里分配新的对象。

这就是这个业务系统的运行模型。

9. 对完整的支付系统内存占用需要进行预估

支付订单对象知识核心业务支付系统里的一小部分,真实的支付系统线上运行,肯定每秒会创建大量其他的对象。

结合这个访问压力以及核心对象的内存占据,大致可以估算出整个支付系统每秒钟大致会占据多少内存空间。

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

然后下一秒继续来新的请求创建大概1MB的对象放在新生代里,接着变成垃圾,再来下一秒。

循环多次之后,新生代里垃圾太多,就会触发Minor GC回收掉这些垃圾。这就是一个完整系统的大致JVM层面的内存使用模型。

10. 支付系统的JVM对内存应该怎么设置?

结合支付系统的核心业务流程分析,一般这种线上业务系统,常见的机器配置是2核4G,或者是4核8G。

如果用2核4G的机器来部署,那么刨去机器本身要用的内存空间,最后JVM进程最多就是2G内存。而这2G内存还得分配给方法区、栈内存、堆内存几块区域,那么堆内存最多可能只有1G多的内存空间。

而堆内存又分为新生代和老年代,老年代少说也要几百MB的内存空间,那么新生代可能也就几百MB的内存了。

结合上述业务流程,如果针对整个支付系统的预估,大致每秒会占据1MB左右的内存空间。

意味着,如果新生代只有几百MB的内存空间,就会导致运行几百秒之后,新生代内存空间就满了,从而触发Minor GC。

而频繁的触发 Minor GC,是会影响性能的。

因此至少考虑采用4核8G的机器来部署支付系统,那么JVM进程至少可以给4G以上内存,新生代至少可以分配到2G以上内存空间。

这样子可以做到新生代每秒1MB左右的内存时,需要将近半小时到1小时才会让新生代触发Minor GC,从而大大降低 GC 的频率。

JVM内存设置案例如下: 机器采用4核8G,然后-Xms和-Xmx设置为3G,给整个堆内存3G内存空间,-Xmn设置为2G,给新生代2G内存空间。

如果业务量更大,可以考虑横向扩展部署5台机器,或者10台机器,这样每台机器处理的请求更少,对JVM的压力更小。

11. 思考题

1、 Spring Boot 和 Tomcat部署系统怎么设置JVM参数?

答:Spring Boot在启动的时候可以加上JVM参数,Tomcat在bin目录下的 catalina.sh中可以加入 JVM 参数。

2、 如何简单计算一个对象?

答:一个对象大概包括,头(markword+kclass)reference 引用,基本类型。

在64位系统,默认开启压缩下,markword 8字节,kclass 4字节,reference 4字节。

基本类型 长度
boolean 1
byte 1
short 2
char 2
int 4
float 4
long 8
double 8

数组对象压缩后,对象头16字节,比普通对象头(8+4)多4字节,是多了一个记录长度的4字节。最后要不起padding,因为必须 hotspot 对象字节是8的倍数。