1. 程式人生 > >消滅毛刺!HBase2.0全鏈路offheap效果拔群

消滅毛刺!HBase2.0全鏈路offheap效果拔群

阿里雲HBase2.0版本正式上線

阿里雲HBase2.0版本是基於社群2018年釋出的HBase2.0.0版本開發的全新版本。在社群HBase2.0.0版本基礎上,做了大量的改進和優化,吸收了眾多阿里內部成功經驗,比社群HBase版本具有更好的穩定性和效能,同時具備了HBase2.0提供的全新能力。HBase2.0提供的新功能介紹可以參照這篇文章。如果想要申請使用全新的HBase2.0版本,可以在此連結申請試用。在HBase2.0提供的眾多功能中,最引人注目的就是全鏈路的offheap能力了。根據HBase社群官方文件的說法,全鏈路的offheap功能能夠顯著減少JVM heap裡的資料生成和拷貝,減少垃圾的產生,減少GC的停頓時間。


線上業務在使用hbase讀寫資料時,我們可能會發現,HBase的平均延遲會很低,可能會低於1ms,但P999延遲(99.9%請求返回的最大時間)可能會高達數百ms。這就是所謂的"毛刺",這些毛刺可能會造成我們的線上業務出現部分請求超時,造成服務質量的下降。而對於HBase來說,GC的停頓,很多時候是造成這樣的毛刺的“罪非禍首”。那HBase2.0中的全鏈路offheap對減少GC停頓,降低P999延遲,真的有那麼神奇的功效嗎?

全鏈路offheap原理

在HBase的讀和寫鏈路中,均會產生大量的記憶體垃圾和碎片。比如說寫請求時需要從Connection的ByteBuffer中拷貝資料到KeyValue結構中,在把這些KeyValue結構寫入memstore時,又需要將其拷貝到MSLAB中,WAL Edit的構建,Memstore的flush等等,都會產生大量的臨時物件,和生命週期結束的物件。隨著寫壓力的上升,GC的壓力也會越大。讀鏈路也同樣存在這樣的問題,cache的置換,block資料的decoding,寫網路中的拷貝等等過程,都會無形中加重GC的負擔。而HBase2.0中引入的全鏈路offheap功能,正是為了解決這些GC問題。大家知道Java的記憶體分為onheap和offheap,而GC只會整理onheap的堆。全鏈路Offheap,就意味著HBase在讀寫過程中,KeyValue的整個生命週期都會在offheap中進行,HBase自行管理offheap的記憶體,減少GC壓力和GC停頓。


寫鏈路的offheap包括以下幾個優化:

  1. 在RPC層直接把網路流上的KeyValue讀入offheap的bytebuffer中
  2. 使用offheap的MSLAB pool
  3. 使用支援offheap的Protobuf版本(3.0+)

讀鏈路的offheap主要包括以下幾個優化:

  1. 對BucketCache引用計數,避免讀取時的拷貝
  2. 使用ByteBuffer做為服務端KeyValue的實現,從而使KeyValue可以儲存在offheap的記憶體中
  3. 對BucketCache進行了一系列效能優化

對比測試

全鏈路offheap效果怎麼樣,是騾子是馬,都要拿出來試試了。測試的準備工作和相關引數如下:

HBase版本

本次測試選用的1.x版本是雲HBase1.1版本截止目前為止最新的AliHB-1.4.9版本,2.x版本是雲HBase2.0版本截止目前為止最新的AliHB-2.0.1。這裡所有的版本號均為阿里內部HBase分支——AliHB的版本號,與社群的版本號無任何關聯。

機型

所有的測試都是針對一臺8核16G的ECS機器上部署的RegionServer。底層的HDFS共有兩個datanode(副本數為2),其中一個與該RegionServer部署在同一臺。每個datanode節點掛載了4塊150GB的SSD雲盤

測試工具

本次測試所用的是hbase自帶的pe工具,由於原生的PE工具不支援不支援單行put和指定batch put數量,因此我對PE工具做了一定的改造,並回饋給了社群,具體內容和使用方法參見這篇文章

