天天看點

Hbase調優:HBase 調整 Java 垃圾收集算法

作者:滌生大資料

本文整理來自英特爾 Java 性能架構師 Eric Kaczmarek 探讨了如何針對 100% YCSB 讀取調整 Apache HBase 的 Java 垃圾回收 (GC) 背景:企業Hbase GC時間長,造成Hbase請求逾時

Hbase調優:HBase 調整 Java 垃圾收集算法

Apache HBase是一個提供 NoSQL 資料存儲的 Apache 開源項目。HBase 通常與 HDFS 一起使用,在世界範圍内被廣泛使用。知名使用者包括 Facebook、Twitter、Yahoo 等。從開發人員的角度來看,HBase 是一個“分布式、版本化、非關系型資料庫,仿照 Google 的 Bigtable,一個用于結構化資料的分布式存儲系統”。HBase 可以通過縱向擴充(即部署在更大的伺服器上)或橫向擴充(即部署在更多伺服器上)輕松處理非常高的吞吐量。 從使用者的角度來看,每個查詢的延遲非常重要。當我們與使用者一起測試、調整和優化 HBase 工作負載時,我們現在遇到了很多真正想要 99% 操作延遲的人。

這意味着從用戶端請求到傳回用戶端的響應的往返行程,全部在 100 毫秒内完成。 有幾個因素會導緻延遲的變化。最具破壞性和不可預測的延遲入侵者之一是 Java 虛拟機 (JVM) 的“stop the world”垃圾收集(記憶體清理)暫停。 為了解決這個問題,我們使用 Oracle jdk7u21 和 jdk7u60 G1 (Garbage 1st) 收集器嘗試了一些實驗。我們使用的伺服器系統基于具有超線程(40 個邏輯處理器)的 Intel Xeon Ivy-bridge EP 處理器。它有 256GB DDR3-1600 RAM 和三個 400GB SSD 作為本地存儲。這個小型設定包含一個主站和一個從站,配置在一個節點上,負載适當縮放。我們使用 HBase 版本 0.98.1 和本地檔案系統來存儲 HFile。HBase測試表配置為4億行,大小為580GB。

我們使用預設的 HBase 堆政策:40% 用于 blockcache,40% 用于 memstore。YCSB 用于驅動 600 個工作線程向 HBase 伺服器發送請求 以下圖表顯示 jdk7u21 使用. 我們指定了要使用的垃圾收集器、堆大小和所需的垃圾收集 (GC)“停止世界”暫停時間。-XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100

Hbase調優:HBase 調整 Java 垃圾收集算法

在這種情況下,我們遇到了劇烈波動的 GC 暫停。在初始峰值達到 17.5 秒後,GC 暫停的範圍從 7 毫秒到 5 整秒。下圖顯示了穩态期間的更多詳細資訊:

Hbase調優:HBase 調整 Java 垃圾收集算法

圖 2 告訴我們 GC 暫停實際上分為三個不同的組:(1) 在 1 到 1.5 秒之間;(1) 在 1 到 1.5 秒之間;(2) 0.007秒至0.5秒之間;(3) 尖峰在 1.5 秒到 5 秒之間。這很奇怪,是以我們測試了最近釋出的jdk7u60,看看資料是否有任何不同: 我們使用完全相同的 JVM 參數運作相同的 100% 讀取測試:.-XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100

Hbase調優:HBase 調整 Java 垃圾收集算法

Jdk7u60 大大提高了 G1 在穩定階段處理初始尖峰後的暫停時間尖峰的能力。Jdk7u60 在一小時的運作中産生了 1029 次年輕的和混合的 GC。GC 大約每 3.5 秒發生一次。Jdk7u21 進行了 286 次 GC,每次 GC 大約每 12.6 秒發生一次。Jdk7u60 能夠将暫停時間控制在 0.302 到 1 秒之間,而沒有出現大的峰值。 下面的圖 4 讓我們更仔細地觀察了穩定狀态下 150 次 GC 暫停:

