小言_互联网的博客

小米Talos GC性能调优实践

270人阅读  评论(0)


  本文讲述了关于Talos GC性能调优的实践过程。

往期文章回顾:小米数据收集利器:AgentSource

背景

Talos作为小米自研的消息队列,在公司内部已经有了较为广泛的应用,支持了多个重点的离线和在线业务,随之而来的是业务对性能更高的要求。而对消息队列来说,最关键的性能指标不外乎吞吐和延时。对于吞吐,因为Talos的设计思想就是面向吞吐型的消息队列,再加上前文(Talos网卡负载优化:基于个性化一致性哈希的负载均衡)中所述Talos对网卡负载的优化,吞吐性能已经不是Talos的瓶颈。在高吞吐的场景下,Talos仍待解决的是GC问题对延时的影响,前文(万亿级消息背后:小米在消息队列的实践)中介绍了Talos在GC问题上经历的三个阶段,本文将详细介绍各阶段遇到的问题和解法。

>>>>

CMS中的重要概念

1.分代回收

目前主流的垃圾回收都采用“分代回收“的思路,CMS虽然是一款针对老年代的垃圾收集器,但仍免不了和年轻代打交道,因此这里简单描述一下”分代回收“思想。

年轻代:包含一个Eden区和两个Survivor区,其中Eden区是对象产生的区域,Survivor区主要存储存活着但还不能晋升到老年代的对象

老年代:存放常驻内存对象的区域

分代回收:当Eden区满时,会发生一次年轻代回收(Young GC),将仍存活的对象复制到一个Survivor区,当这个Survivor区满时,存活但没有达到晋升条件的对象全部被复制到另一个Survivor区中,达到晋升条件的对象则被放入老年代;当老年代空间使用率达到阈值,触发一次老年代回收(Old GC),将老年代中已经”死掉“的对象清理掉。需要注意的是,常说的Full GC是包含年轻代回收和老年代回收的。

2. CMS原理简述

CMS(Concurrent Mark Sweep),是一种以牺牲吞吐量为代价来缩短应用停顿时间的、垃圾收集过程中的大部分线程可以和应用线程并发执行的、面向老年代的、采用并发标记-清除算法的垃圾收集器,一次CMS GC的主要过程如下(灰色代表应用线程,黑色代表GC线程,其中Initial-mark和Remark阶段会暂停应用进程的执行,称为Stop the world(STW)):

Initial-mark:标记老年代中所有能被年轻代和GC Root直接引用的对象(GC Root可以理解为本次GC时肯定不会被回收的部分对象,例如静态变量)

Marking/Pre-cleaning:Marking表示在应用程序运行的同时遍历Initial-mark阶段标记出来的对象,并递归扫描这些对象可达的所有对象(标记出老年代中所有存活的对象);Pre-cleaning表示清理应用程序并发执行时出现的引用关系变化,例如刚晋升的对象和直接分配在老年代的对象

Remark:暂停所有应用线程,重新扫描堆中的对象,在开始清理之前保持标记的正确性,查找在并发标记阶段漏掉的对象

Concurrent Sweeping:回收被标记为不可达的对象,这一阶段存活的对象并没有发生移动

3.内存碎片问题

了解”标记-清除“算法的同学应该知道这种方法将会产生大量的内存碎片,这是因为:CMS将内存空间划分成一个个的网格,内存是连续分配的,但回收时只收集”死亡“的对象,不关心仍存活的对象。因此当应用程序执行一段时间后,必然会出现大量不连续的空闲内存碎片,导致老年代虽然还有很大剩余空间,但无法找到足够大的连续空间来分配申请的对象,触发Full GC(Promotion failed)

4.Floating Garbage 问题

在并发清理阶段,用户线程仍在运行,如果产生新的垃圾,在此次GC中无法得到处理,只能等到下次清理,这些对象称为Floating Garbage。当Floating Garbage过多导致内存空间不足,那么CMS GC过程中就会出现Full GC(Concurrent Mode Failure)

>>>>

Talos+CMS遇到的问题

