亿级流量电商系统调优实战

JVM调优分类

调优是一个很大的概念,简单的说就是把系统进行优化。(假设代码没有问题的情况下,通过参数配置使程序的往我们期待的情况运行(高吞吐量?低延时?))

但是站在一个系统的角度,能够干的事情太多了。我们一般把JVM调优分为以下三类:

  1. JVM预调优
  2. 优化JVM运行环境(慢,卡顿等)
  3. 解决JVM中的问题(OOM)

调优中,最明显的是OOM。因为会抛出异常。当然它也只是调优的一部分。预调优和优化运行环境估计很多人的做法都只是服务器重启而已。我们使用AB工具测压(在我的其他博客JVM调优-内存优化有详细介绍),其实主要做的就是预调有与优化运行环境。在此再总结一下。

JVM预调优

业务场景设定

调优是要分场景的。所以一定要明确项目的场景设定。像现在大家都是微服务架构。服务拆分出来以后更适合做场景设定。

比如这个一部分服务注重吞吐量,另一部分注重用户体验(用户响应时间)等等。

无监控不优化

这里的监控指的是压力测试。能够看到结果,有数据体现的。不要用感觉去优化。所有的东西一定要有量化的指标。比如吞吐量,响应时间,服务器资源,网络资源等等。总之一句话——无监控不优化。

处理步骤

计算机内存需求

计算内存需求。内存不是越大越好。对于一般系统来说,内存的需求是弹性的。内存小,回收速度快也能承受。所以内存大小没有固定的规范。虚拟机栈的大小在高并发情况下可变小。(1M用不完,节约内存给堆或元空间)。

元空间(方法区)保险起见还是设定一个最大值(默认情况下元空间是没有大小限制的),一般限定几百M就够用了。为什么还限定元空间?

由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大当一个程序运行起来后,元空间的大小一般不会再变,或者变动很小),对于8G物理内存的机器来说,一般我会将这两个值都设置为256M(PS:读者可以根据自己的实际情况再调整)。

选定CPU

对系统来说,CPU性能是越高越好。这个按照预算来定(CPU巨贵)。

尤其是现在服务器做了虚拟机化后,虚拟机的性能指标不能单看虚拟化后的参数指标了,更应该看物理机的情况。

讲一下腾讯课堂享学king老师曾经发生的例子:以前使用8台服务器负载均衡完成。当时的机器是分两批到达。第一批4台电脑对程序进行压测完全没问题。但第二批的4台电脑到了之后,8台机器一起跑程序就会变卡。最终找到原因,后四台机器看虚拟机参数都是达标的,但是找到物理机后发现性能是远远落后的老年机。

选择合适的垃圾回收器

对于吞吐量优先的场景,就只有一种选择,就是使用PS组合(Paraller Scavenge+Parallel Old)(有没有和我一样有个疑问,为啥不是PP组合,而是PS组合的?)。这里把知识点再串一下。我们都说CMS是追求吞吐量的垃圾回收器,为啥还有PS组合呢?

其实PS组合才真正的极限做到了吞吐量。他在干活的时候全力干,在扫垃圾的时候全力扫。因此他的垃圾回收速度效率是更高的。只是他太用蛮力了,整个垃圾回收都是STW的。因此虽然快,但是在用户体验的业务场景中可能会造成较长时间的卡顿。

对于响应时间优先的场景,在JDK1.8中优先考虑G1,其次是CMS垃圾回收器。

设定新生代的大小,分代年龄

在我的其他文章JVM调优-内存优化一文中解释了:

吞吐量优先的应用:一般吞吐量优先的应该都有一个较大的新生代和一个较小的老年代。原因是可以尽可能回收掉大部分短期对象,减少中期对象,而老年代尽量存放长期存活对象。

设定日志参数

-XX:+PrintGC 输出 GC 日志

-XX:+PrintGCDetails 输出 GC 的详细日志

-XX:+PrintGCTimeStamps 输出 GC 的时间戳(以基准时间的形式)