Hbase調優:HBase 調整 Java 垃圾收集算法

在穩定狀态下,jdk7u60 能夠将平均暫停時間保持在 369 毫秒左右。比jdk7u21好很多,但是還是達不到我們給的100毫秒的要求。–Xx:MaxGCPauseMillis=100 為了确定我們還能做些什麼來獲得 1 億秒的暫停時間,我們需要更多地了解 JVM 的記憶體管理和 G1(垃圾優先)垃圾收集器的行為。下圖顯示了 G1 如何進行 Young Gen 收集。

Hbase調優:HBase 調整 Java 垃圾收集算法

當 JVM 啟動時,根據 JVM 啟動參數,它要求作業系統配置設定一個大的連續記憶體塊來托管 JVM 的堆。該記憶體塊由 JVM 劃分為多個區域。

Hbase調優:HBase 調整 Java 垃圾收集算法

如圖 6 所示,Java 程式使用 Java API 配置設定的每個對象首先進入左側年輕代的 Eden 空間。一段時間後,Eden 變滿,觸發了 Young generation GC。仍然被引用(即“活着”)的對象被複制到 Survivor 空間。當對象在年輕代中存活了幾次 GC 後,它們就會被提升到老年代空間。當 Young GC 發生時,Java 應用程式的線程會停止,以便安全地标記和複制活動對象。這些停止是臭名昭著的“停止世界”GC 暫停,這使得應用程式在暫停結束之前沒有響應。

Hbase調優:HBase 調整 Java 垃圾收集算法

老一代也會變得擁擠。在某個級别(由預設為總堆的 45% 控制)會觸發混合 GC。它收集年輕一代和老一代。混合 GC 暫停由年輕代在混合 GC 發生時清理所需的時間控制。-XX:InitiatingHeapOccupancyPercent=? 是以我們可以在 G1 中看到,“停止世界”GC 暫停主要取決于 G1 标記和複制活動對象到 Eden 空間之外的速度。考慮到這一點,我們将分析 HBase 記憶體配置設定模式将如何幫助我們調整 G1 GC 以獲得我們期望的 100 毫秒暫停。 在 HBase 中,有兩個記憶體結構消耗了它的大部分堆:用于BlockCache讀取操作的緩存 HBase 檔案塊,以及緩存最新更新的 Memstore。

Hbase調優:HBase 調整 Java 垃圾收集算法

新對象形成LruBlockCache,Memstore首先進入Young generation的Eden空間。如果它們存活的時間足夠長(即,如果它們沒有被逐出LruBlockCache或從 Memstore 中清除),那麼在幾次 GC 之後,它們就會進入 Java 堆的老年代。當 Old generation 的可用空間小于給定threshOld(InitiatingHeapOccupancyPercent開始)時,混合 GC 開始并清除 Old generation 中的一些死對象,從 Young gen 複制活動對象,并重新計算 Young gen 的 Eden 和 Old gen 的HeapOccupancyPercent. 最終,當HeapOccupancyPercent達到一定水準時,FULL GC會發生一個巨大的“停止世界” GC 暫停以清理 Old gen 中的所有死亡對象。 在研究了“”生成的 GC 日志之後,我們注意到在 HBase 100% 讀取期間,它從未增長到足以引發完整 GC 的程度。我們看到的 GC 暫停主要由年輕一代“停止世界”暫停和随時間增加的引用處理所主導。

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicyHeapOccupancyPercent

