16_每日上亿请求流量电商系统案例
2209字约7分钟
2024-08-10
案例背景
这里以订单系统为例,我们来估算一下每日上亿请求量的电商系统,每日活跃用户量
一般按每个用户平均访问 20
次,上亿请求流量,大概 500
万日活用户,按照 10%
付费转化率来计算,每天大概 50
万订单
50w
订单按照每天 4
个小时高峰期计算,平均每秒钟大概几十个订单,几十个订单的压力下,根本不需要对 JVM
太多关注,几乎没什么压力
我们需要考虑的是电商大促这种特殊场景,比如双 11
零点的时候,很多人等着大促开始下单购物,那么此时每秒接近 1000
的下单请求,我们针对这个场景做优化
大促下订单系统内存使用模型分析
假设订单系统部署 3
台机器,每台服务器标配 4核8G
,从机器的 CPU
、内存资源角度来看,抗住每秒 300
个下单请求没有问题
问题在于对 JVM
有限的内存资源进行合理的分配和优化,包括对垃圾回收进行合理优化,让 JVM
的 GC
次数尽可能少,尽量避免 Full GC
,减少 JVM
的 GC
对高峰期系统的影响
处理下单请求涉及多个接口调用,是比较耗时的,基本每秒处理 100~300
个下单请求是差不多的
每个订单就按 1KB
来估算,300
个订单就是 300KB
的内存开销,算上订单库存、优惠券等业务对象,扩大个 20
倍,除了下单外,还会有查询之类的操作,再扩大 10
倍的量
每秒钟大概 60MB(300KB * 20 * 10)
的内存开销,可以认为一秒过后,这 60MB
就是垃圾对象了,因为订单处理完,相关对象都失去了引用,可以回收
JVM 内存分配
假设是 4核8G
的机器,JVM
的内存一般会给到 4G
,剩下几个 G
留给操作系统之类的使用,不要想着把机器内存一下子都耗尽
其中堆内存我们可以给到 3G
,新生代给 1.5G
,老年代也是 1.5G
每个线程的 Java虚拟机栈
有 1MB
,JVM
假如有几百个线程,那么大概会有几百兆,再给永久代 256MB
,这 4G
就用的差不多了,JVM
参数如下
-Xms3072M -Xmx3072M -Xmn1536M -XX:PermSize=256M -XX:MaxPermSize=256M -Xss1M
需要注意的是,-XX:-HandlePromotionFailure
参数在 JDK1.6
以后就废弃了,不用配置这个参数了。老年代可用空间 > 新生代对象总和、老年代可用空间 > 历次 Young GC
升入老年代对象平均大小,两个条件满足一个,就可以直接进行 Young GC
订单系统不停的运行,每秒处理 300
个订单,占据新生代 60MB
内存空间,一秒过后这 60MB
对象变成垃圾对象,-XX:SurvivorRatio=8
参数默认值是 8
,那么 Eden
区 1.2G
内存空间大概 20
秒就会被占满
然后进行 Young GC
,GC
后的存活对象大概 100MB
左右,放入 S1
区
再次运行 20
秒,把 Eden
区占满,再次垃圾回收 Eden
和 S1
中的对象,存活对象可能还是在 100MB
左右会进入 S2
区
新生代优化
Survivor 区空间够不够
在进行 JVM
优化的时候,第一个要考虑的是,通过估算,你的新生代 Survivor
区到底够不够
按照上面分析的,每次新生代垃圾回收在 100MB
左右,有时可能会突破 150MB
,那岂不是经常出现 Young GC
过后对象无法放入 Survivor
中,频繁让对象进入老年代?
还有,即使是 100MB
对象进入 Survivor
区,因为这是一批同龄对象,超过了 Survivor
区空间的 50%
,也可能导致对象进入老年代
按照我们的分析,Survivor
区域是明显不足的,像这种普通业务系统,大部分对象都是短生命周期的,对象根本不应该频繁进入老年代,也没必要给老年代维持过大内存空间
考虑把新生代调整为 2G
,老年代为 1G
,那么 Eden
区为 1.6G
,每个 Survivor
区为 200MB
,这样大大降低了新生代对象进入老年代的概率,JVM
参数如下
-Xms3072M -Xmx3072M -Xmn2048M -XX:PermSize=256M -XX:MaxPermSize=256M -Xss1M -XX:SurvivorRatio=8
新生代对象躲过多少次垃圾回收后进入老年代
按照我们分析的内存运行模型,基本 20
多秒触发一次 Young GC
,如果按照 -XX:MaxTenuringThreshold
参数默认的 15
次来说,连续躲过 15
次 GC,也就是一个对象在新生代停留超过了几分钟,这类对象进入老年代也是应该的
那我们有必要提高这个参数的值,比如增加到 20
次,或者 30
次呢?
这个需要结合系统的运行模型来思考,如果躲过 15
次 GC
都几分钟了,一个对象几分钟都不能被回收,说明肯定是系统里类似 @Controller
、@Service
之类注解标注的,需要长期存活的核心业务逻辑组件
这类对象一般很少,一个系统累计起来最多也就几十 MB
,应该进入老年代
所以这类情况,你提高 -XX:MaxTenuringThreshold
参数也没啥用,让这类对象在新生代里多停留几分钟?
其实这个参数我们甚至可以降低他的值,比如 -XX:MaxTenuringThreshold=5
降低到 5
次,在新生代停留超过 1
分钟了,尽快让他进入老年代,别在新生代里占着内存
多大对象直接进入老年代
一般来说,-XX:PretenureSizeThreshold=1M
设置个 1MB
足以,因为很少有超过 1MB
的大对象,如果有,那可能是你提前分配了一个大数组之类的东西
老年代优化
多久触发一次 Full GC
可能触发 Full GC
情况:
1、每次
Young GC
前,检查发现 老年代可用内存空间<
历次Young GC
后升入老年代的平均对象大小。按照目前分析模型来看,很多次Young GC
过后才有能可能一两次碰巧200MB
对象升入老年代,这个概率基本是很小的2、某次
Young GC
后升入老年代的对象有几百MB
,老年代可用空间不足了3、
-XX:CMSInitiatingOccupancyFaction
参数,比如设定值为92%
,比如老年代空间使用超过了92%
,自行触发Full GC
这些条件都需要老年代近乎占满的时候才会触发,很可能是系统运行半个小时到一个小时以后,才会有 1GB
的对象进入老年代
这个推论很重要,高峰期可能也就 1
小时才 1
次 Full GC
,高峰期一过,订单系统访问压力就很小了,可能要几个小时才一次 Full GC
老年代 GC 时会发生 Concurrent Mode Failure 吗?
经过前面分析,假设订单系统运行 1
小时之后,老年代大概有 900MB
对象,剩余可用空间 100MB
,此时触发一次 Full GC
在 CMS
垃圾回收的时候,尤其是并发清理期间,系统是可以并发运行的,万一有 200MB
对象进入老年代
这个时候就会触发 Concurrent Mode Failure
问题,老年代放不下这 200MB
对象,此时会导致立马进入 Stop the World
,然后切换 CMS
为 Serial Old
,直接停止系统工作线程,然后单线程进行老年代垃圾回收
这种情况发生概率比较小,暂时没有必要针对小概率时间特意优化参数
CMS 垃圾会之后内存碎片整理的频率应该多高?
CMS
完成 Full GC
之后,一般需要执行内存碎片的整理,可以设置多少次 Full GC
之后执行一次内存碎片整理
通过前面的分析,Full GC
可能也就 1
小时执行一次,然后高峰期过去之后,可能几个小时才会有一次 Full GC
保持默认的设置,每次 Full GC
之后都执行一次内存碎片整理就可以了
总结
对很多普通的 Java
系统而言,对系统运行期间内存使用模型做好预估,分配好合理内存空间,尽量让 Young GC
之后的存活对象都留在 Survivor
里不要去老年代,其余 GC
参数不做太多优化就可以了