在Uber的数据平台上运行查询可以让我们生成数据驱动各个层面的决策,从预测骑手需求在高流量事件中识别和解决瓶颈问题在驱动程序注册过程中.我们的Apache基于hadoop的数据平台摄食数百拍字节的分析数据以最小的延迟存储在一个建立在的Hadoop分布式文件系统(HDFS)。
我们的数据平台利用了几个开源项目(Apache蜂巢, Apache转眼间,以及Apache火花)的交互式和长时间运行的查询,服务于Uber不同团队的无数需求。所有这些服务都是用Java或Scala构建的,并运行在开源平台上Java虚拟机(JVM).
Uber在过去几年里的增长成倍增加了数据量和处理数据所需的相关访问负载,导致服务消耗了更多的内存。不断增加的内存消耗暴露了各种各样的问题,包括长时间的垃圾收集(GC)暂停,内存泄露,内存不足(伯父)例外,以及内存泄漏.
完善我们数据平台的这一核心领域,可以确保优步内部的决策者及时获得可操作的商业情报,让我们为用户提供最好的服务,无论是连接乘客与司机、餐厅与外卖人员,还是货运公司与运营商。
为了保持内部数据服务的可靠性和性能,需要调优GC参数和内存大小,并降低系统生成Java对象的速率。在此过程中,它帮助我们开发了围绕优化JVM的最佳实践,我们希望社区中的其他人能从中受益。
什么是JVM垃圾收集?
JVM运行在本地机器上,作为编写在其中执行的Java程序的操作系统。它将运行程序中的指令转换为在本地操作系统上运行的指令和命令。
JVM垃圾收集进程查看堆内存,确定哪些对象正在使用,哪些对象没有使用,并删除未使用的对象,以回收可用于其他目的的内存。JVM堆由更小的部分或代组成:年轻代、老一代和永久代。
在Young Generation中,所有新对象都被分配和老化,这意味着它们存在的时间被监控。当Young Generation用它分配的全部内存填满时,一个次要的垃圾收集发生。所有次要垃圾回收都是“停止世界”事件,这意味着JVM停止所有应用程序线程,直到操作完成。
Old Generation用于存储长期存在的对象。通常,为每个Young Generation对象设置一个阈值,当满足该年龄时,对象将移动到Old Generation。最终,老一代需要一个主要的垃圾收集,它可以是一个完整的或部分的“Stop the World”事件,这取决于在JVM程序参数中配置的垃圾收集的类型。
永久生成存储类或内嵌字符串。这不是为了让从旧代中幸存下来的物品永久保存下来。如果该区域即将满,将会有一个GC,它仍然被视为主要GC。
JVM垃圾收集器包括传统的串行GC,平行GC,并发标记扫描(CMS)GC,垃圾第一垃圾收集器(G1 GC),以及一些新的活力/ C4,谢南多厄河谷,动作.
收集垃圾的过程通常包括标记、清理和压缩阶段,但不同的收集器可能有例外。串行GC是一种基本的垃圾收集器,它在整个收集过程中停止应用程序,并以串行方式收集垃圾。并行GC以多线程方式执行所有步骤,从而提高了收集吞吐量。CMS GC试图通过与应用程序线程并发执行大多数垃圾收集工作来最小化暂停。G1 GC收集器是一个并行的、并发的、增量压缩的低暂停垃圾收集器。
使用这些传统垃圾收集器,当JVM堆大小增加时,GC暂停时间通常会增加。这个问题在大型服务中更为严重,因为它们通常需要一个大的堆,例如几百gb。
新的垃圾清洁工喜欢活力/ C4Shenandoah和ZGC试图解决这个问题,通过并发地运行收集阶段并比传统收集器更频繁地增量运行收集阶段来最小化暂停。此外,暂停时间不会随着堆大小的增加而增加,这对于数据基础设施中的大规模服务是可取的。
除了垃圾收集器之外,对象创建速率也会影响GC暂停的频率和持续时间。在这里,对象创建速率定义了在给定的时间范围内创建的以字节为单位的对象的数量(以秒为单位)。很容易理解,当速率提高时,会创建更多的对象并占用堆,从而更频繁地触发GC并导致更长的暂停。
根据我们在JVM上进行垃圾收集的经验,我们已经确定了其他从业者在使用此类系统时可以利用的五个关键要点。
调优HDFS NameNode内存和垃圾回收
在Uber,我们使用HDFS,它运行在具有高容错和高吞吐量的商用硬件上数据分析,存储和基础设施。例如,HDFS为我们的商业智能、机器学习、欺诈检测和其他需要快速访问匿名商业数据的活动。
HDFS的主/复制体系结构意味着HDFS集群由工作节点和单个活动的主服务器NameNode组成,HDFS集群的核心.NameNode跟踪集群内存中的元数据和运行状况信息。例如,我们在运行时将集群中每个文件和目录的元数据加载到NameNode内存中。NameNode也可以映射块复制到机器列表。
随着文件和工作节点数量的增加,NameNode会使用更多的内存。此外,NameNode负责处理文件操作请求,这也会消耗存储中间Java对象的内存。考虑到NameNode的所有活动,它需要大量的内存,在我们的一些集群中可能超过200g。
因为活动的主NameNode不能水平向外扩展,它是集群中所有请求的集中瓶颈。优步实施了各种倡议扩展我们的HDFS集群,以适应数据和请求的增长,包括使用Hadoop查看文件系统(ViewFs),升级HDFS,控制小文件的数量,使用观察者NameNode,创建NameNode联合会,并执行GC调优。然而,所有这些举措只是缓解了规模的挑战。当我们开始这个项目时,NameNode仍然是HDFS集群扩展的瓶颈。
GC调优是一个优化GC参数和堆生成大小的过程,以适应JVM内存的运行时使用,它已经被证明是减少NameNode暂停时间的有效方法,从而减少请求延迟并提高吞吐量。
NameNode并发标记扫描GC调优
在前一篇文章,我们讨论了我们的第一个GC调优工作。在这之后,我们执行了第二轮GC调优,并试验了一种新的GC方法,连续并发压缩集合(C4),是Azul System的Zing JVM的一个组件。
在GC日志里,我们发现了很多小GC暂停(>100毫秒),有些甚至更长时间的暂停(大于1秒)。这一发现让我们感到惊讶,因为JVM的堆设计应该使小GC暂停可以忽略不计,持续时间远远低于我们所看到的时间。这些服务中断减慢了HDFS RPC队列请求时间(HDFS性能的典型度量标准)和端到端延迟。这些问题表明JVM的内存设置和GC参数没有得到优化,需要进行调优。
年轻代堆调优
我们的NameNode使用CMS作为JVM老一代GC,而年轻一代使用ParNew是《停止世界》(Stop the World)的收藏家。随着内存消耗的增加,我们观察到年老代和年轻代的对象都填满了堆。有时,我们注意到更频繁的GC发生与更大的内存使用。如果堆变得太大并且经常发生GC,通常的做法是增加堆的大小。
在进行GC调优工作的几个月前,我们将堆的总大小从120 gb增加到160 gb,以适应不断增长的内存需求。但是,所有其他参数,例如青年一代,保持不变。之后,我们注意到GC时间增加了,尤其是在Young Generation中。
生成的GC日志的图表GCViewer,如图1所示,我们发现ParNew GC是造成GC暂停的主要原因,在提高堆的总大小后,ParNew GC的平均时间增加了约35%。这个结果表明,我们应该首先从Young Generation GC调优开始,以实现最大的改进。
我们假设增大Old Generation的堆大小会影响ParNew的性能。为了测试这个想法,我们测试了Old Generation和Eden空间(Young Generation的一部分)之间的不同堆分配。我们尝试了一个总共50g的堆,其中6g分配给Eden空间,而另一个总共160g的堆,其中6g分配给Eden空间。这些评估验证了当Old Generation大小增加时,ParNew GC时间显著增加。
我们怀疑,当老一代的堆大小增加时,年轻一代GC增加的原因是因为系统必须扫描老一代的活动对象引用。基于这一理论,我们尝试了不同的Young Generation尺寸和参数,例如- xx: ParGCCardsPerStrideChunk,以提高ParNew GC速度。ParGCCardsPerStrideChunk控制分配在工作线程之间的任务的粒度,这些工作线程从老代为年轻代查找引用。如果没有正确设置块大小,可能会影响GC性能。
由于很难复制用于测试的生产文件系统,所以我们基于NameNode通信模式设计了测试。例如,listStatus,contentSummary,并且写操作比其他文件系统操作产生更多的内存压力,因此测试集中于使用这三种操作。为了创建大量的操作,我们在生产集群中使用了大型Apache Spark作业,这可以在测试NameNode上生成大量的文件系统操作。
我们在我们的复制系统上测试了三组参数,如下所示:
- 当前JVM设置:-Xmx160g -Xmn7.4g
- 当前JVM设置:-Xmx160g -Xmn7.4g -XX:ParGCCardsPerStrideChunk=32k
- 将Young Generation增加到16g:-Xmx160g -Xmn16g -XX:ParGCCardsPerStrideChunk=32k
下面图2中的图表比较了三组不同参数对NameNode的RPC队列平均时间及其GC时间/计数的影响。
这些评估表明,我们的第三个参数产生了最好的性能:RPCQueueTimeAvgTime从超过500毫秒下降到400毫秒,RPCQueueTimeNumOps从8000增加到12000,最大GC Time从22秒下降到1.5秒,最大GC计数从90下降到70。我们还尝试将Young Generation增加到32g,但是我们看到了更糟糕的性能,所以没有在图2中包含这个结果。
此外,我们可以得出这样的结论:当堆的总大小增加时,Young Generation大小也需要增加。否则,JVM在GC方面不能很好地执行,对HDFS的性能产生负面影响。
老一代堆调优
Old Generation堆设置中的一个重要参数是CMSInitiatingOccupancyFraction,它设置一个阈值,超过该阈值将触发下一次垃圾收集。占用率定义为Old Generation堆的占用率。
在GC调优工作开始之前,该值被设置为40。我们取了这个参数的GC日志的一个示例,看看是否有优化它的空间。
下面的图3显示了在CMS GC标记过程的初始标记和最终注释阶段之间的老一代增长率(以字节为单位的大小增长率)。在一个小时的时间范围内,老一代的累积增长约为4gb。考虑到我们的老一代使用了160g中的107g,这一比例为107/160(67%)。假设没有Old Generation GC,一个小时的增长,包括4gb的Old Generation增长,将达到111/160(69%)的比率。
GCViewer显示了相同小时内的统计数据,如下面的图4所示:
我们可以增加CMSInitiatingOccupancyFraction参数,但是这样做可能会有延长标记暂停(垃圾收集的一个阶段)的风险。由于CMS在此配置中没有占用太多暂停时间(占总暂停时间的3.2%),所以我们可以保持它的原样。
CMSInitiatingOccupancyFraction,是一个重要的GC参数,影响GC发生的频率和每个GC的持续时间。为了找到它的最佳性能设置,我们需要分析两个GC统计数据,即Old Generation增长率和Old Generation GC暂停时间的百分比。在我们的例子中,我们没有看到优化这个参数的太多空间,所以让它保持原样。
我们评估的其他参数
除了我们评估的上述GC参数之外,还有其他几个可能影响GC性能的参数,例如TLABSize而且ConcGCThreads.
我们评估TLABSize查看缓冲区的大小是否会影响GC性能。TLAB代表T头本地分配缓冲区,这是Young Generation伊甸园空间中的一个区域,专门分配给线程,以避免线程同步等资源密集型操作。TLAB的大小由该参数配置TLABSize.
Java飞行记录仪(用于收集关于正在运行的Java应用程序的诊断数据的工具)在下面的图5中显示了JVM自动TLAB调整大小的良好效果。NameNode在默认情况下不会生成大量的大型对象,它的浪费率仅在1.1%左右,因此我们保持默认设置。
我们还计算了参数ConcGCThreads,它定义GC将并发使用的线程数量,以查看GC线程的并发性是否会影响GC性能。此参数的大小越大,并发收集完成得越快。但是,如果一个主机上的cpu数量是固定的,则会更大ConcGCThreads输入数意味着更少的线程将工作在用户的应用程序上。我们通过将JVM默认的6个线程增加到12个,然后增加到24个线程来测试这个参数。我们没有注意到RPC队列平均时间有任何改进,所以我们保持原始设置。
在第二轮GC调优过程中,我们了解到,为了提高性能,我们需要不断地监视文件系统更改,理解NameNode进程的特征,并基于它们调优参数,这比测试每个大量的JVM参数更有效。
实验C4垃圾回收
在我们对NameNode进行了如上所述的第二轮GC调优之后,由Uber的增长所刺激的数据增长进一步增加了NameNode内存的压力。G1 GC是一个低暂停的垃圾收集器,它已经成为Java版本9中的默认垃圾收集器,在Uber中被广泛使用。我们通过模拟生产负载来测试G1 GC,但是我们观察到它使用了非常大的堆外内存。例如,在NameNode中有200g的JVM保留堆内存,G1 GC需要大约另外150g的堆外内存,但性能仍然不如CMS。
在声称具有最小GC暂停的较新的GC方法中,我们决定看看Zing/C4。Zing是Azul Technologies的JVM的名称,C4(连续并发压缩收集的缩写)是它的默认垃圾收集器。
这种GC似乎很有希望满足我们的需求,因为它可以支持大量的堆大小(我们测试的堆大小达到650 gb),而没有显著的GC暂停(大多数时间小于3毫秒)。减少大堆大小的GC暂停对NameNode的性能有很大的好处,因为HDFS NameNode需要大的堆大小。
为了实现最小化暂停时间的目标,C4在应用程序线程的同时运行GC线程。(更多细节可以在Azul的网站上找到C4白皮书).
通过我们的评估,我们主要关注NameNode的关键指标——RPC队列延迟。在我们的测试中,C4在拥有40核CPU和256g物理内存的硬件上比CMS高出约30%,如下图6所示:
我们使用测功器这个工具模拟了一个拥有数千个节点的HDFS集群的性能特征,使用不到5%的生产所需硬件,并通过对目标NameNode重放我们的生产流量来进行评估,该目标NameNode配置为为JVM的堆大小预留200 g,实际堆使用约150g。
此外,Zing可以在CMS和G1 GC完全失败的情况下处理非常大的堆管理。我们在大约300和450 gb的堆空间上尝试了这些gc,方法是将所有inode增加一倍或两倍,表示文件和目录,NameNode。CMS甚至在启动NameNode进程时都遇到了困难,而Zing处理得很好。
值得注意的是,Zing在我们的测试中具有更强大的cpu或更多内核的机器上表现得特别好。在这些硬件配置中,它的GC线程运行得更快,从而产生更好的应用程序性能。
尽管C4有许多优点,但关于它的一个警告是,这种GC比CMS使用更多的堆外内存。对于已经存在低内存空间问题的服务器,管理员在采用C4之前需要留出更多的空间,以避免出现内存不足的异常。
总的来说,C4提供了有希望的结果,与CMS相比,延迟降低了29%,从约24毫秒到约17毫秒。此外,当使用更大的堆大小时,GC暂停时间不会增加,这是与传统垃圾收集器相比的优势。尽管我们发现C4使用了更多的堆外内存,但它的性能优势非常令人鼓舞,我们将此解决方案推荐给其他正在寻找JVM优化解决方案的团队。
通过优化JVM内存使用减少Hive Metastore延迟
除了HDFS NameNode的内存挑战,Apache蜂巢(我们数据平台内的另一个大型服务)也会遇到内存问题。Hive提供了类似sql的接口来查询存储在数据湖.Hive Metastore是Hive的两个主要组件之一,它存储表的所有元数据。除了为Hive自身提供元数据支持,Hive Metastore还在其他服务或查询引擎的数据基础设施中扮演着关键角色Apache火花而且转眼间.
规模和访问模式
正如上面HDFS NameNode部分所提到的,当jvm驱动的业务增长时,数据、元数据和访问元数据的请求也会增长。Hive Metastore作为所有元数据的真实来源,必须增加其内存使用量,以适应元数据大小和请求数量的增长。
例如,单个Hive Metastore实例每分钟大约处理1500到2000个请求,需要50 gb的堆大小。对于如此巨大的堆,GC暂停可能会显著影响性能。Hive Metastore上的延迟下降可以对依赖的关键服务(如Presto)产生放大效应。堆和GC需要很好地调整,以保持Hive Metastore的性能完好无损。
减少对象创建以改善API延迟
在一个特殊的事件中,我们得到了一个关于服务中糟糕的堆管理如何加剧最终用户延迟的实物教训。这一切都始于Hive Metastore内部用户报告的偶尔的高峰(约2到4秒)延迟,通常在100毫秒以内。对于像Presto这样的关键交互用例,这些峰值当然是不可容忍的。一个可以获得的调用时间长达3秒,而本应少于50毫秒。
延迟高峰可能是由许多因素造成的,最常见的是:
- 其中一个上游依赖项降级。
- 同步和队列冲突。
- 由于GC, JVM暂停。
我们排除了第一种可能性,因为上游依赖没有明显的退化。在查看了源代码之后,我们也排除了第二种可能性,因为正在调查的API没有任何类型的锁定。为了研究第三种可能性,我们需要分析GC日志。
GC日志提供关于堆运行状况和行为的重要信号,最重要的是,度量JVM的GC触发的全局暂停。有大量的UI工具可用于分析GC日志,例如GCeasy.
GCeasy的一份报告(如图7所示)显示,有2258个gc耗时在0到1秒之间,平均暂停时间约为177毫秒,还有一两个异常值。这些数字可以得出一个明确的结论:有很多非常频繁的短停顿,平均停顿时间太高了。
在这种特殊情况下,所有这些GC都属于年轻一代,每次GC发生时,Hive Metastore平均暂停时间为177毫秒。这个结果表明许多新对象创建得太快了。而它们没有被送到老一代的事实表明太多的物体被过早地摧毁了。从本质上说,垃圾的产生率太高了,大约是400mbps。
这种高垃圾创建率与高度动态振荡的堆模式相关,如下面的图8所示:
高垃圾创建率表明:
- 一个繁忙的应用程序,传入的请求太多。
- 低效的内存管理。
我们排除了第一种可能性,因为堆模式在不为任何流量服务的非生产实例中类似。
要调试第二种可能性,我们需要了解应用程序中的线程在做什么。类获取的线程转储jstack效用可以捕获所有线程及其调用堆栈和状态的快照。
线程转储显示,处于可运行状态的调度线程非常少。但是,即使在重复转储线程之后,其中一个线程也始终处于可运行状态。
与线程对应的代码是一个调度的度量收集器守护进程,它在循环中重复以下两个步骤:
- 获取所有JVM关键指标mbean.
- 将它们发布到TSDB服务器。
线程的后退应该是1秒,但是被错误地设置为1毫秒,导致mbeans度量调用每秒发生1000次,产生大量垃圾。
增加返回时间解决了这个问题,堆振荡减少了,如下图9所示:
正确设置线程的返回时间也大大降低了gc的数量,从2258降到143,如下图10所示:
对我们的内部数据用户来说,最好的部分是Hive Metastore延迟大幅下降,如下图11所示:
了解过多的对象创建是如何导致严重的系统退化的,我们可以找到一个看似简单的解决方案,即高效地设计应用程序以创建更少的对象。随后,GC降低了Young Generation暂停的速率,改善了整体系统延迟。对于其他遇到类似退化问题的人,我们强烈建议寻找通过Hive Metastore更精确地调优JVM内存的方法。
调优协调器垃圾收集器,提高Presto服务的可靠性
除了HDFS NameNode和Hive Metastore,我们还调优了Presto的协调器GC以提高其可靠性。我们使用开源分布式SQL查询引擎Presto对Apache Hadoop数据湖运行交互式分析查询。Uber的Presto生态系统由处理存储在Hadoop中的数据的各种节点组成,每个Presto集群都有一个编译SQL和调度任务的协调节点,以及一些联合执行任务的工作节点。
快速协调器JVM内存压力
Presto协调器是解析语句、计划查询和管理Presto工作节点的服务器。它有效地充当Presto安装的大脑,也是客户机连接到它以提交语句以执行的节点。
协调器的性能对整个集群和最终用户的请求有很大的影响。Presto还要求协调器分配大量内存来处理这些任务,在我们的例子中,是200gb的堆内存。
这种大的堆大小导致频繁的GC和长时间的GC暂停,进而导致高错误率和高的端到端延迟。我们采用G1 GC,因为它是一个低暂停的JVM垃圾收集器,但我们仍然看到频繁的长时间GC暂停。
两个事件让我们了解到如何减少Presto协调器GC中的长暂停并防止连续停顿完整的GCs.成功解决这些事件对工程师在性能和可靠性方面产生了积极的影响。
减少Presto协调器的长垃圾收集暂停时间
当系统用户通过Presto提交更多查询时,我们观察到错误率增加了。我们每周的平均错误率报告显示,它高达2.75%,这意味着只有97.25%的可靠性。在分析了服务日志和GC日志之后,我们发现Presto协调器的GC暂停时间很长,高达总运行时间的6.59%。在GC暂停期间,协调器将停止处理所有用户请求,从而导致更长的延迟和会话超时。
对GC暂停的进一步分析表明,很大一部分时间花在字符串重复数据删除,这是在Java版本8中引入的特性更新20从重复的字符串对象中节省内存。当启用此特性时,G1 GC访问字符串对象并存储其哈希值。当收集器检测到具有相同哈希码的另一个字符串对象时,它比较这两个字符串的char数组。如果匹配,则只使用一个字符数组,另一个字符数组将在G1 GC收集过程中收集。
JDK文档因为这个特性只最少地讨论基准测试和开销,相反,主要关注性能改进。根据我们的经验,我们发现当JVM堆大小达到几百gb时,开销可能会被放大。我们通过启用和禁用该特性来测试性能,以查看GC暂停时间是否显著下降。
禁用“字符串重复数据删除”使GC暂停时间从总运行时间的6.59%到3%,如下图12所示:
GC暂停时间的减少导致错误率从2.5%下降到0.73%。
防止连续的全垃圾回收
在另一起事故中,我们发现Presto协调器有时停止接受查询,并且不处理当前运行的查询。我们检查了服务日志,发现了WARN条目,如下所示:
2019 - 07 - 10 - t04:04:47.362z警告
continuoustaskstatusfetrer -20190710_040047_05467_jdjqr.8.45获取任务状态错误
运行jstack实用程序(用于Java线程的堆栈跟踪程序)在Presto协调器上确定有许多线程处于阻塞状态。通过检查GC日志,我们发现在协调器停止之前,JVM中有一系列长时间的GC暂停,然后出现内存不足的错误。上传GC日志到GCeasy来绘制GC暂停时间,我们发现当协调器停止时发生了几个完整的GC,如下图13所示:
进一步的调查显示,在每次完整GC之后,回收的字节很少,如下面的图14所示:
当每次全GC无法回收足够的字节继续时,它将触发另一次全GC,以此类推。在此循环重复几次之后,抛出内存不足的异常并停止服务。
通常,连续的完整gc是由JVM堆大小分配不足造成的,JVM堆大小小于应用程序所需的大小。要选择合适的堆大小,需要考虑以下几点:
- 通常,堆使用的JVM可用RAM应该少于75%。当然,除了分配给JVM的RAM之外,我们还需要考虑同一主机上的操作系统和其他进程将需要多少RAM。而且,如果它在Linux主机上运行,则需要考虑IO缓冲。
- 我们可以使用详细的GC日志来找出最大的内存占用。内存占用是完整GC回收所有垃圾时所利用的堆大小。通常,堆的总大小应该大于最大内存占用。考虑到流量可能会出现突发情况,我们将总堆大小设置为比内存占用大20%。
在堆大小增加10%之后,这个问题得到了解决,JVM堆稳定下来。
总之,我们发现字符串重复数据删除特性会增加GC暂停的额外时间,并导致很高的错误率。虽然我们发现,现有的大多数关于该特性的文档都关注于它节省内存的好处,但很少讨论它的副作用。此外,连续的完整gc是大规模服务中的常见问题。我们调查了一个发生这种情况的实例,发现调优堆大小可以解决这个问题。我们强烈建议处于类似情况的组织不要忽视字符串复制的影响,并考虑如何增加堆大小来弥补。
关键的外卖
通过我们维护和改进对Uber数据平台的查询支持的经验,我们学到了许多关于如何成功优化JVM内存和GC调优的关键经验。我们将这些知识整合为更细粒度的要点,以便在Uber的规模上提高JVM和GC性能,如下:
- 辨别是否需要JVM内存调优。对于HDFS NameNode、Hive Server2和Presto协调器等大型服务,JVM内存调优是提高性能、吞吐量和可靠性的有效方法。当GC暂停频繁超过100毫秒时,性能会受到影响,通常需要进行GC调优。在我们的GC调优场景中,我们看到HDFS吞吐量增加了约50%,HDFS延迟减少了约20%,Presto的每周错误率从2.5%下降到0.73%。
- 选择正确的堆总大小。JVM堆的总大小决定了JVM收集垃圾的频率和时间。实际大小显示为verbose GC日志中的最大内存占用。不正确的堆大小可能导致糟糕的GC性能,甚至触发内存不足异常。
- 选择正确的Young Generation堆大小。Young Generation大小应该在选择总堆大小之后确定。在设置堆大小之后,我们建议针对不同的Young Generation大小对性能度量进行基准测试,以找到最佳设置。通常,Young Generation的大小应该是总堆大小的20%到50%,但如果服务具有很高的对象创建率,则Young Generation的大小应该增加。
- 确定最具影响力的GC参数。有许多GC参数要调优,但通常更改一两个参数会产生重大影响,而其他参数则可以忽略不计。例如,我们发现年轻一代的规模和ParGCCardsPerStrideChunk显著地提高了性能,但是在更改时我们没有看到多大的差异TLABSize而且ConcGCThreads.在调优Presto GC时,我们发现字符串重复数据删除设置对性能影响最大。
- 测试下一代GC算法。在大型数据基础设施中,关键服务通常具有非常大的JVM堆大小,从几百gb到太字节不等。传统的GC算法难以处理这种规模,而且GC暂停时间很长。下一代GC算法,如C4、ZGC和Shenandoah,显示了有希望的结果。在我们的案例中,我们发现C4比CMS (P90 ~24ms)减少了延迟(P90 ~17ms)。
前进
虽然在过去两年中,我们通过调优数据基础设施中各种大型服务的JVM垃圾收集,在性能、吞吐量和可靠性方面改进了服务,但仍然有更多的工作要做。
例如,我们开始将C4 GC集成到生产中的HDFS NameNode服务中。如上所述,随着分期环境中令人鼓舞且有益的性能改进,我们相信C4将有助于防止NameNode瓶颈问题并减少请求延迟。
分布式应用程序(尤其是Apache Spark)中的GC调优是我们未来想要研究的另一个领域。例如,在我们的数据平台都是建立在Spark之上的,我们的Hive服务也依赖于Spark。JVM分析器,一个Uber开发的开源工具,可以帮助我们分析Spark中的GC性能,从而提高它的性能。
如果使用您的侦探技能来决定如何优化大数据内存对您有吸引力,请考虑一下申请我们团队的一个职位!






