本文整理来自英特尔Java性能架构师EricKaczmarek探讨了如何针对100YCSB读取调整ApacheHBase的Java垃圾回收(GC)背景:企业HbaseGC时间长,造成Hbase请求超时 ApacheHBase是一个提供NoSQL数据存储的Apache开源项目。HBase通常与HDFS一起使用,在世界范围内被广泛使用。知名用户包括Facebook、Twitter、Yahoo等。从开发人员的角度来看,HBase是一个分布式、版本化、非关系型数据库,仿照Google的Bigtable,一个用于结构化数据的分布式存储系统。HBase可以通过纵向扩展(即部署在更大的服务器上)或横向扩展(即部署在更多服务器上)轻松处理非常高的吞吐量。从用户的角度来看,每个查询的延迟非常重要。当我们与用户一起测试、调整和优化HBase工作负载时,我们现在遇到了很多真正想要99操作延迟的人。 这意味着从客户端请求到返回客户端的响应的往返行程,全部在100毫秒内完成。有几个因素会导致延迟的变化。最具破坏性和不可预测的延迟入侵者之一是Java虚拟机(JVM)的stoptheworld垃圾收集(内存清理)暂停。为了解决这个问题,我们使用Oraclejdk7u21和jdk7u60G1(Garbage1st)收集器尝试了一些实验。我们使用的服务器系统基于具有超线程(40个逻辑处理器)的IntelXeonIvybridgeEP处理器。它有256GBDDR31600RAM和三个400GBSSD作为本地存储。这个小型设置包含一个主站和一个从站,配置在一个节点上,负载适当缩放。我们使用HBase版本0。98。1和本地文件系统来存储HFile。HBase测试表配置为4亿行,大小为580GB。 我们使用默认的HBase堆策略:40用于blockcache,40用于memstore。YCSB用于驱动600个工作线程向HBase服务器发送请求以下图表显示jdk7u21使用。我们指定了要使用的垃圾收集器、堆大小和所需的垃圾收集(GC)停止世界暂停时间。XX:UseG1GCXms100gXmx100gXX:MaxGCPauseMillis100 在这种情况下,我们遇到了剧烈波动的GC暂停。在初始峰值达到17。5秒后,GC暂停的范围从7毫秒到5整秒。下图显示了稳态期间的更多详细信息: 图2告诉我们GC暂停实际上分为三个不同的组:(1)在1到1。5秒之间;(1)在1到1。5秒之间;(2)0。007秒至0。5秒之间;(3)尖峰在1。5秒到5秒之间。这很奇怪,所以我们测试了最近发布的jdk7u60,看看数据是否有任何不同:我们使用完全相同的JVM参数运行相同的100读取测试:。XX:UseG1GCXms100gXmx100gXX:MaxGCPauseMillis100 Jdk7u60大大提高了G1在稳定阶段处理初始尖峰后的暂停时间尖峰的能力。Jdk7u60在一小时的运行中产生了1029次年轻的和混合的GC。GC大约每3。5秒发生一次。Jdk7u21进行了286次GC,每次GC大约每12。6秒发生一次。Jdk7u60能够将暂停时间控制在0。302到1秒之间,而没有出现大的峰值。下面的图4让我们更仔细地观察了稳定状态下150次GC暂停: 在稳定状态下,jdk7u60能够将平均暂停时间保持在369毫秒左右。比jdk7u21好很多,但是还是达不到我们给的100毫秒的要求。Xx:MaxGCPauseMillis100为了确定我们还能做些什么来获得1亿秒的暂停时间,我们需要更多地了解JVM的内存管理和G1(垃圾优先)垃圾收集器的行为。下图显示了G1如何进行YoungGen收集。 当JVM启动时,根据JVM启动参数,它要求操作系统分配一个大的连续内存块来托管JVM的堆。该内存块由JVM划分为多个区域。 如图6所示,Java程序使用JavaAPI分配的每个对象首先进入左侧年轻代的Eden空间。一段时间后,Eden变满,触发了YounggenerationGC。仍然被引用(即活着)的对象被复制到Survivor空间。当对象在年轻代中存活了几次GC后,它们就会被提升到老年代空间。当YoungGC发生时,Java应用程序的线程会停止,以便安全地标记和复制活动对象。这些停止是臭名昭著的停止世界GC暂停,这使得应用程序在暂停结束之前没有响应。 老一代也会变得拥挤。在某个级别(由默认为总堆的45控制)会触发混合GC。它收集年轻一代和老一代。混合GC暂停由年轻代在混合GC发生时清理所需的时间控制。XX:InitiatingHeapOccupancyPercent?所以我们可以在G1中看到,停止世界GC暂停主要取决于G1标记和复制活动对象到Eden空间之外的速度。考虑到这一点,我们将分析HBase内存分配模式将如何帮助我们调整G1GC以获得我们期望的100毫秒暂停。在HBase中,有两个内存结构消耗了它的大部分堆:用于BlockCache读取操作的缓存HBase文件块,以及缓存最新更新的Memstore。 新对象形成LruBlockCache,Memstore首先进入Younggeneration的Eden空间。如果它们存活的时间足够长(即,如果它们没有被逐出LruBlockCache或从Memstore中清除),那么在几次GC之后,它们就会进入Java堆的老年代。当Oldgeneration的可用空间小于给定threshOld(InitiatingHeapOccupancyPercent开始)时,混合GC开始并清除Oldgeneration中的一些死对象,从Younggen复制活动对象,并重新计算Younggen的Eden和Oldgen的HeapOccupancyPercent。最终,当HeapOccupancyPercent达到一定水平时,FULLGC会发生一个巨大的停止世界GC暂停以清理Oldgen中的所有死亡对象。在研究了生成的GC日志之后,我们注意到在HBase100读取期间,它从未增长到足以引发完整GC的程度。我们看到的GC暂停主要由年轻一代停止世界暂停和随时间增加的引用处理所主导。 XX:PrintGCDetailsXX:PrintGCTimeStampsXX:PrintAdaptiveSizePolicyHeapOccupancyPercent 完成该分析后,我们对默认的G1GC设置进行了三组更改:Use开启该标志时,GC在Young和mixedGC时使用多线程处理不断增加的引用。使用HBase的这个标志,GC重新标记时间减少了75,整体GC暂停时间减少了30。XX:ParallelRefProcEnabledSetXX:ResizePLABandXX:ParallelGCThreads8(logicalprocessors8)(58)PromotionLocalAllocationBuffers(PLAB)在Young收集期间使用。使用了多个线程。每个线程可能需要在Survivor或Old空间中为正在复制的对象分配空间。PLAB需要避免线程竞争管理空闲内存的共享数据结构。每个GC线程都有一个PLAB用于Survival空间,一个用于Old空间。我们希望停止调整PLAB的大小,以避免GC线程之间的大量通信成本,以及每次GC期间的变化。我们希望将GC线程的数量固定为8(logicalprocessors8)(58)。这个公式是Oracle最近推荐的。通过这两个设置,我们能够在运行期间看到更平滑的GC暂停。将100GB堆的默认值从5更改为1根据的输出,我们注意到G1未能满足我们期望的100GC暂停时间的原因是处理Eden所花费的时间。换句话说,在我们的测试中,G1平均需要369毫秒来清空5GB的Eden。然后,我们使用标志将Eden大小从5降低到1。通过此更改,我们看到GC暂停时间减少到100毫秒。XX:G1NewSizePercentXX:PrintGCDetailsandXX:PrintAdaptiveSizePolicyXX:G1NewSizePercent 从这个实验中,我们发现G1清理Eden的速度约为每100毫秒1GB,或者对于我们使用的HBase设置,每秒10GB。 基于该速度,我们可以设置Eden大小可以保持在1GB左右。例如:XX:G1NewSizePercent32GB堆,XX:G1NewSizePercent364GB堆,XX:G1NewSizePercent2100GB及以上堆,XX:G1NewSizePercent1所以我们最终的HRegionserver命令行选项是:XX:UseG1GCXms100gXmx100g(我们测试中使用的堆大小)XX:MaxGCPauseMillis100(测试中所需的GC暂停时间)XX:ParallelRefProcEnabledXX:ResizePLABXX:ParallelGCThreads8(408)(58)28XX:G1NewSizePercent1 这是运行100读取操作1小时的GC暂停时间图表: 在此图表中,即使是最高的初始稳定峰值也从3。792秒减少到1。684秒。最开始的峰值不到1秒。结算后,GC能够将暂停时间保持在100毫秒左右。下图比较了jdk7u60在稳定状态下使用和不使用调优的运行情况: 我们上面描述的简单GC调整给出了理想的GC暂停时间,大约100毫秒,平均106毫秒和7毫秒标准偏差。 经验总结: HBase是一个响应时间关键的应用程序,它要求GC暂停时间是可预测和可管理的。使用Oraclejdk7u60,根据报告的GC信息,我们能够将GC暂停时间调低到我们想要的100毫秒。XX:PrintGCDetailsXX:PrintGCTimeStampsXX:PrintAdaptiveSizePolicyserverXX:UseG1GCXX:UnlockExperimentalVMOptionsXX:G1NewSizePercent5XX:ParallelRefProcEnabledXX:ConcGCThreads8XX:ParallelGCThreads16XX:G1HeapRegionSize32mXX:G1MixedGCCountTarget32XX:G1OldCSetRegionThresholdPercent5XX:SurvivorRatio4XX:InitiatingHeapOccupancyPercent70XX:G1ReservePercent15XX:G1HeapWastePercent5XX:MaxGCPauseMillis50XX:GCPauseIntervalMillis100