GC实战—浮动内存导致的CPU过高调优

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 哥伦布在2019春节战役期间,由于接入的应用越来越多,对系统性能要求越来越高,提高系统的吞吐率,以及提升性能,是我们春节战役期间必须要做的事情。 系统的性能优化不单单是对JVM的参数调优,也不是某一段代码的改造,而是一个系统的工程,往往会出现牵一发而动全身,简单的解决,很容易治标不治本从而掩盖问题的本质,而这些深藏的问题才是我们解决问题关键。 本次的浮动内存发现就是一次扑朔迷离的查找过程,cp

哥伦布在2019春节战役期间,由于接入的应用越来越多,对系统性能要求越来越高,提高系统的吞吐率,以及提升性能,是我们春节战役期间必须要做的事情。

系统的性能优化不单单是对JVM的参数调优,也不是某一段代码的改造,而是一个系统的工程,往往会出现牵一发而动全身,简单的解决,很容易治标不治本从而掩盖问题的本质,而这些深藏的问题才是我们解决问题关键。

本次的浮动内存发现就是一次扑朔迷离的查找过程,cpu利用率过高,常见的问题都是线程使用不当造成的。但从这个方向去解决,你会发现很难解决本次的cpu过高问题,甚至效果不明显,若是从整个系统的各个指标协同分析验证,才会找到解决的最佳方案。

 

一、现象

 

1.高峰期报警

服务一般的高峰期都是晚上的19点—22点左右,期间的访问QPS可达到日间的2倍左右,访问量过大,机房的机器报警就接踵而至:

load1过高4 以上,CPU利用率达到40%以上。

一般的尽快解决方案都是扩容,加更多的机器来分流,但这只是一个临时的方案,浪费资源不说,还会掩盖真实问题。

 

2.监控信息

 

jvm内存

 

load

 

线程:

 

基于监控信息,我们发现以下问题:

  1. 10分钟 oldGC一次,每个波峰间隔时间8-10分钟左右.

  2. CPU load过高,高峰期常在load1 在2-6之间浮动(4核),cpu利用率在14%—46%之间浮动。

  3. jvm的 内存的波峰波谷过于频繁,不是长方体或椭圆形,呈锯齿状

  4. 线程过多,单个系统线程达到1000以上

 

二、分析

 

基于监控反馈的信息我们可以从以下几个点着手:

按道理来说CPU过高我们最应该从线程优化这个点入手,但通过监控信息的对比,我们发现,最严重的问题还是来之JVM内存管理.

从上图我们可以看出oldGC过于频繁:

10分钟 oldGC一次,每个波峰间隔时间8-10分钟左右.

这是一个重要的信息,我们可以理解为由于频繁GC导致CPU利用率上升。因此降低GC的频率是我们的首要任务。

三、第一次尝试-GC调优

GC调优我们很容易想到的方案,是调整新生代和老生代的比率,提升新生代看空间,降低SurvivorRatio,从而延缓新生代的晋升。由于第一版的了解过于肤浅和紧急,我们的方案简单粗暴:

  1. Total Head 提升1g

  2. ParNew head 提升1g

  3. SurvivorRatio 10->7

上面的结果可想而知,效果很勉强。

  1. oldGC从8-10分钟,改变问10—12分钟,提升10%左右

  2. CPU 利用率 高峰期最高46%,load1 (2-6)没变化

 

四、第二次尝试-浮动内存调优

鉴于第一次分析不仔细,我们在需要对jvm内存进行dump分析,看下具体是什么原因导致old不断GC。

我们借助以下工具:

  1. Eagleeye

  2. Zprofiler

从dump下的head信息,我们看到,有两部分占用内存过高。

 

内存泄漏可疑点分析:

 

通过head占比分析,我们发现一些数据库load对象很可疑,使用了大量的hashMap缓存数据。通过追踪代码,我们发现以下问题:

 

以上代码是从数据库定时load数据到内存中,每次load之后,之前的map对象被遗弃。造成的现象:

