Hbase調(diào)優(yōu):HBase 調(diào)整 Java 垃圾收集算法
?本文整理來自英特爾 Java 性能架構(gòu)師 Eric Kaczmarek 探討了如何針對(duì) 100% YCSB 讀取調(diào)整 Apache HBase 的 Java 垃圾回收 (GC) 背景:企業(yè)Hbase GC時(shí)間長(zhǎng),造成Hbase請(qǐng)求超時(shí)。
Apache HBase是一個(gè)提供 NoSQL 數(shù)據(jù)存儲(chǔ)的 Apache 開源項(xiàng)目。HBase 通常與 HDFS 一起使用,在世界范圍內(nèi)被廣泛使用。知名用戶包括 Facebook、Twitter、Yahoo 等。從開發(fā)人員的角度來看,HBase 是一個(gè)“分布式、版本化、非關(guān)系型數(shù)據(jù)庫,仿照 Google 的 Bigtable,一個(gè)用于結(jié)構(gòu)化數(shù)據(jù)的分布式存儲(chǔ)系統(tǒng)”。HBase 可以通過縱向擴(kuò)展(即部署在更大的服務(wù)器上)或橫向擴(kuò)展(即部署在更多服務(wù)器上)輕松處理非常高的吞吐量。 從用戶的角度來看,每個(gè)查詢的延遲非常重要。當(dāng)我們與用戶一起測(cè)試、調(diào)整和優(yōu)化 HBase 工作負(fù)載時(shí),我們現(xiàn)在遇到了很多真正想要 99% 操作延遲的人。
這意味著從客戶端請(qǐng)求到返回客戶端的響應(yīng)的往返行程,全部在 100 毫秒內(nèi)完成。 有幾個(gè)因素會(huì)導(dǎo)致延遲的變化。最具破壞性和不可預(yù)測(cè)的延遲入侵者之一是 Java 虛擬機(jī) (JVM) 的“stop the world”垃圾收集(內(nèi)存清理)暫停。 為了解決這個(gè)問題,我們使用 Oracle jdk7u21 和 jdk7u60 G1 (Garbage 1st) 收集器嘗試了一些實(shí)驗(yàn)。我們使用的服務(wù)器系統(tǒng)基于具有超線程(40 個(gè)邏輯處理器)的 Intel Xeon Ivy-bridge EP 處理器。它有 256GB DDR3-1600 RAM 和三個(gè) 400GB SSD 作為本地存儲(chǔ)。這個(gè)小型設(shè)置包含一個(gè)主站和一個(gè)從站,配置在一個(gè)節(jié)點(diǎn)上,負(fù)載適當(dāng)縮放。我們使用 HBase 版本 0.98.1 和本地文件系統(tǒng)來存儲(chǔ) HFile。HBase測(cè)試表配置為4億行,大小為580GB。
我們使用默認(rèn)的 HBase 堆策略:40% 用于 blockcache,40% 用于 memstore。YCSB 用于驅(qū)動(dòng) 600 個(gè)工作線程向 HBase 服務(wù)器發(fā)送請(qǐng)求 以下圖表顯示 jdk7u21 使用. 我們指定了要使用的垃圾收集器、堆大小和所需的垃圾收集 (GC)“停止世界”暫停時(shí)間。-XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100
在這種情況下,我們遇到了劇烈波動(dòng)的 GC 暫停。在初始峰值達(dá)到 17.5 秒后,GC 暫停的范圍從 7 毫秒到 5 整秒。下圖顯示了穩(wěn)態(tài)期間的更多詳細(xì)信息:
圖 2 告訴我們 GC 暫停實(shí)際上分為三個(gè)不同的組:(1) 在 1 到 1.5 秒之間;(1) 在 1 到 1.5 秒之間;(2) 0.007秒至0.5秒之間;(3) 尖峰在 1.5 秒到 5 秒之間。這很奇怪,所以我們測(cè)試了最近發(fā)布的jdk7u60,看看數(shù)據(jù)是否有任何不同: 我們使用完全相同的 JVM 參數(shù)運(yùn)行相同的 100% 讀取測(cè)試:.-XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100
Jdk7u60 大大提高了 G1 在穩(wěn)定階段處理初始尖峰后的暫停時(shí)間尖峰的能力。Jdk7u60 在一小時(shí)的運(yùn)行中產(chǎn)生了 1029 次年輕的和混合的 GC。GC 大約每 3.5 秒發(fā)生一次。Jdk7u21 進(jìn)行了 286 次 GC,每次 GC 大約每 12.6 秒發(fā)生一次。Jdk7u60 能夠?qū)和r(shí)間控制在 0.302 到 1 秒之間,而沒有出現(xiàn)大的峰值。 下面的圖 4 讓我們更仔細(xì)地觀察了穩(wěn)定狀態(tài)下 150 次 GC 暫停:
在穩(wěn)定狀態(tài)下,jdk7u60 能夠?qū)⑵骄鶗和r(shí)間保持在 369 毫秒左右。比jdk7u21好很多,但是還是達(dá)不到我們給的100毫秒的要求。–Xx:MaxGCPauseMillis=100 為了確定我們還能做些什么來獲得 1 億秒的暫停時(shí)間,我們需要更多地了解 JVM 的內(nèi)存管理和 G1(垃圾優(yōu)先)垃圾收集器的行為。下圖顯示了 G1 如何進(jìn)行 Young Gen 收集。
當(dāng) JVM 啟動(dòng)時(shí),根據(jù) JVM 啟動(dòng)參數(shù),它要求操作系統(tǒng)分配一個(gè)大的連續(xù)內(nèi)存塊來托管 JVM 的堆。該內(nèi)存塊由 JVM 劃分為多個(gè)區(qū)域。
如圖 6 所示,Java 程序使用 Java API 分配的每個(gè)對(duì)象首先進(jìn)入左側(cè)年輕代的 Eden 空間。一段時(shí)間后,Eden 變滿,觸發(fā)了 Young generation GC。仍然被引用(即“活著”)的對(duì)象被復(fù)制到 Survivor 空間。當(dāng)對(duì)象在年輕代中存活了幾次 GC 后,它們就會(huì)被提升到老年代空間。當(dāng) Young GC 發(fā)生時(shí),Java 應(yīng)用程序的線程會(huì)停止,以便安全地標(biāo)記和復(fù)制活動(dòng)對(duì)象。這些停止是臭名昭著的“停止世界”GC 暫停,這使得應(yīng)用程序在暫停結(jié)束之前沒有響應(yīng)。
老一代也會(huì)變得擁擠。在某個(gè)級(jí)別(由默認(rèn)為總堆的 45% 控制)會(huì)觸發(fā)混合 GC。它收集年輕一代和老一代?;旌?GC 暫停由年輕代在混合 GC 發(fā)生時(shí)清理所需的時(shí)間控制。-XX:InitiatingHeapOccupancyPercent=? 所以我們可以在 G1 中看到,“停止世界”GC 暫停主要取決于 G1 標(biāo)記和復(fù)制活動(dòng)對(duì)象到 Eden 空間之外的速度。考慮到這一點(diǎn),我們將分析 HBase 內(nèi)存分配模式將如何幫助我們調(diào)整 G1 GC 以獲得我們期望的 100 毫秒暫停。 在 HBase 中,有兩個(gè)內(nèi)存結(jié)構(gòu)消耗了它的大部分堆:用于BlockCache讀取操作的緩存 HBase 文件塊,以及緩存最新更新的 Memstore。
新對(duì)象形成LruBlockCache,Memstore首先進(jìn)入Young generation的Eden空間。如果它們存活的時(shí)間足夠長(zhǎng)(即,如果它們沒有被逐出LruBlockCache或從 Memstore 中清除),那么在幾次 GC 之后,它們就會(huì)進(jìn)入 Java 堆的老年代。當(dāng) Old generation 的可用空間小于給定threshOld(InitiatingHeapOccupancyPercent開始)時(shí),混合 GC 開始并清除 Old generation 中的一些死對(duì)象,從 Young gen 復(fù)制活動(dòng)對(duì)象,并重新計(jì)算 Young gen 的 Eden 和 Old gen 的HeapOccupancyPercent. 最終,當(dāng)HeapOccupancyPercent達(dá)到一定水平時(shí),F(xiàn)ULL GC會(huì)發(fā)生一個(gè)巨大的“停止世界” GC 暫停以清理 Old gen 中的所有死亡對(duì)象。 在研究了“”生成的 GC 日志之后,我們注意到在 HBase 100% 讀取期間,它從未增長(zhǎng)到足以引發(fā)完整 GC 的程度。我們看到的 GC 暫停主要由年輕一代“停止世界”暫停和隨時(shí)間增加的引用處理所主導(dǎo)。
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicyHeapOccupancyPercent?
完成該分析后,我們對(duì)默認(rèn)的 G1 GC 設(shè)置進(jìn)行了三組更改:
- Use開啟該標(biāo)志時(shí),GC在Young和mixed GC時(shí)使用多線程處理不斷增加的引用。使用 HBase 的這個(gè)標(biāo)志,GC 重新標(biāo)記時(shí)間減少了 75%,整體 GC 暫停時(shí)間減少了 30%。-XX:+ParallelRefProcEnabled
- Set -XX:-ResizePLAB and -XX:ParallelGCThreads=8+(logical processors-8)(5/8)Promotion Local Allocation Buffers (PLAB) 在 Young 收集期間使用。使用了多個(gè)線程。每個(gè)線程可能需要在 Survivor 或 Old 空間中為正在復(fù)制的對(duì)象分配空間。PLAB 需要避免線程競(jìng)爭(zhēng)管理空閑內(nèi)存的共享數(shù)據(jù)結(jié)構(gòu)。每個(gè) GC 線程都有一個(gè) PLAB 用于 Survival 空間,一個(gè)用于 Old 空間。我們希望停止調(diào)整 PLAB 的大小,以避免 GC 線程之間的大量通信成本,以及每次 GC 期間的變化。我們希望將 GC 線程的數(shù)量固定為 8+(logical processors-8)( 5/8). 這個(gè)公式是 Oracle 最近推薦的。通過這兩個(gè)設(shè)置,我們能夠在運(yùn)行期間看到更平滑的 GC 暫停。
- 將 100GB 堆的默認(rèn)值從 5更改為 1 根據(jù) 的輸出,我們注意到 G1 未能滿足我們期望的 100GC 暫停時(shí)間的原因是處理 Eden 所花費(fèi)的時(shí)間。換句話說,在我們的測(cè)試中,G1 平均需要 369 毫秒來清空 5GB 的 Eden。然后,我們使用標(biāo)志將 Eden 大小從 5 降低到 1。通過此更改,我們看到 GC 暫停時(shí)間減少到 100 毫秒。-XX:G1NewSizePercent-XX:+PrintGCDetails and -XX:+PrintAdaptiveSizePolicy-XX:G1NewSizePercent=
從這個(gè)實(shí)驗(yàn)中,我們發(fā)現(xiàn)G1清理Eden的速度約為每100毫秒1GB,或者對(duì)于我們使用的HBase設(shè)置,每秒10GB。
基于該速度,我們可以設(shè)置Eden 大小可以保持在 1GB 左右。例如:-XX:G1NewSizePercent=
- 32GB 堆,-XX:G1NewSizePercent=3
- 64GB 堆,-XX:G1NewSizePercent=2
- 100GB及以上堆,-XX:G1NewSizePercent=1
- 所以我們最終的 HRegionserver 命令行選項(xiàng)是:
- -XX:+UseG1GC
- -Xms100g -Xmx100g(我們測(cè)試中使用的堆大?。?/li>
- -XX:MaxGCPauseMillis=100(測(cè)試中所需的 GC 暫停時(shí)間)
- –XX:+ParallelRefProcEnabled
- -XX:-ResizePLAB
- -XX:ParallelGCThreads= 8+(40-8)(5/8)=28
- -XX:G1NewSizePercent=1
這是運(yùn)行 100% 讀取操作 1 小時(shí)的 GC 暫停時(shí)間圖表:
在此圖表中,即使是最高的初始穩(wěn)定峰值也從 3.792 秒減少到 1.684 秒。最開始的峰值不到 1 秒。結(jié)算后,GC 能夠?qū)和r(shí)間保持在 100 毫秒左右。下圖比較了 jdk7u60 在穩(wěn)定狀態(tài)下使用和不使用調(diào)優(yōu)的運(yùn)行情況:
我們上面描述的簡(jiǎn)單 GC 調(diào)整給出了理想的 GC 暫停時(shí)間,大約 100 毫秒,平均 106 毫秒和 7 毫秒標(biāo)準(zhǔn)偏差。
經(jīng)驗(yàn)總結(jié):
HBase 是一個(gè)響應(yīng)時(shí)間關(guān)鍵的應(yīng)用程序,它要求 GC 暫停時(shí)間是可預(yù)測(cè)和可管理的。使用 Oracle jdk7u60,根據(jù)報(bào)告的 GC 信息,我們能夠?qū)?GC 暫停時(shí)間調(diào)低到我們想要的 100 毫秒。-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy