亿级流量高并发下GC预估与调优

在这里插入图片描述
亿级流量系统,其实就是每天点击量在亿级的系统,根据淘宝的一个官方的数据分析。 每个用户一次浏览点击 20~40 次之间,推测出每日活跃用户(日活用户)在 500 万左右。同时结合淘宝的一个点击数据,可以发现,能够付费的也就是橙色的部分(cart)的用户,比例只有 10%左右。 90%的用户仅仅是浏览,那么我们可以通过图片缓存、Redis 缓存等技术,我们可以把 90%的用户解决掉。 10%的付费用户,大概算出来是每日成交 50 万单左右。

一、GC预估

如果是普通业务,一般处理时间比较平缓,大概在 3,4 个小时处理,算出来每秒只有几十单,这个一般的应用可以处理过来(不需要 JVM 预估调优) 另外电商系统中有大促场景(秒杀、限时抢购等),一般这种业务是几种在几分钟。我们算出来大约每秒 2000 单左右的数据,承受大促场景的使用 4 台服务器(使用负载均衡)。每台订单服务器也就是大概 500 单/秒 我们测试发现,每个订单处理过程中会占据 0.2MB 大小的空间(什么订单信息、优惠券、支付信息等等),那么一台服务器每秒产生 100M 的内存空间, 这些对象基本上都是朝生夕死,也就是 1 秒后都会变成垃圾对象。
在这里插入图片描述
加入我们设置堆的空间最大值为 3 个 G,我们按照默认情况下的设置,新生代 1/3 的堆空间,老年代 2/3 的堆空间。Eden:S0:S1=8:1:1,
我们推测出,old 区=2G,Eden 区=800M,S0=S1=100M
根据对象的分配原则(对象优先在 Eden 区进行分配),由此可得,8 秒左右 Eden 区空间满了。每 8 秒触发一个 MinorGC(新生代垃圾回收),这次 MinorGC 时,JVM 要 STW,但是这个时候有 100M 的对象是不能回收的(线程暂停,对象需要 1 秒
后都会变成垃圾对象),那么就会有 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
1、首先看一下堆空间:old 区=1G,Eden 区=1.4G,S0=S1=300M
在这里插入图片描述
2、那么第一点,Eden 区大概需要 14 秒才能填满,填满之后,100M 的存活对象会进入 S0 区(由于这个区域变大,不会触发动态年龄判断)
在这里插入图片描述
3、再过 14 秒,Eden 区,填满之后,还是剩余 100M 的对象要进入 S1 区。但是由于原来的 100M 已经是垃圾了(过了 14 秒了),所以,S1 也只会 有 Eden 区过来的 100M 对象,S0 的 100M 已经别回收,也不会触发动态年龄判断。
在这里插入图片描述
4、反反复复,这样就没有对象会进入 old 区,就不会触发 FullGC,同时我们的 MinorGC 的频次也由之前的 8 秒变为 14 秒,虽然空间加大,但是换来 的还是 GC 的总时间会减少。
5、-Xss256K -XX:MetaspaceSize= 128M -XX:MaxMetaspaceSize= 128M 栈一般情况下很少用到 1M。所以为了线程占用内存更少,我们可以减少到 256K 元空间一般启动后就不会有太多的变化,我们可以设定为 128M,节约内存空间。
6、-XX:MaxTenuringThreshold=2 这个是分代年龄(年龄为 2 就可以进入老年代),因为我们基本上都使用的是 Spring 架构,Spring 中很多的 bean 是 长期要存活的,没有必要在 Survivor 区过渡太久,所以可以设定为 2,让大部分的 Spring 的内部的一些对象进入老年代。
7、-XX:ParallelGCThreads=8 线程数可以根据你的服务器资源情况来设定(要速度快的话可以设置大点,根据 CPU 的情况来定,一般设置成 CPU 的整 数倍)

三、JVM 调优实战
项目介绍

在这里插入图片描述

代码介绍

在这里插入图片描述
在 Linux 服务跑起来 java -cp ref-jvm3.jar -XX:+PrintGC -Xms200M -Xmx200M ex13.FullGCProblem

CPU占用过高排查实战
1.先通过 top 命令找到消耗 cpu 很高的进程 id 假设是 2732

top 命令是我们在 Linux 下最常用的命令之一,它可以实时显示正在执行进程的 CPU 使用率、内存使用率以及系统负载等信息。其中上半部分显示的是 系统的统计信息,下半部分显示的是进程的使用率统计信息。
在这里插入图片描述

2.执行 top -p 2732 单独监控该进程
3、在第 2 步的监控界面输入 H,获取当前进程下的所有线程信息

在这里插入图片描述

4、找到消耗 cpu 特别高的线程编号,假设是 2734(要等待一阵)
5、执行 jstack 123456 对当前的进程做 dump,输出所有的线程信息
6 将第 4 步得到的线程编号 11354 转成 16 进制是 0x7b

在这里插入图片描述
也可以通过计算器来换算。

7 根据第 6 步得到的 0x7b 在第 5 步的线程信息里面去找对应线程内容
8 解读线程信息,定位具体代码位置