1.每隔一段时间(5分钟以内)会有一批大对象被释放

2.大对象会随着系统的运营时长以及新应用的加入,越来越大

3.浮动对象是用来解决缓存被动更新的问题

4.浮动大对象对GC造成很大的压力,大量的浮动对象在新生代没有被释放就晋升到老年代,造成老年代频繁GC

 

通过以上分析,我们可以定位频繁GC的主要问题就是 load里面的map,由于不断的定时废弃,造成了大量的浮动大对象GC。

 

通过扫描,我们发现系统中由于没有引入统一的缓存架构,大量需要缓存的代码,都采用这种 定时废弃的方式来进行。

如此多的代码,改动工作量还是很大的,这也促进我们对YKCache 框架的推出,YKCache主要用来改善缓存的统一管理接入工作,热点缓存的维护,以及多级缓存(local+tair+db)的支持。

 

YKCache特点:

1.多级缓存(local+tair+db),

2.热点数据支持自动维护。

3.地侵入接入,不改造代码

4.缓存支持主动更新和被动更新

 

在引入YKCache之后,我们对内存占比最大的两个load进行改造,同时移除某个二方包里面的loadMap,对此我们队head进行观察。

 

 

我们可以看到,load的map内存占用比降低了,因为缓存框架的引入,内存中不必缓存全部数据,只维护热点数据,内存占比降低了60%以上。同时热点缓存直接接入老年代,减少了老年代的GC频率。

 

但是这并不是终点,GC依然没有达到我们想要的目标。

 

五、第三次尝试-浮动内存+GC调优

以上我虽然解决的浮动内存的问题,但是我们还有一个问题忽略了,就是:

对象晋升的问题

 

1.对象晋升

我们知道对象晋升达到以下两个条件之一就可以发生:

1.minor gc 之后,存活于survivor 区域的对象的age会+1,当超过(默认)15的时候,转移到老年代。

2.动态对象,如果survivor空间中相同年龄所有的对象大小的综合和大于survivor空间的一半,年级大于或等于该年级的对象就可以直接进入老年代。

我们通过加入PrintTenuringDistribution 参数,观察对象的晋升情况。

Desired survivor size 178946048 bytes, new threshold 15 (max 15)

- age 1: 88665064 bytes, 88665064 total

- age 2: 40523456 bytes, 129188520 total

- age 3: 40853440 bytes, 170041960 total

: 2722627K->253593K(2796224K), 0.2869541 secs] 3523830K->1114986K(4893376K), 0.2876559 secs] [Times: user=0.68 sys=0.02, real=0.29 secs]

2019-01-23T14:49:48.763+0800: 72770.431: [GC (Allocation Failure) 2019-01-23T14:49:48.763+0800: 72770.431: [ParNew

Desired survivor size 178946048 bytes, new threshold 4 (max 15)

- age 1: 93440696 bytes, 93440696 total

- age 2: 16810912 bytes, 110251608 total

- age 3: 35219480 bytes, 145471088 total

- age 4: 40484456 bytes, 185955544 total

: 2700313K->280078K(2796224K), 0.1973428 secs] 3561706K->1141471K(4893376K), 0.1982309 secs] [Times: user=0.46 sys=0.01, real=0.20 secs]

2019-01-23T14:49:59.610+0800: 72781.278: [GC (Allocation Failure) 2019-01-23T14:49:59.611+0800: 72781.279: [ParNew

Desired survivor size 178946048 bytes, new threshold 15 (max 15)

- age 1: 88155400 bytes, 88155400 total

- age 2: 14958464 bytes, 103113864 total

- age 3: 10194096 bytes, 113307960 total

- age 4: 35023952 bytes, 148331912 total

: 2726798K->188190K(2796224K), 0.2553227 secs] 3588191K->1089172K(4893376K), 0.2560257 secs] [Times: user=0.54 sys=0.00, real=0.25 secs]

 

