亿级流量电商系统JVM模型参数二次优化

简介: 亿级流量电商系统JVM模型参数二次优化

亿级流量电商系统JVM模型参数预估方案,在原来的基础上采用ParNew+CMS垃圾收集器


一、亿级流量分析及jvm参数设置



1. 需求分析


大促在即,拥有亿级流量的电商平台开发了一个订单系统,我们应该如何来预估其并发量?如何根据并发量来合理配置JVM参数呢?


假设,现在有一个场景,一个电商平台,比如京东,需要承担每天上亿的流量。现在开发了一个订单系统,那么这个订单系统每秒的并发量是多少呢?我们应该如何分配其内存空间呢?先来分析一下

1187916-20211013172629244-425517351.png


每日亿级流量,平均一个用户点击量在20-30左右,通过这个计算出日活用户数约1亿/20=500万, 看的人多,买的人少,通常下单率不超过10%,我们按照留存率10%来计算,日均订单约50万单。这是分两种情况:


  • 一种是普通流量,非特殊节假日,通常早上、中午、晚上非工作时间有1个小时的时间集中购买。我们按照早上1小时,中午1小时,晚上1小时来计算,也就是3小时。这样平均到每秒就是50万/3/3600=46, 也就是及时并发,通常我们的服务都是一个集群,有好几台服务器承受着几十并发,应该不成问题。
  • 另一种是大促流量,比如双十一,基本流量都集中在双十一当天的投几分钟。这时每秒的并发量大概在50万/10/60=866,平均每秒并发量不到1000。这时服务集群有3台服务器,没太服务器承受的压力是400单/s。


2. 常规方案及问题暴露


对于这每秒400但会产生多大的对象呢?


1187916-20211014144905050-2120301001.png

我们假设订单对象的大小是1kb,实际上订单对象的大小和订单对象中的字段有关系,我们假设是1kb。每秒400单,也就是会产生400kb的订单对象。下单还涉及到其他对象,比如库存,优惠券,积分等等,我们将对象扩大20倍, 大约是(400kb*20)/秒. 可能同时还有其他操作,比如查询订单的操作,我们再讲其扩大10倍,大约是80M,也就是每秒产生约80M的对象,这些对象在1s后都会变为垃圾。


对于一台4核8G的服务器来说,通常我们不设置JVM参数,也可能会根据物理机的8G内存来设置JVM参数。如果根据JVM参数来设置参数如何设置呢?


之前说过开启逃逸分析会将对象分配到栈上,我们这里计算分析的时候暂且忽略逃逸分析分配到栈上的对象,因为这部分对象相对来说比较少。下面我们来验证上面的预估算法是否准确,会有什么样的问题呢?


物理机有8G,分给os操作系统3G,分给JVM5G,然后JVM中给堆分配3G,元数据空间分配512M,线程栈分配1M等等。这是估算,不够精细,到底分配这么多空间够不够呢,会不会浪费呢?会产生什么样的问题呢?

设置jvm参数大致如下:
-Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M


这样设置到底行不行呢?有没有问题呢?我们来看看运行时数据区:


1187916-20211014103515878-832225855.png

根据计算


  • 整个堆空间3G
  • Eden区800M
  • s1/s2各100M
  • 方法区512M
  • 一个线程1M


按照这个模型来分析,得到如下结果:


1187916-20211014103819135-1358779405.png

  1. 大促期间1s产生80M的对象数据。我们知道对象数据都是放在Eden园区,Eden园区一共800M,那么大约10s就放满了,放满了就会触发Minor GC
  2. 触发Minor GC的期间,会Stop The World暂停业务线程。在第10s触发MinorGC的时候,前9s的720M数据都已经变成垃圾了,会被回收掉,最后1s的80M数据由于还有对象引用,只是暂停了业务线程,因此不是垃圾,不能被回收。会被放入S1区。
  3. 在Survivor区有一个对象动态年龄判断机制。什么是对象动态年龄判断机制呢?


当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,

例如:Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。

对象动态年龄判断机制一般是在minor gc之后触发的。