表屬性

測試表的分割槽為64個,compression演算法為SNAPPY,Encoding設定為NONE。所有的region都只在一臺RegionServer上。

相關的HBase引數

共同引數

  • HBase的heap大小為9828MB,其中新生代區大小為1719MB
  • 使用的GC演算法為CMS GC,當老年代佔用大小超過75%時開始CMS GC。
  • hfile.block.cache.size 為0.4, 也就是說預設的lru cache的大小為3931.2MB
  • hbase.regionserver.global.memstore.size 為0.35, 即預設的memstore的大小為3439.8MB
  • 開啟了讀寫分離,在做寫相關的測試時,寫執行緒為90個,讀執行緒為10個。在做讀相關測試時(包括讀寫混合),寫執行緒為20個,讀執行緒為80個

HBase2.xoffheap相關引數

在測寫場景時,使用了HBase2.x的預設引數,即只開啟了RPC鏈路上的offheap,並沒有開始memstore的offheap。因為根據測試,我們發現開啟memstore的offheap並沒有帶來多大改善,究其原因,還是因為Memstore的offheap只是把KeyValue資料offheap,而Memstore本身使用的Java原生的ConcurrentSkipListMap,其索引結構會在JVM的heap中產生大量的記憶體碎片,因此只把KeyValue offheap的效果並不是很明顯。畢竟,在HBase-1.x開始,就有了MSLAB來管理Memstore中的KeyValue物件,記憶體結構已經比較緊湊。

在測讀場景時:

  • hbase-env.sh中設定HBASE_OFFHEAPSIZE=5G (RPC和HDFS 客戶端需要部分DirectMemory)
  • hbase.bucketcache.ioengine 調成offheap
  • hbase.bucketcache.size 調成 3911,即使用3911MB的DirectMemory來做L2 的cache來 cache data block(之前的測試發現L1中meta block index block的大小大約為20MB,所以在原來onheap的cache基礎上減去了20MB)
  • 由於cahce的一部分放入offheap,heapsize減至6290MB
  • block cache的比例不變,用來做L1 cache來cache META block(可能遠遠大約meta block的需求,但測試中只需保證meta block 100%命中即可,大了不會影響測試)

注意,本次測試旨在測試HBase2.x與HBase1.x版本在相同壓力下延遲和GC的表現情況,並非測試HBase的最大吞吐能力,因此測試所用的客戶端執行緒數也只限制在了60~64個,遠沒有達到雲HBase的最大吞吐能力

單行寫場景

單行寫測試時使用PE工具開啟64個寫執行緒,每個寫執行緒隨機往HBase表中寫入150000行,共960w行。每行的value size為200bytes。所用的PE命令為

hbase pe --nomapred --oneCon=true --valueSize=200 --compress=SNAPPY --rows=150000 --autoFlush=true --presplit=64 randomWrite 64
版本 TPS AVG RT 95% RT 99% RT 99.9% RT MAX RT
AliHB-1.4.9 38737 1.1ms 2ms 2ms 45ms 140ms
AliHB-2.0.1 40371 0.7ms 1ms 2ms 5ms 140ms
版本 AVG younggc Time younggc GC頻率
AliHB-1.4.9 90ms 0.6次/s
AliHB-2.0.1 110ms 0.28次/s

可以看到,使用了HBase-2.x的寫鏈路offheap後,單行寫的P999延遲從45ms降低到了5ms,效果非常明顯。同時吞吐有5%的提升,帶來這種效果的原因就是寫鏈路的offheap使HBase在heap的young區減少了臨時物件的產生,younggc發生的頻率從0.6次每秒降低到了0.28次每秒。這樣受到younggc影響的請求量也會大大減少。因此P999延遲急劇下降.

批量寫

在批量寫測試中,一次batch的個數是100。使用的命令為:

