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 参数不做太多优化就可以了