也就是说当在Survivor区经过几代的回收以后,如果对象总和大于Survivor区域的一半,则会直接放入到老年代。Survivor是100M,第10s的对象是80M,大于100M,会直接将这个对象放入到老年代。

1187916-20211014110244477-1977458166.png


  1. 老年代一共有2G空间,2G空间执行多少次会满呢?2G/80M=25次,也就是发生25次(25秒)Minor GC就会触发一次Full GC。这个频率就太高了,通常应该要很少触发Full GC,起码也得1个小时触发一次。而触发的原因是因为垃圾对象(这些对象1s后都变成垃圾了),这样肯定是不行的。我们需要优化JVM参数。


3. JVM优化


有问题有就解决问题。问题的根本原因是老年代发生了Full GC,为什么会发生Full GC呢?


之所以80M对象会放到了老年代是因为每秒产生的数据 大于 Survivor区空间的一半。所以,我们可以调整Survivor区大小。通常我们不会修改默认的Eden:S1:S2的比例,所以,我们可以考虑从整体扩大新生代的内存空间。假设我们扩大到2G,让老年代是1G。


1187916-20211014111234027-960948486.png

这时会怎么样呢?


  • Young区占2G,Eden区有1.6G, S1、S2各有200M。


这时在分析:

1187916-20211014142616963-1685610883.png



  • Eden区有1.6G,每秒产生80M的对象放到Eden区,大约1.6G/80=20s放满。
  • 放满以后触发Minor GC, 此时前19s的对象都已经成为垃圾被回收,第20s的对象被转移到S1区。
  • 此时,S1区有200M,80<S1区空间的一半,所以不会转移到老年代。这样第一次GC结束
  • 又过了20s,进行第二次Minor GC,这次Eden区又产生了1.52G的垃圾被回收,之前在S1区的80M对象也已经变成垃圾被回收。新的80M对象被放入到S2区。没有进入到老年代。
  • 以此类推,第三次,第四次,垃圾对象不会再进入老年代,因此也不会在发生Full GC.

由此分析,大大降低了Full GC发生的频率。


最终参数设置:


-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M 
为了更清晰的看到效果,可以打印GC详细日志
-XX:+PrintGCDetails

4. 总结


通过上面的数据分析,我们要养成一个习惯,做任何事情都是要有理有据,不能是拍脑袋就说出来的。一定要能够经得起验证的。


二、亿级流量jvm参数优化--使用parNew和CMS垃圾收集器



1. 需求分析


上面的参数设置,帮我们解决了多次触发Full GC的问题,通过调整参数以后,我们看出在预期正常情况下,基本不会触发Full GC。但如果有意外情况呢?比如,我们的一台服务器能够承受的最大并发量是400/s,但如果在秒杀的时候,并发量超过了这种情况是在不发生意外的情况下。假如并发流量达到1000,内存模型是怎么样的呢?


1187916-20211022142447362-1233225660.png


根据这个估算模型,正常情况下订单系统可以承接的订单并发量是400单/s,但遇到某一个大促活动,很可能并发量冲到700单/s, 1000单/s,这是一秒产生的垃圾就不是60M了,可能是120M,甚至更多。根据之前的分析,这时又会频繁的触发Full GC了。当然了,我们有很多办法来控制并发量,比如限流、扩容。但这里我们从JVM的角度来分析,如何处理这个问题。


正常情况我们的jvm参数是如下设置:


‐Xms3072M ‐Xmx3072M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8

经过上面的分析,这样设置可能会由于动态对象年龄判断原则导致频繁full gc。于是我们设置如下JVM参数,尽量避免触发full GC


‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8

2. JVM优化


这个原理在上面已经说过了,但是如果并发量从峰值400单/s,一下冲到700~1000单/s。这时候,很显然,又会触发Full GC了,因为内存对象从原来的80M,变成了160M甚至更多,Survior区200M空间,他的一半小于160M, 所以会直接放入到老年代。针对这个问题,我们来做参数优化。


优化一:分代年龄从15变成5


系统默认的分代年龄是15,也就是一个对象在Survivor两个区轮回15次才会进入到老年代。15次大概是多长时间呢?我们来计算一下,按照参数来分析一下内存模型,如下图:


‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8


1187916-20211024081434845-858940078.png


每秒钟产生80M垃圾,放入到Eden区,Eden区一共1.6G,预计20s放满,触发Minor GC, 然后大部分对象被回收,只有一小部分对象进入到Survivor区。第二次回收的时候,上次进入Survivor区的大部分对象被垃圾回收,另一部分进入到另一个Survivor区。这些进入到另一个Survivor的对象要经历15次Minor GC,也就是年龄是15的时候,被转移到老年代,花费大约20s*15约5分钟的时间才能进入到老年代。其实这些长期存活的对象都是java运行或者spring运行是的一些java.lang.String, java.util.Math, 和一些bean对象。既然这些对象本身是长期存活的,那么我们就没必要让他经历那么多代才进入到老年代。

我们完全可以将默认的15岁改小一点,比如改为5,那么意味着对象要经过5次minor gc才会进入老年代,如果经历5次Minor GC还没有被回收,我们完全可以认为她就是要长期存活的对象了,将其移动到老年代,而不是继续一直占用survivor区空间。整个过程时间不到两分钟。


设置参数如下:


‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8  
‐XX:MaxTenuringThreshold=5

优化二:大对象直接进入老年代


对于多大的对象直接进入老年代合适呢?这个一般可以结合你自己系统看下有没有什么大对象生成,预估下大对象的大小,一般来说设置为1M就差不多了,很少有超过1M的大对象,这些对象一般就是你系统初始化分配的缓存对象,比如大的缓存List,Map之类的对象。 设置大对象直接进入老年代使用的参数:-XX:PretenureSizeThreshold

参数设置如下:

‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8  
‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M

优化三:替换垃圾收集器为ParNew + CMS


JDK8默认使用的垃圾回收器是-XX:+UseParallelGC(年轻代)和-XX:+UseParallelOldGC(老年代),通常使用Parallel会有什么问题呢?经验告诉我们,当系统内存较大的时候(超过4G,经验值),系统对停顿时间是比较敏感的。 通常大于4G内存,我们可以采用ParNew + CMS垃圾收集器。可不可以使用G1收集器呢?G1收集器通常是内存大于8G时使用的。 内存小于8G时,在jdk8中G1收集器的算法耗费的内存要比CMS多。所以这里我们替换垃圾收集器为ParNew + CMS。设置使用ParNew + CMS的参数是:-

XX:+UseParNewGC -XX:+UseConcMarkSweepGC


经验: 很多使用jdk8的公司都是用时ParNew + CMS垃圾回收


参数设置如下:


‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8  
‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC

替换成ParNew + CMS垃圾收集器能解决上面并发流量达到700~1000单/s的问题么?我们来分析一下:


1) 当并发流量导到700单/s的时候, 原来每秒产生80M垃圾,现在可能达到160M,那么年轻代Survivor放不下,会直接放入到老年代。


2)当兵发流量大了的时候,本来系统能承受的是400单/s, 但是突增到500单/s的时候,原来每秒可以处理一个订单,现在可能1秒处理不完了,要2秒甚至更多。那么就有可能在垃圾回收的时候,2s内的对象的引用关系都还在,不能被回收,刚好又大于新生代一半的空间,也会被直接放入老年代。


3)经过上面的优化,发生一次Minor GC,大约要20s, 老年代有1G空间,

1G/160M*20/60=2分钟。2分钟触发一次GC,通常高峰流量也就半个小时左右。2分钟触发一次GC,这也不太合适。


优化四:设置CMS收集器的参数


1) 避免并发失败参数设置


在CMS收集器那块我们说过,CMS正在收集垃圾但还没有完成的时候,又产生了新的垃圾,导致再次触发垃圾回收,这就发生死循环了,这就是concurrentmode failure并发失败。为了避免并发失败,这时会停止CMS垃圾回收的全部线程,进入到Serial Old串行垃圾收集。串行速度是很慢的,严重影响用户体验。我们尽量不要让这种情况发生。因此,我们设置垃圾回收参数:‐XX:CMSInitiatingOccupancyFraction,我们设置老年代达到一定比例比如80%就出发Full GC,留出足够大的空间给大对象,这样就不会触发Serial Old了。


这个值默认是92,也可以设置成80,但设置成80就表示,剩下20%的内存空间正常情况下处于闲置了。


参数设置如下:


‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8  
‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
‐XX:CMSInitiatingOccupancyFraction=92

2)压缩整理参数设置


我们可以设置在发生Full GC之后进行内存空间的压缩整理。这里涉及到两个参数,一个是开启压缩整理,另一个是触发几次Full GC整理一次内存空间。


-XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片) 
-XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
  这个参数是说执行多少次Full GC以后进行一次压缩。如果其值是3,则表示执行3次Full GC,进行一次压缩整理。

在触发了CMS垃圾回收之后,进行内存整理,也会对性能有一定的影响的。 因为他也会STW。这个过程不会特别慢,这和剩余的对象有关,剩余的对象少,效率就高。剩余的对象多,效率就低。因为在整理的过程中,对象的地址会发生变化。


对于我们上面的案例,我们可以设置每次垃圾回收后都进行整理,为什么可以这么设置呢?因为我们full GC发生的频率很低。偶尔搞一次大促呢?也没关系,大促的前面二三十分钟流量最高,二三十分钟触发一次Full GC没关系的,因为大促基本结束了。

如果系统压力比较大,触发Full GC很频繁,这个参数就不要这么设置了。可以设置-XX:CMSFullGCsBeforeCompaction为3次,5次。


不做碎片整理可不可以呢?


最好不要,因为如果不做碎片整理,老年代的碎片就会越来越多,正常的大对象都放不下了。


参数设置如下:


‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8  
‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
‐XX:CMSInitiatingOccupancyFraction=92 ‐XX:+UseCMSCompactAtFullCollection ‐XX:CMSFullGCsBeforeCompaction=0


相关文章
|
2月前
|
安全 Java 应用服务中间件
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
什么是类加载器,类加载器有哪些;什么是双亲委派模型,JVM为什么采用双亲委派机制,打破双亲委派机制;类装载的执行过程
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
|
28天前
|
监控 架构师 Java
JVM进阶调优系列(6)一文详解JVM参数与大厂实战调优模板推荐
本文详述了JVM参数的分类及使用方法,包括标准参数、非标准参数和不稳定参数的定义及其应用场景。特别介绍了JVM调优中的关键参数,如堆内存、垃圾回收器和GC日志等配置,并提供了大厂生产环境中常用的调优模板,帮助开发者优化Java应用程序的性能。
|
1月前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
43 3
|
1月前
|
存储 算法 Java
深入理解Java虚拟机(JVM)及其优化策略
【10月更文挑战第10天】深入理解Java虚拟机(JVM)及其优化策略
41 1
|
1月前
|
Java Android开发 开发者
【编程进阶知识】精细调控:掌握Eclipse JVM参数配置的艺术
本文详细介绍了如何在Eclipse中配置JVM参数,包括内存的初始和最大值设置。通过具体步骤和截图演示,帮助开发者掌握JVM参数的精细调控,以适应不同的开发和测试需求。
41 1
|
1月前
|
Java
【JVM】双亲委派模型
【JVM】双亲委派模型
20 1
|
1月前
|
监控 Java
Java的JVM如何优化?
Java的JVM如何优化?
55 3
|
3月前
|
C# 开发者 Windows
震撼发布:全面解析WPF中的打印功能——从基础设置到高级定制,带你一步步实现直接打印文档的完整流程,让你的WPF应用程序瞬间升级,掌握这一技能,轻松应对各种打印需求,彻底告别打印难题!
【8月更文挑战第31天】打印功能在许多WPF应用中不可或缺,尤其在需要生成纸质文档时。WPF提供了强大的打印支持,通过`PrintDialog`等类简化了打印集成。本文将详细介绍如何在WPF应用中实现直接打印文档的功能,并通过具体示例代码展示其实现过程。
292 0
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
6天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。