通过以上日志我们可以发现,大量的对象在age很小的时候就晋升了,这说明两个问题:

1.新生代空间不足

2.survivor空间不足

但问题1和2已经扩大了,我们不可能无限扩大,由于我们是互联网应用,大量请求到来的时候,会产生大量的临时对象,我们需要考虑的是让这些临时对象尽可能的留在新生代,而不是晋升。

我们可以提高 from 区的利用率。

 

2.TargetSurvivorRatio

我们可以尝试加上-XX:TargetSurvivorRatio=90,提高 from 区的利用率,使 from 区使用到 90%时,再讲对象送入到老年代。

加上-XX:TargetSurvivorRatio=90,之后我们观察gc日志:

Desired survivor size 414115424 bytes, new threshold 15 (max 15)

  • age 1: 53927288 bytes, 53927288 total

  • age 2: 20714864 bytes, 74642152 total

  • age 3: 7596264 bytes, 82238416 total

  • age 4: 3017336 bytes, 85255752 total

  • age 5: 3873704 bytes, 89129456 total

  • age 6: 6201984 bytes, 95331440 total

  • age 7: 5167776 bytes, 100499216 total

  • age 8: 5968880 bytes, 106468096 total

  • age 9: 13705944 bytes, 120174040 total

  • age 10: 2725960 bytes, 122900000 total

  • age 11: 5891312 bytes, 128791312 total

  • age 12: 15099224 bytes, 143890536 total

  • age 13: 3007408 bytes, 146897944 total

  • age 14: 1947336 bytes, 148845280 total

  • age 15: 879432 bytes, 149724712 total : 2425961K->195062K(2696384K), 0.1951935 secs] 3259218K->1028912K(4793536K), 0.1957401 secs] [Times: user=0.42 sys=0.06, real=0.20 secs] 2019-02-17T21:07:53.115+0800: 205984.589: [GC (Allocation Failure) 2019-02-17T21:07:53.115+0800: 205984.590: [ParNew Desired survivor size 414115424 bytes, new threshold 15 (max 15)

age 1: 61925600 bytes, 61925600 total

  • age 2: 22640624 bytes, 84566224 total

  • age 3: 6638528 bytes, 91204752 total

  • age 4: 5696944 bytes, 96901696 total

  • age 5: 2959920 bytes, 99861616 total

  • age 6: 3602640 bytes, 103464256 total

  • age 7: 6189360 bytes, 109653616 total

  • age 8: 5161344 bytes, 114814960 total

  • age 9: 5949672 bytes, 120764632 total

  • age 10: 13695168 bytes, 134459800 total

  • age 11: 2725096 bytes, 137184896 total

  • age 12: 4659184 bytes, 141844080 total- age 13: 14082208 bytes, 155926288 total- age 14: 2899928 bytes, 158826216 total

  • age 15: 1929920 bytes, 160756136 total: 2442102K->196395K(2696384K), 0.1910995 secs] 3275952K->1031074K(4793536K), 0.1916514 secs ] [Times: user=0.45 sys=0.00, real=0.20 secs]

基于以上日志我们可以看出,对象的年龄多数晋升到MaxTenuringThreshold(默认值15)设置的值才完成晋升。另外我们还可以把大对象直接送入到老年代,通过设置-XX:PetenureSizeThreshold,设置大对象直接进入年老代的阈值。当对象的大小超过这个值时,将直接在年老代分配。

 

3.优化结果

基于上述优化之后的结果。

 

可以看到上图中:

1.高峰期,oldGC 从10分钟提升到50分钟一次

2.低峰期,oldGc 6小时1次

3.cpu利用率 高峰期 从46% 降低到26%

4.load从2-6区间,降低到1.4-3区间

 

六、其他优化

 

1.线程优化