了解CMS的重要概念之后,本节将重点介绍在实践中遇到的问题。Talos最初使用的垃圾收集器是CMS,在吞吐量不大的集群CMS能够很好的工作,但是在一些业务流量特别大的集群(特别是在线业务),会频繁遇到Full GC的问题,对延时造成极大影响,这里将问题分为两个阶段:

1.频繁Full GC阶段

关键参数:

堆大小为20GB,其中年轻代占7GB(survivor区896MB,Eden区5.25GB),老年代占13GB

-Xmx20480m

-Xms20480m

-Xmn7680m

-XX:SurvivorRatio=6

-XX:MaxTenuringThreshold=3

-XX:CMSInitiatingOccupancyFraction=70

问题描述:

Talos 进程每隔几秒出现一次耗时4秒左右的Full GC,原因是Promotion Failed,这代表应用进程在申请内存时发生了对象晋升失败。具体来说是由于在申请空间时 Eden 区满而触发 Young GC,在 Young GC 时尝试把 Survivor 区的对象(age3 -> age4)挪到 Old 区,但是老年代已经没有空间或没有连续的大块空间,于是导致 “对象晋升失败”,即Promotion Failed,从而触发 Full GC。如下图所示,通过GC日志也可以确认,发生GC时老年代已经占满。

此时面临一个问题,为什么 CMS 没有及时清理老年代呢?

根据上面设定的参数,我们知道Old 区只有13G,而通过观察GC日志发现,Talos 进程的常驻对象大概在 10-12GB 左右。这应该就是导致频繁Full GC的根本原因:老年代实际可用的空间只有1-2G,CMS回收空间的速度跟不上申请空间的速度,从而不断触发Full GC。

解决方案:

  • 调大堆的大小

  • 缩小年轻代的大小,转移给老年代

因为内存资源相对比较有限,在调整堆之前,让我们先通过日志来分析一下年轻代的空间利用情况。Young GC具体细节如下:

已知年轻代大小为7G,Eden区占5.25GB,两个Survivor区各占896MB,结合上图可得:

  • Young 区已经使用的6GB,在本次GC结束后能回收大部分对象,空间占用由 6GB 降为 90MB

  • Young GC 中花在Survivor区的复制开销相对较小,每次大概在几十兆左右

  • Survivor 区的对象一共有90MB左右,但设定的survivor区有896MB,空间利用率较低

  • 红框部分表示本次Young GC前后,年轻代和整堆的空间变化情况。可以计算出Survivor区晋升到Old区的对象为12.2MB((6003686-94210) - (17375735-11478816) = 12557)

由以上结论可以看出,年轻代的空间是存在浪费的,因此可以调整一部分空间转移给老年代,调整思路为:将Survivor大小调整为200MB,并将Eden区和Survivor区的比例调大到8:1,年轻代大小调整为2GB。

这样既可以保证年轻代正常运转,又在不扩大堆的前提下将老年代空间扩大了5GB。调整后关键参数如下:

-Xmx20480m

-Xms20480m

-Xmn2560m

-XX:SurvivorRatio=8

-XX:MaxTenuringThreshold=3

-XX:CMSInitiatingOccupancyFraction=70

调整后,频繁Full GC的问题得到了解决,优化效果如下:

  • 常驻内存维持在 10-12GB 左右,但不再频繁触发 full gc

  • 因为调小了年轻代大小,YGC 由原来的平均 90ms+ 缩减到 40ms

2.周期性Full GC阶段

问题描述:

在稳定运行一段时间后,Talos集群开始出现由CMS内存碎片问题导致的周期性Full GC,最初一个周期为30天,在Talos上线一些新功能后,加重了对内存的使用,此时周期缩减为7天,单次耗时19秒左右。从监控角度来看,Full GC时的请求堆积情况非常严重,会对延迟敏感型的线上业务造成较大的影响。

解决方案:

由于CMS使用标记-清除算法,没有整理内存空间的机会,老年代空间会随应用时间的增长而被“耗尽”(此处“耗尽”是指没有连续的空间可供分配),因此如果仍然使用CMS,周期性Full GC的问题无法得到完全的解决,所以开始转向调研其他垃圾收集器。