hbase pe --nomapred --oneCon=true --valueSize=200 --compress=SNAPPY --rows=200000 --autoFlush=true --presplit=64 --multiPut=100 randomWrite 64

測試的場景和引數配置與單行寫保持一致

版本 TPS AVG RT 95% RT 99% RT 99.9% RT MAX RT
AliHB-1.4.9 81477 72ms 110ms 220ms 350ms 420ms
AliHB-2.0.1 ​97985 ​67ms 75ms 220ms ​280ms 300ms
版本 AVG younggc Time younggc GC頻率
AliHB-1.4.9 120ms 0.6次/s
AliHB-2.0.1 180ms 0.28次/s

可以看到,使用了HBase-2.x的寫鏈路offheap後,從平均延遲到最大延遲,都有不同程度的下降,GC的頻率也降到1.x版本的一半以下。因此吞吐也上漲了20%。

100%Cache命中單行Get

在此場景中,先使用以下命令先往表中灌了120w行資料

hbase pe --nomapred --oneCon=true --valueSize=200 --compress=SNAPPY --rows=200000 --autoFlush=true --presplit=64 --multiPut=100 sequentialWrite  60

再保證所有資料刷盤,major compact成一個檔案後,先做cache的預熱,然後使用如下命令進行單行讀取:

 hbase pe --nomapred --oneCon=true --rows=200000 randomRead   60

測試結果如下:

版本 QPS AVG RT 95% RT 99% RT 99.9% RT MAX RT
AliHB-1.4.9​ 53895 ​0.04ms ​1ms ​1ms ​1ms 30ms
AliHB-2.0.1​ 49518 0.05ms ​​0ms ​1ms ​1ms ​14ms

注:百分比的延遲統計最低解析度是1ms,所以低於1ms時會顯示為0

版本 AVG younggc Time younggc GC頻率
AliHB-1.4.9​ ​25ms 0.4次/s
AliHB-2.0.1​ 8ms 0.35次/s

可以看到,在100%記憶體命中場景下,HBase2.x的吞吐效能有了8%的下滑。這是預料之中的,這在HBase的官方文件中也有解釋:讀取offheap的記憶體會比讀onheap的記憶體效能會稍稍下滑。另外,由於在100%記憶體命中的場景下,onheap的cache也不會發生置換,所以產生的gc開銷會比較小,所以在這個場景中,HBase1.x版本的P999延遲也已經比較低。但是,在這個GC不會很嚴重的場景裡(沒有寫,沒有開Block-encoding,cache裡內容不用decode可以直接使用),HBase2.x版本仍然可以把最大延遲降到1.x版本的一半,非常難能可貴。

部分cache命中單行讀

在這個場景中,先使用以下命令往表中灌了3600w行資料,這些資料會超過設定的cache大小,從而會產生一定的cache miss。
灌資料:

hbase pe --nomapred --oneCon=true --valueSize=200 --compress=SNAPPY --rows=600000 --autoFlush=true --presplit=64 --multiPut=100 sequentialWrite  60

再保證所有資料刷盤,major compact成一個檔案後,先做cache的預熱,然後使用如下命令進行單行讀取:

hbase pe --nomapred --oneCon=true --rows=600000 randomRead   60
版本 QPS AVG RT 95% RT 99% RT 99.9% RT MAX RT
AliHB-1.4.9​​ 14944 ​2.5ms ​5ms 80ms 200ms ​300ms
AliHB-2.0.1​ ​15372 ​1.7ms 5ms ​34ms 65ms ​130ms
版本 AVG younggc Time younggc GC頻率 CMS GC Remark AVG Time CMS GC 頻率
AliHB-1.4.9​​ 80ms 2.4次/s 50ms 0.25次/s
AliHB-2.0.1​ 21ms 2.5次/s 0 0