在这里插入图片描述
发现找是 VM 的线程占用过高,我们发现我开启的参数中,有垃圾回收的日志显示,所以我们要换一个思路,可能是我们的业务线程没问题,而是垃圾 回收的导致的。 (代码中有打印 GC 参数,生产上可以使用这个 jstat –gc 来统计,达到类似的效果) 是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据,在没有 GUI 图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。 假设需要每 250 毫秒查询一次进程 13616 垃圾收集状况,一共查询 10 次,那命令应当是:jstat-gc 13616 250010
在这里插入图片描述
使用这个大量的 FullGC 了 还抛出了 OUT Of Memory
在这里插入图片描述
S0C:第一个幸存区的大小 S1C:第二个幸存区的大小 S0U:第一个幸存区的使用大小 S1U:第二个幸存区的使用大小 EC:伊甸园区的大小 EU:伊甸园区的使用大小 OC:老年代大小 OU:老年代使用大小 MC:方法区大小 MU:方法区使用大小 CCSC:压缩类空间大小 CCSU:压缩类空间使用大小 YGC:年轻代垃圾回收次数 YGCT:年轻代垃圾回收消耗时间 FGC:老年代垃圾回收次数 FGCT:老年代垃圾回收消耗时间 GCT:垃圾回收消耗总时间

怎么办?OOM 了. 我们可以看到,这个里面 CPU 占用过高是什么导致的? 是业务线程吗?不是的,这个是 GC 线程占用过高导致的。JVM 在疯狂的进行垃圾回收,再回顾下之前的知识,JVM 中默认的垃圾回收器是多线程的(回 顾下之前的知识),所以多线程在疯狂回收,导致 CPU 占用过高。

内存占用过高思路

用于生成堆转储快照(一般称为 heapdump 或 dump 文件)。jmap 的作用并不仅仅是为了获取 dump 文件,它还可以查询 finalize 执行队列、Java 堆和永
久代的详细信息,如空间使用率、当前用的是哪种收集器等。和 jinfo 命令一样,jmap 有不少功能在 Windows 平台下都是受限的,除了生成 dump 文件的 -dump 选项和用于查看每个类的实例、 空间占用统计的-histo 选项在所有操作系统都提供之外
在这里插入图片描述
把 JVM 中的对象全部打印出来, 但是这样太多了,那么我们选择前 20 的对象展示出来, jmap –histo 1196 | head -20
在这里插入图片描述
定位问题的关键,就是这条命令。 很多个 88 万个对象。
在这里插入图片描述

问题总结(找到问题)

一般来说,前面这几行,就可以看出,到底是哪些对象占用了内存。 这些对象回收不掉吗?是的,这些对象回收不掉,这些对象回收不掉,导致了 FullGC,里面还有 OutOfMemory.
在这里插入图片描述
任务数多于线程数,那么任务会进入阻塞队列,就是一个队列,你进去,排队,有机会了,你就上来跑。 但是同学们,因为代码中任务数一直多于线程数,所以每 0.1S,就会有 50 个任务进入阻塞对象,50 个任务底下有对象,至少对象送进去了,但是没执行。 所以导致对象一直都在,同时还回收不了
为什么回收不了。Executor 是一个 GCroots
在这里插入图片描述

总结

在 JVM 出现性能问题的时候。(表现上是 CPU100%,内存一直占用)
1、 如果 CPU 的 100%,要从两个角度出发,一个有可能是业务线程疯狂运行,比如说想很多死循环。还有一种可能性,就是 GC 线程在疯狂的回收,因 为 JVM 中垃圾回收器主流也是多线程的,所以很容易导致 CPU 的 100%
2、 在遇到内存溢出的问题的时候,一般情况下我们要查看系统中哪些对象占用得比较多,我的是一个很简单的代码,在实际的业务代码中,找到对应的 对象,分析对应的类,找到为什么这些对象不能回收的原因,就是我们前面讲过的可达性分析算法,JVM 的内存区域,还有垃圾回收器的基础,当然, 如果遇到更加复杂的情况,你要掌握的理论基础远远不止这些(JVM 很多理论都是排查问题的关键)

常见问题分析
超大对象

代码中创建了很多大对象 , 且一直因为被引用不能被回收,这些大对象会进入老年代,导致内存一直被占用,很容易引发 GC 甚至是 OOM

超过预期访问量

通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值。 比如如果一个系统高峰期的内存需求需要 2 个 G 的堆空间,但是堆空间设置比较小,导致内存不够,导致 JVM 发起频繁的 GC 甚至 OOM

内存泄漏

大量对象引用没有释放,JVM 无法对其自动回收

长生命周期的对象持有短生命周期对象的引用

例如将 ArrayList 设置为静态变量,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏

连接未关闭

如数据库连接、网络连接和 IO 连接等,只有连接被关闭后,垃圾回收器才会回收对应的对象。

变量作用域不合理

例如,1.一个变量的定义的作用范围大于其使用范围,2.如果没有及时地把对象设置为 null

我们一般优化的思路有一个重要的顺序:
  1. 程序优化,效果通常非常大;
  2. 扩容,如果金钱的成本比较小,不要和自己过不去;
  3. 参数调优,在成本、吞吐量、延迟之间找一个平衡点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值