>>>>

G1中的重要概念

1.G1原理简述

G1是Garbage-First的缩写,与CMS相比,G1是一种有内存整理过程的、优先收集存活对象最少区域的、面向年轻代和老年代的、物理空间不必连续的垃圾收集器。

Region:整个堆空间被尽可能地划分为约2048个Region(单个Region大小为1~32MB),每个Region都可以是Eden/Survivor/Old区的任意一个

Remembered Sets(RSets):每个Region对应一个RSet,用来跟踪指向该区域的对象引用,避免全堆扫描

Collection Sets(CSets):CSets是一次GC中将被回收的Region集合,GC时所有CSet区域中的存活对象都会被移动到新的区域

Humongous Object:超过Region大小一半的对象称为巨型对象,会在创建时直接被分配到Old Regions中

2.工作模式

G1是同时面向年轻代和老年代的垃圾收集器,因此包括Young GC 和 Mixed GC两种工作模式。

Young GC:

  • Young 区由一系列不连续的Region组成,因此可以很容易在GC过程中扩/缩容

  • Young GC 会将存活对象转移到Survivor和老年代

  • Young GC 会计算 Eden 区和 Survivor 区在下次回收之前所需的的空间,然后动态调整新生代Region个数,从而达到控制Young GC耗时的目的

Mixed GC:

  • 当老年代使用的空间超过阈值(IHOP),会触发 Global Concurrent Marking 统计出回收收益高的老年代Region,然后开始一系列连续的Mixed GC,这个过程称为Concurrent Marking Cycle Phases

  • 一系列连续的Mixed GC中,GC的最大次数和单次回收的空间大小可以通过参数-XX:G1MixedGCCountTarget和-XX:G1OldCSetRegionThresholdPercent进行配置

  • 除了GC次数超过最大值以外,如果可回收空间的比例小于参数 -XX:G1HeapWastePercent ,也会中断Mixed GC周期

  • Mixed GC 是面向全堆进行垃圾收集的,所以通常会伴随一次Young GC,但与Full GC不同的是,Mixed GC只能回收部分老年代的Region

  • Mixed GC过程中会将多个老年代Region中仍存活的小对象集中到一个Region中,也就是说Mixed GC会对老年代空间进行内存整理

  • G1 GC 提供了一种停顿时间预测模型,用户可以通过参数-XX:MaxGCPauseMillis设定每次GC的期望停顿时间

3.Concurrent Marking Cycle Phases:

Concurrent Marking Cycle Phases包括Global Concurrent Marking和Mixed GCs,具体步骤如下图,需要注意的是Global Concurrent Marking不是GC过程,并不会和Mixed GC同时发生,它只负责标记、统计出收集收益高的老年代region

Initial-mark:标记所有能直接可达的根对象,当达到IHOP阈值时并不会直接触发,而是等待下一次Young GC,利用其STW时间完成初始标记

Concurrent Marking:和应用程序线程并发的执行,从GC Roots开始对堆中的对象标记,标记出各个region中存活的对象

Remark:暂停所有应用线程,重新扫描堆中的对象,标记在并发标记阶段发生变化的对象;

Clean:统计有存活对象和没有存活对象的region,更新RSet,把完全空闲的region收集到可分配队列

Evacuation Reclaim:拷贝整理老年代存活对象的过程,需要暂停应用程序

>>>>

为什么选择G1GC

根据以上简述可以得出几个结论:

  • G1在停顿时间上添加了预测机制,用户可以设置期望停顿时间,Stop The World时间相对可控

  • G1的年轻代和老年代空间并不是固定的,当现有年轻代分区占满时,JVM会分配新的空闲Region加入到年轻代空间,老年代也是如此。还记得在CMS中出现的年轻代空间有大量浪费的情况吗?那将不再是问题。另一方面,由于Talos无法保证每台机器的吞吐完全一致,不同机器的GC压力也是不一样的,该特性使得Talos进程可以根据情况自行调整年轻代和老年代的空间大小,以应对不同的GC压力

  • G1 GC的回收过程中有内存整理,理论上不会产生内存碎片!