线程优化主要着重于线程池的优化,系统大量使用线程池,但对线程池的滥用也会造成资源的浪费,基于此有以下建议:

  1. CPU 密集型任务配置尽可能小的线程,建议Ncpu+1 个。

  2. IO 密集型任务,线程多数在等待,配置尽可能多的线程,建议 2*Ncpu。

  3. 不建议maxPoolSize设置的过大,多开线程解决不了问题,加大CPU管理负担。

  4. 超时时间不必太长,加大下游负担

 

2.日志异步

线上由于用户基数大,产生的日志量也大,如果没对日志进行异步写处理,会导致这一块会耗费大量的资源在IO上,所以日志异步队列化,也是提升系统性能的必不可少的。

 

3.DB优化

DB的优化主要主要体现在慢查询的优化,这一块集团提供的iDB工具已经很好的支持,定期的去查看下DB性能,针对慢查询的sql语句优化也是必不可少的优化缓解。

 

4.性能调优层次

为了提升系统性能,我们需要对系统的各个角度和层次来进行优化,针对系统的调优不是只有jvm调优一项,而是需要针对系统来整体调优,才能提升系统的性能。我们不能指望一个系统架构有缺陷或者代码层次优化没有穷尽的应用,通过jvm调优令其达到一个质的飞跃,这是不可能的。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
1月前
|
监控 并行计算 数据处理
构建高效Python应用:并发与异步编程的实战秘籍,IO与CPU密集型任务一网打尽!
在Python编程的征途中,面对日益增长的性能需求,如何构建高效的应用成为了每位开发者必须面对的课题。并发与异步编程作为提升程序性能的两大法宝,在处理IO密集型与CPU密集型任务时展现出了巨大的潜力。今天,我们将深入探讨这些技术的最佳实践,助你打造高效Python应用。
36 0
|
6天前
|
弹性计算 Kubernetes Perl
k8s 设置pod 的cpu 和内存
在 Kubernetes (k8s) 中,设置 Pod 的 CPU 和内存资源限制和请求是非常重要的,因为这有助于确保集群资源的合理分配和有效利用。你可以通过定义 Pod 的 `resources` 字段来设置这些限制。 以下是一个示例 YAML 文件,展示了如何为一个 Pod 设置 CPU 和内存资源请求(requests)和限制(limits): ```yaml apiVersion: v1 kind: Pod metadata: name: example-pod spec: containers: - name: example-container image:
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
55 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
15天前
|
存储 关系型数据库 MySQL
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
187 1
|
30天前
|
存储 监控 算法
JVM调优深度剖析:内存模型、垃圾收集、工具与实战
【10月更文挑战第9天】在Java开发领域,Java虚拟机(JVM)的性能调优是构建高性能、高并发系统不可或缺的一部分。作为一名资深架构师,深入理解JVM的内存模型、垃圾收集机制、调优工具及其实现原理,对于提升系统的整体性能和稳定性至关重要。本文将深入探讨这些内容,并提供针对单机几十万并发系统的JVM调优策略和Java代码示例。
46 2
|
26天前
|
运维 JavaScript Linux
容器内的Nodejs应用如何获取宿主机的基础信息-系统、内存、cpu、启动时间,以及一个df -h的坑
本文介绍了如何在Docker容器内的Node.js应用中获取宿主机的基础信息,包括系统信息、内存使用情况、磁盘空间和启动时间等。核心思路是将宿主机的根目录挂载到容器,但需注意权限和安全问题。文章还提到了使用`df -P`替代`df -h`以获得一致性输出,避免解析错误。
|
2月前
|
存储 关系型数据库 MySQL
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
125 5
|
2月前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。
|
1月前
|
C# 开发工具 Windows
C# 获取Windows系统信息以及CPU、内存和磁盘使用情况
C# 获取Windows系统信息以及CPU、内存和磁盘使用情况
40 0
|
2月前
|
Prometheus Kubernetes 监控
使用kubectl快速查看各个节点的CPU和内存占用量
在Kubernetes集群中,安装metrics-server,并使用kubectl快速查看集群中各个节点的资源使用情况。
118 0

热门文章

最新文章