在部分cache命中的場景中,由於會有一定的cahce miss,在讀的過程中,會產生cache內容的置換。如果這些記憶體的置換髮生在heap裡,會顯著加重GC的負擔。因此,在這個GC壓力比較大的場景中,HBase2.x的全鏈路讀offheap產生了非常優秀的效果,無論是吞吐,平均延遲還是P999和最大延遲,都全面超越HBase1.x版本。由於cache不會在heap中產生垃圾,因此GC的頻率和耗時都顯著降低,基本消滅了CMSGC。更加難能可貴的是,使用了offheap的bucketcache由於每個bucket都是固定大小,因此在放入不定大小的data block時不可能完全放滿,從而會造成一些空間的浪費。因此雖然我把兩者的cache大小調到一樣的大小,HBase1.x的測試中,data block的命中率有58%,HBase2.x的測試中命中率只有40%。也就是說,HBase2.x在命中率更低的情況下,取得的吞吐和延遲都更加優秀!但這從另外一個方面說明,同樣的記憶體大小,在使用offheap功能後,cache的命中率會降低,因此使用offheap時最好使用速度更高的介質做儲存,比如本次測試中選用的SSD雲盤。保證讀取速度不會被落盤而拖慢太多。

讀寫混合測試

讀寫混合測試是大部分生產環境中面對的真實場景。大批量的寫和部分命中的讀都會產生GC壓力,兩者一起發生,GC壓力可想而知。
在這個測試中,灌資料和讀取和部分cache命中場景中使用的命令一致。只不過在讀取的同時,在另外一臺客戶端上起了一個20個執行緒的批量寫測試,去寫另外一個Table

hbase pe --nomapred --oneCon=true --valueSize=200 --table=WriteTable  --compress=SNAPPY --blockEncoding=DIFF  --rows=600000000 --autoFlush=true --presplit=64 --multiPut=100 randomWrite 20
版本 QPS AVG RT 95% RT 99% RT 99.9% RT MAX RT
AliHB-1.4.9​ 3945 ​11ms 5ms 180ms ​8700ms ​9000ms
AliHB-2.0.1​ 12028 ​2ms ​5ms ​45ms ​100ms 250ms

注:表中的QPS指的是讀的吞吐

版本 AVG younggc Time younggc GC頻率 CMS GC Remark AVG Time CMS GC 頻率 Full GC time(concurrent mode failure)
AliHB-1.4.9​ ​ 80ms 0.7次/s ​/(絕大部分退化成full gc) ​0.08次/s 約7~9s
AliHB-2.0.1 ​40ms 2.1次/s ​/ ​幾乎為0

在讀寫混合測試中,在此壓力下,CMS GC的速度已經跟不上heap中產生的垃圾的速度。因此在發生CMS時,由於CMS還沒完成時old區已經滿(concurrent mode failure),因此CMS GC都退化成了Full GC,從而產生了7到9s的‘stop the world’停頓。因此,1.x中P999被這樣的Full GC影響,P999已經上升到了8700ms。而由於HBase2.x使用了讀鏈路offheap。在此場景中仍然穩如泰山,CMS GC發生的頻率幾乎為0。所以在讀寫混合場景中,HBase2.x的吞吐是HBase1.x的4倍,P999延遲仍然保持在了100ms之內!

總結

通過上面的測試,我們發現HBase2.x的全鏈路offheap功能確實能夠降低GC停頓時間,在各個場景中,都顯示出了非常顯著的效果。特別是在部分cache命中和讀寫混合這兩個通常在生產環境中遇到的場景,可謂是效果拔群。所以說HBase2.x中的全鏈路offheap是我們在生產環境中去降低毛刺,增加吞吐的利器。

雲端使用

HBase2.0版本目前已經在阿里雲提供商業化服務,任何有需求的使用者都可以在阿里雲端使用深入改進的、一站式的HBase服務。雲HBase版本與自建HBase相比在運維、可靠性、效能、穩定性、安全、成本等方面均有很多的改進,歡迎大家通過下面的連線申請使用阿里雲HBase2.0版本,使用全鏈路offheap這個利器去給生產服務帶來更好的穩定性和服務質量。
https://www.aliyun.com/product/hbase