-XX:+PrintGCDateStamps 输出 GC 的时间戳(如:2013-05-04T21:53:59.234+0800)

-XX:+PrintHeapAtGC 在进行 GC 的前后打印出堆的信息

-Xloggc:../logs/gc.log 日志文件的输出路径

注意:一般记录日志的是,如果只有一个日志文件肯定不行,有时候一个高并发项目一天产生的日志文件就上 T,其实记录日志这个事情,应该是运维干的事情。日志文件帮助我们分析问题

优化 JVM 运行环境(慢、卡顿等)

一般造成JVM卡顿的或慢的原因无非两部分:

  1. CPU占用过高。
  2. 内存占用过高,

所以需要我们具体问题具体分析,可以从这两方面思考排查。

解决JVM中的问题(OOM等)

栈溢出

HotSpot 版本中栈帧的大小是固定的,是不支持拓展的。

java.lang.StackOverflowError 一般的方法调用是很难出现的,如果出现了可能会是无限递归。

虚拟机栈带给我们的启示:方法的执行因为要打包成栈桢,所以天生要比实现同样功能的循环慢,所以树的遍历算法中:递归和非递归(循环来实现)都有存在的意义。递归代码简洁,非递归代码复杂但是速度较快。

OutOfMemoryError:不断建立线程,JVM 申请栈内存,机器没有足够的内存。

同时要注意,栈区的空间 JVM 没有办法去限制的,因为 JVM 在运行过程中会有线程不断的运行,没办法限制,所以只限制单个虚拟机栈的大小。

堆溢出

内存溢出:申请内存空间,超出最大堆内存空间。

如果是内存泄漏,检查自己的代码。

如果是内存溢出,则通过 调大 -Xms,-Xmx 参数。

如果不是内存泄漏,就是说内存中的对象却是都是必须存活的,那么久应该检查 JVM 的堆参数设置,与机器的内存对比,看是否还有可以调整的空间,再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行时的内存消耗。

方法区溢出

(1) 运行时常量池溢出

(2)方法区中保存的 Class 对象没有被及时回收掉或者

本机直接内存溢出

直接内存的容量可以通过 MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现 OOM 异常;

由直接内存导致的内存溢出,一个比较明显的特征是在 HeapDump 文件中不会看见有什么明显的异常情况,如果发生了 OOM,同时 Dump 文件很小,可以考虑重点排查下直接内存方面的原因。

亿级流量电商系统JVM调优

亿级流量系统

 

亿级流量系统,其实就是每天点击量在亿级的系统。根据淘宝的官方数据分析。每一个用户一次浏览点击20~40次之间,推测出每次活跃用户(日活用户)在500万左右。同时结合淘宝的一个点击数据,可以发现能够付费的用户只有10%左右。
90%的用户仅仅是浏览,那么我们可以通过图片缓存,Redis缓存,我们可以把90%的用户解决掉。

 

10%的付费用户,大概算出来是每日成交50万单左右。

GC预估

如果是普通业务,一般处理时间比较平缓,大概在3,4个小时处理。算出来每秒只有几十单。这个一般服务器不做任何处理(不需要预估调优)。另外电商系统中有大促场景(秒杀,限时抢购等)。一般这种业务是集中在几分钟之内。我们算出大约每秒2000单左右的数据。

承受大促场景使用4台服务器(使用负载均衡)。每台订单服务器也就是大概500单每秒。

我们测试发现,每一个订单处理过程中会占据0.2MB大小的空间(订单信息,优惠券,支付信息等等)。那么一台服务器每秒产生500*0.2=100M的内存空间。这些对象经过处理后就没用了,也就是属于朝生夕死的对象。1s后都会变为垃圾。

 

加入我们设置堆的空间最大值为3个G,我们按照默认情况下的设置,新生代1/3的堆空间,老年代2/3的堆空间。Edem:From:to = 8:1:1。

我们推测出,old =2G,Eden =800M,S0=S1=100M