第三点是Talos目前最需要的特性,这也是之前CMS无法解决的问题,因此我们开始调研和尝试使用G1 GC。

>>>>

Talos+G1遇到的问题

在测试集群模拟线上压力,验证使用G1后的效果是否符合预期

1.测试环境

保持和线上一致的读写压力:单机写入流量 150MB/s,读取300MB/s

根据调研和CMS的使用经验,参数设置如下:

2.GC日志分析

在稳定运行一段时间后,测试集群开始出现由GC引起的请求堆积,通过gc_log_visualizer工具将GC日志可视化后进行分析,结果如下图:

对上图中的曲线做一些说明:

  • 截取了某天0点-11点的数据,横轴代表时间,纵轴代表内存使用情况,单位是MB

  • 在12~14G浮动的绿色曲线代表老年代使用的内存空间,可以看到老年代常驻内存对象长时间超过IHOP(20*65%=13GB)

  • 在200MB~2G浮动的紫色曲线代表年轻代使用的内存情况,年轻代非常小可能会导致Young GC非常频繁

  • 在13~16G浮动的黄色曲线代表整个堆的使用情况,可以看到堆空间还存在一定浪费,有大约4G左右的空间没有被合理利用

  • 蓝色的圆点代表发生了一次to-space exhausted,表示的意思是没有足够的空间复制对象,对象复制到survivor区和晋升老年代可能出现to-space exhausted

  • 在2G左右浮动的黄色方块代表一次Mixed GC回收的空间,可以看到每次回收的空间都比较小

下图表示GC在整个程序执行时间的占比,可以看到最高已经达到15%左右(CMS中该值为6%左右):

下图表示年轻代和老年代GC的停顿时间,可以看到:

  • Young GC最长达到了4.5秒

  • 7点前没有Mixed GC,之后Mixed GC非常频繁,一个小时大概出现60次并发标记周期,每个周期只发生2次MixedGC

3.调整思路

根据上述分析,调整思路如下:

  • 这里在初始设置时存在一个误解,和CMS不同的是,IHOP是老年代空间占堆的比例,而不是老年代中的使用率,因此将IHOP阈值调大为70%

  • Mixed GC太频繁,每次回收的内存又比较少,因此减小-XX:G1OldCSetRegionThresholdPercent为3%,并调大-XX:G1HeapWastePercent为10%,以此达到控制 Mixed GC 频率和单次耗时的目的

  • 年轻代太小,将-XX:G1MaxNewSizePercent调整为30%

调整后再使用gc_log_visualizer进行绘图,结果如下:

调整后的效果非常明显:

  • gc耗时有了显著下降,平均时间在70ms左右,减少了GC对Talos读写请求的影响 

  • 年轻代大小在不断调整,说明Young区压力适中,自适应性良好(若压力过大,STW耗时较长,则Young区会一直减小至G1NewSizePercent 附近;若压力太小,则young区会一直扩大至G1MaxNewSizePercent 附近) 

  •  40分钟左右触发1次并发标记周期,周期内发生8次Mixed GC,一共回收5G左右内存,说明此时old区和Mixed GC压力较小,大部分对象都在Young区得到了回收

  • GC耗时在应用程序执行时间中的占比为[0.1%~3.4%],吞吐性能良好

4.长达4秒的 Young GC