完成該分析後,我們對預設的 G1 GC 設定進行了三組更改:

  1. Use開啟該标志時,GC在Young和mixed GC時使用多線程處理不斷增加的引用。使用 HBase 的這個标志,GC 重新标記時間減少了 75%,整體 GC 暫停時間減少了 30%。-XX:+ParallelRefProcEnabled
  2. Set -XX:-ResizePLAB and -XX:ParallelGCThreads=8+(logical processors-8)(5/8)Promotion Local Allocation Buffers (PLAB) 在 Young 收集期間使用。使用了多個線程。每個線程可能需要在 Survivor 或 Old 空間中為正在複制的對象配置設定空間。PLAB 需要避免線程競争管理空閑記憶體的共享資料結構。每個 GC 線程都有一個 PLAB 用于 Survival 空間,一個用于 Old 空間。我們希望停止調整 PLAB 的大小,以避免 GC 線程之間的大量通信成本,以及每次 GC 期間的變化。我們希望将 GC 線程的數量固定為 8+(logical processors-8)( 5/8). 這個公式是 Oracle 最近推薦的。通過這兩個設定,我們能夠在運作期間看到更平滑的 GC 暫停。
  3. 将 100GB 堆的預設值從 5更改為 1 根據 的輸出,我們注意到 G1 未能滿足我們期望的 100GC 暫停時間的原因是處理 Eden 所花費的時間。換句話說,在我們的測試中,G1 平均需要 369 毫秒來清空 5GB 的 Eden。然後,我們使用标志将 Eden 大小從 5 降低到 1。通過此更改,我們看到 GC 暫停時間減少到 100 毫秒。-XX:G1NewSizePercent-XX:+PrintGCDetails and -XX:+PrintAdaptiveSizePolicy-XX:G1NewSizePercent=

從這個實驗中,我們發現G1清理Eden的速度約為每100毫秒1GB,或者對于我們使用的HBase設定,每秒10GB。

基于該速度,我們可以設定Eden 大小可以保持在 1GB 左右。例如:-XX:G1NewSizePercent=

  • 32GB 堆,-XX:G1NewSizePercent=3
  • 64GB 堆,-XX:G1NewSizePercent=2
  • 100GB及以上堆,-XX:G1NewSizePercent=1
  • 是以我們最終的 HRegionserver 指令行選項是:
  • -XX:+UseG1GC
  • -Xms100g -Xmx100g(我們測試中使用的堆大小)
  • -XX:MaxGCPauseMillis=100(測試中所需的 GC 暫停時間)
  • –XX:+ParallelRefProcEnabled
  • -XX:-ResizePLAB
  • -XX:ParallelGCThreads= 8+(40-8)(5/8)=28
  • -XX:G1NewSizePercent=1

這是運作 100% 讀取操作 1 小時的 GC 暫停時間圖表:

Hbase調優:HBase 調整 Java 垃圾收集算法

在此圖表中,即使是最高的初始穩定峰值也從 3.792 秒減少到 1.684 秒。最開始的峰值不到 1 秒。結算後,GC 能夠将暫停時間保持在 100 毫秒左右。下圖比較了 jdk7u60 在穩定狀态下使用和不使用調優的運作情況:

Hbase調優:HBase 調整 Java 垃圾收集算法

我們上面描述的簡單 GC 調整給出了理想的 GC 暫停時間,大約 100 毫秒,平均 106 毫秒和 7 毫秒标準偏差。

經驗總結:

HBase 是一個響應時間關鍵的應用程式,它要求 GC 暫停時間是可預測和可管理的。使用 Oracle jdk7u60,根據報告的 GC 資訊,我們能夠将 GC 暫停時間調低到我們想要的 100 毫秒。-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy

-server  -XX:+UseG1GC  
-XX:+UnlockExperimentalVMOptions 
 -XX:G1NewSizePercent=5 
  -XX:+ParallelRefProcEnabled 
 -XX:ConcGCThreads=8 
  -XX:ParallelGCThreads=16 
   -XX:G1HeapRegionSize=32m 
    -XX:G1MixedGCCountTarget=32  
    -XX:G1OldCSetRegionThresholdPercent=5  
    -XX:SurvivorRatio=4  
    -XX:InitiatingHeapOccupancyPercent=70 
    -XX:G1ReservePercent=15 
     -XX:G1HeapWastePercent=5 
      -XX:MaxGCPauseMillis=50  
      -XX:GCPauseIntervalMillis=100           

繼續閱讀