根据对象的分配原则(对象优先在 Eden 区进行分配),由此可得,8 秒左右 Eden 区空间满了。

每 8 秒触发一个 MinorGC(新生代垃圾回收),这次 MinorGC 时,JVM 要 STW,但是这个时候有 100M 的对象是不能回收的(线程暂停,对象需要 1 秒后都会变成垃圾对象,因此这1s内的无法回收的对象集合有100M),那么就会有 100M 的对象在本次不能被回收(只有下次才能被回收掉)

所以经过本次垃圾回收后。本次存活的 100M 对象会进入 S0 区,但是由于另外一个 JVM 对象分配原则(如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄)

所以这样的对象本质上不会进去 Survivor 区,而是进入老年代 。

 

 

所以我们推算,大概每个 8 秒会有 100M 的对象进入老年代。大概 20*8=160 秒,也就是 2 分 40 秒左右 old 区就会满掉,就会触发一次 FullGC,一般来说,这次 FullGC 是可以避免的,同时由于 FullGC 不单单回收老年代+新生代,还要回收元空间,这些 FullGC 的时间可能会比较长(老年代回收的朝生夕死的对象,使用标记清除/标记整理算法决定了效率并不高,同时元空间也要回收一次,进一步加大 GC 时间)。所以问题的根本就是做到如何避免没有必要的 FullGC。

GC调优

我们在项目中加入 VM 参数:

-Xms3072M -Xmx3072M -Xmn2048M -XX:SurvivorRatio=7

-Xss256K -XX:MetaspaceSize= 128M -XX:MaxMetaspaceSize= 128M

-XX:MaxTenuringThreshold=2

-XX:ParallelGCThreads=8

-XX:+UseConcMarkSweepGC

首先看一下堆空间:old 区=1G,Eden 区=1.4G,S0=S1=300M

 

那么第一点,Eden 区大概需要 14 秒才能填满,填满之后,100M 的存活对象会进入 Form 区(由于这个区域变大300/2=150M>100M,不会触发动态年龄判断)

 

再过 14 秒,Eden 区,填满之后,还是剩余 100M 的对象要进入 S1 区。但是由于原来的 100M 已经是垃圾了(过了 14 秒了),所以,S1 也只会有 Eden 区过来的 100M 对象,S0 的 100M 已经别回收,也不会触发动态年龄判断。

 

 

反反复复,这样就没有对象会进入 old 区,就不会触发 FullGC,同时我们的 MinorGC 的频次也由之前的 8 秒变为 14 秒,虽然空间加大,但是换来的还是 GC 的总时间会减少

空间一般启动后就不会有太多的变化,我们可以设定为 128M,节约内存空间。

-Xss256K -XX:MetaspaceSize= 128M -XX:MaxMetaspaceSize= 128M 。虚拟机栈一般情况下很少用到 1M。所以为了线程占用内存更少,我们可以减少到 256K元空间一般启动后就不会有太多的变化,我们可以设定为 128M,节约内存空间。-XX:MaxTenuringThreshold=2 这个是分代年龄(年龄为 2 就可以进入老年代),因为我们基本上都使用的是 Spring 架构,Spring 中很多的 bean 是长期要存活的,没有必要在 Survivor 区过渡太久,所以可以设定为 2,让大部分的 Spring 的内部的一些对象进入老年代。(朝生夕死的对象撑不过一回合)-XX:ParallelGCThreads=8 线程数可以根据你的服务器资源情况来设定(要速度快的话可以设置大点,根据 CPU 的情况来定,一般设置成 CPU 的整数倍。(这种场景就属于CPU密集型场景)。-XX:+UseConcMarkSweepGC 因为这个业务响应时间优先的,所以还是可以使用 CMS 垃圾回收器或者 G1 垃圾回收器。

 

 

 

 

 

 

 

 

 

 

 

猜你喜欢

转载自blog.csdn.net/weixin_47184173/article/details/110607760