此时G1的性能已经基本满足Talos的需求了,但是还有一个隐患没有得到解决,就是长达4秒的Young gc还没有定位到原因。通过查看日志可以发现:这些4秒左右的GC其实都是Full GC(Allocation Failure)


   
  1. 2020-01-02T00:32:07.042+0800: 222101.087: [GC pause (G1 Evacuation Pause) (young)
  2. Desired survivor size 33554432 bytes, new threshold 3 (max 3)
  3. - age 1: 8913160 bytes, 8913160 total
  4. - age 2: 3161912 bytes, 12075072 total
  5. - age 3: 1664776 bytes, 13739848 total
  6. 222101.087: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 37632, predicted base time: 32.03 ms, remaining time: 57.97 ms, target pause time: 90.00 ms]
  7. 222101.087: [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 0 regions, survivors: 1 regions, predicted young region time: 2.34 ms]
  8. 222101.087: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 0 regions, survivors: 1 regions, old: 0 regions, predicted pause time: 34.37 ms, target pause time: 90.00 ms]
  9. 222101.089: [G1Ergonomics ( Heap Sizing) attempt heap expansion, reason: region allocation request failed, allocation request: 509312 bytes]
  10. 222101.089: [G1Ergonomics ( Heap Sizing) expand the heap, requested expansion amount: 509312 bytes, attempted expansion amount: 16777216 bytes]
  11. 222101.089: [G1Ergonomics ( Heap Sizing) did not expand the heap, reason: heap already fully expanded]
  12. 222101.376: [G1Ergonomics ( Concurrent Cycles) request concurrent cycle initiation, reason: occupancy higher than threshold, occupancy: 21474836480 bytes, allocation request: 0 bytes, threshold: 13958643660 bytes ( 65.00 %), source: end of GC]
  13. ( to- space exhausted), 0.2899120 secs]
  14. 2020 -01 -02T00: 32: 07.431+ 0800: 222101.476: [ Full GC (Allocation Failure) 15G-> 13G( 20G), 4.4796644 secs]
  15. [Eden: 0.0B( 400.0M)-> 0.0B( 400.0M) Survivors: 0.0B-> 0.0B Heap: 15.5G( 20.0G)-> 13.5G( 20.0G)], [Metaspace: 59784K-> 59784K( 1103872K)]
  16. Heap after GC invocations= 376434 ( full 561):
  17. garbage- first heap total 20971520K, used 14140603K [ 0x00000002c0000000, 0x00000002c1002800, 0x00000007c0000000)
  18. region size 16384K, 0 young ( 0K), 0 survivors ( 0K)
  19. Metaspace used 59784K, capacity 60254K, committed 61056K, reserved 1103872K
  20. class space used 6020K, capacity 6153K, committed 6272K, reserved 1048576K
  21. }
  22. [Times: user= 5.55 sys= 0.00, real= 4.48 secs]

这里有一个非常诡异的现象,occupancy: 21474836480 bytes 表示堆空间已经使用了20G,但是下一条日志中Heap: 15.5G(20.0G)->13.5G(20.0G)又显示堆空间只用了15.5G,难道是G1 GC计算错误导致的Full GC?

Google一下发现了这个Bug[https://bugs.openjdk.java.net/browse/JDK-8140597],JDK版本和Talos的使用场景都吻合,Talos使用的是OpenJDK_1.8.0,并且进程在处理读写请求时会不断产生巨型对象,这可能会强制中断Mixed GC,导致老年代完全没有被收集,直到触发Full GC,因此可以通过升级JDK解决。

5.监控打点

在测试和调优阶段,我们是通过绘图工具将GC日志转换为曲线图来分析的,但是在线上环境,不可能每次下载日志到本地再用工具绘图,因此 Talos 使用JMX(提供进程运行时获取堆使用情况、GC次数和GC耗时的方法)对进程的内存使用情况、GC次数和GC耗时进行了监控打点,效果如下:

这样一来,我们不仅解决了 CMS 周期性 Full GC 的问题,也填补了Talos在GC监控上的空白。

小结

将G1上线到线上集群运行7天后,读请求堆积情况如下,可以看出,G1 GC已经完全消除了周期性Full GC对读写请求的影响。

至此,Talos 在GC问题上经历的三个阶段就全部说完了,希望大家能够得到一些解决GC问题的思路,有任何问题都可以互相交流,欢迎吐槽 : )

参考文献

[1]Getting Started with the G1 Garbage Collector

[2]Garbage-First Garbage Collector Tuning

[3]G1: One Garbage Collector To Rule Them All

[4]HBase原理与实践-G1GC性能调优

[5]A generational Mostly-concurrent Garbage Collector

[6]Garbage-First Garbage Collection


我就知道你“在看”


转载:https://blog.csdn.net/pengzhouzhou/article/details/103917893
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场