HBase實踐 | HBase內核優化與吞吐能力建設

前言

公司的hbase集羣早先是基於社區1.2.4版本進行搭建的,在時延表現方面起初並不十分理想,受GC尖刺的影響非常嚴重,針對P99響應時延也只能給業務提供不高於100毫秒的SLA承諾,因此在公司層面接入hbase的業務普遍還是面向近線或者離線場景,而針對時延響應要求比較高的在線業務則沒有辦法提供能力支持。

近期隨着社區補丁的陸續合入,以及公司自研補丁的不斷集成,hbase在吞吐能力表現方面已經得到了非常巨大的改善,圖計算場景下針對多跳查詢已經可以達到3~7倍的能力提升,以下主要是在整個吞吐能力建設過程中,我們所做的一些改進與嘗試。

合理高效利用緩存

HBase原生提供了三種類型的緩存支持,分別是LruBlockCache,BucketCache以及MemcachedBlockCache。其中MemcachedBlockCache主要是藉助外部緩存系統來處理相應的塊緩存操作,而在hbase內部採用比較多的還是通過組合LruBlockCache和BucketCache來形成一種複合型的緩存模型,即CombinedBlockCache的實現。其中LruBlockCache主要用來緩存索引塊和布隆數據塊,其數據內容需要保存在堆內;而BucketCache主要用來保存數據塊以及LruBlockCache中淘汰的塊。不同於LruBlockCache,BucketCache是可以支持多種存儲媒介的,比如我們可以將數據保存在堆外,也可以將數據保存到硬盤或者PMEM設備上。即便是將數據保存到硬盤,其對應的訪問效率也是要優於HDFS的,因爲一方面我們可以利用操作系統的零拷貝功能,另一方面可以避免RPC遠程調用以及DN協議帶來的開銷。所以理想情況下HDFS可以只拿來做容災備份處理,而數據的訪問可以從cache層全部命中,因此需要提供一種大容量的緩存能力支持。

但是緩存容量大了以後有可能會帶來以下問題。以公司常用的機器配置模版爲例,通常每臺機器會掛載12塊盤,每塊盤提供5T存儲,因此每臺機器可對外提供約60TB的存儲容量。而如果以每個HFileBlock默認採用64KB存儲來估算的話,60TB的存儲大概需要有近百G的索引塊和布隆數據塊。由於LruBlockCache是基於堆內進行管理的,如果索引塊全部緩存到堆內,將極大增加堆內存的使用開銷。另一方面LruBlockCache所管理的緩存數據是需要通過GC來進行回收的,如果空間分配量過小,那麼緩存的驅逐頻率會更加頻繁,隨之而來的GC壓力也會變得更加明顯,尤其在啓用cacheOnWrite或者prefetchOnOpen特性時。

既然大數據容量場景下采用LruBlockCache不太能滿足我們的需求,那麼我們自然會想到能否採用堆外BucketCache來做替換處理,形成一種新的複合型BlockCache,如下圖所示:

在此模式下,L1層的BucketCache主要通過堆外內存進行管理,而L2層的BucketCache可通過SSD或PMEM進行管理,以此來解決大容量的緩存需求,同時也意味着我們需要針對BucketCache提供分層存儲的能力支持。在功能實現上,分層的BucketCache主要是通過CompositeBucketCache來進行封裝的,其延用了原生CombinedBlockCache的處理邏輯,只不過將L1緩存從FirstLevelBlockCache替換成了BucketCache。因此在類結構上我們只需將CombinedBlockCache的代碼上移到超類(即CompositeBlockCache),然後將CompositeBucketCache和CombinedBlockCache分別繼承該超類即可(目前代碼已提交社區,詳細可參考HBASE-23296)。

數據預熱處理

有了大容量的緩存能力支撐之後,我們希望把所有的索引塊和布隆數據塊全部緩存下來,以減少數據在檢索過程中對磁盤的seek操作。因爲在緩存不命中的情況下,對HFile的讀取有可能需要經過3次seek才能定位到目標想要的數據,這將極大降低讀取效率。

  1. 第一次seek定位到布隆數據塊,用來判斷目標記錄是否存在於該HFile中。

  2. 第二次seek定位到目標索引塊(如果索引有多個層級需要seek多次)。

  3. 第三次根據索引信息定位到目標數據塊。

爲此我們針對數據寫入開啓了cacheOnWrite以及prefetchOnOpen特性,並調整了部分緩存的預熱邏輯,其中包括:

  1. Region啓動加載HFile的過程中,對其大小閾值進行判斷,如果大於限定的閾值,緩存其索引塊和布隆數據塊;而如果沒有大於閾值,則將所有的塊都緩存下來。
    隨着時間的推移和整理操作的不斷迭代,歷史久遠的數據所在的HFile會越來越大,而其訪問頻率則有可能越來越低,因爲大部分業務場景訪問的數據都是最新生成的,所以這裏我們引入了閾值判斷。

  2. 針對整理操作執行同樣的處理,確保新HFile生成之前,其數據內容已在緩存中進行了預熱。

  3. 針對cacheOnWrite特性優化了內存使用(詳細可參考HBASE-23107)

  4. 針對數據讀取操作避免重複預熱。
    針對scan類型的查詢請求,在檢索HFile的過程中一開始是基於pread方式進行讀取的(基於統一的Reader流),當檢索數據量達到一定閾值之後需要切換成stream的方式進行讀取,在整個切換過程中需要重新構建出Reader實例並對load-on-open區域進行再次預熱,這樣便帶來了無謂的資源使用開銷。另外如果啓用了prefetchOnOpen特性,相關的數據塊還會再次進行預熱加載,在緩存使用方面將變得十分不友好,因此針對該問題我們做了相應的補丁修復處理,啓用修復後scan的性能得到了將近30%的提升(詳細可參考HBASE-22888)。

  5. 避免cacheOnWrite以及prefetchOnOpen產生數據重複預熱(詳細可參考HBASE-23355)。

讀寫鏈路GC優化

針對時延響應要求比較高的java系統,GC往往是最爲頭疼的問題,如果讀寫鏈路有大量的臨時對象創建,YGC的執行頻率將變得異常頻繁。而如果對象的使用空間管理不當,還很容易引發碎片問題,進而增加fullgc的觸發頻率。所有這些操作都將換來STW,進而影響整個讀寫鏈路的吞吐時延。

針對GC問題,一種比較好的改善方式是將佔用空間比較大或者使用頻率比較高的對象,採用池化的機制來進行管理,然後基於覆寫的方式將邏輯上已被釋放的空間進行再度利用,從而避免GC層面對象空間的不斷申請與釋放行爲。比如BucketCache針對block的緩存管理方式。

RS啓動過程中會預先分配出block可以使用的內存空間,後續這部分空間將常駐於內存,不參與GC回收。當某個不使用的block被驅逐後,我們可以在邏輯上將其標識爲可覆寫的狀態,這樣有後續的block緩存進來時便可以複用這部分空間,而無需在GC層面將其釋放回收掉。

在GC能力改善方面,社區在2.0之後的版本已經提供了一些非常優秀的補丁,比如:

  1. HBASE-11425
    將端到端的讀取鏈路offheap化處理,通過池化的機制來管理CellBlock報文的序列化與反序列化操作,並且從BucketCache取塊的過程不在需要從堆外拷貝到堆內。

  2. HBASE-15179
    將端到端的寫入鏈路offheap化處理,同時將memstore的chunkpool從堆內移到了堆外,大大縮減了RS進程的堆內存使用開銷。

  3. HBASE-14790
    針對WAL的寫入提供了扇出的能力支持,同時提供了面向ByteBuffer的寫入接口,而不像原生FSHLog只能面向byteArray,這樣便有效避免了WAL數據寫入需要有堆外拷貝到堆內的過程。

  4. HBASE-14918
    提供了in-memory-flush的能力支持,可週期性的將跳錶結構轉換成CellChunkMap,來降低ConcurrentSkipListMap帶來的overhead開銷。

  5. HBASE-21879
    當BlockCache未命中需要從HFile加載目標塊時,該補丁爲塊的加載提供了池化管理功能,避免了每次申請臨時空間來構建HFileBlock對象。

以上補丁已經全部backport回我們自己的版本,補丁啓用後堆內存空間的使用情況得到了極大的改善,臨時對象的申請與釋放頻率不再那麼頻繁,YGC的觸發頻率得到了顯著的下降。然而通過對RS進程進行profile發現,整個讀寫鏈路的GC優化其實還不夠徹底,在很多功能鏈路上還是遺漏了一些細節,比如:

  1. 客戶端向服務端發送put請求時,封裝KV數據的CellBlock報文並沒有採用池化的機制進行管理,每次需要申請臨時的字節數組來封裝
    無論是客戶端還是服務端都需要有對CellBlock報文執行序列化的操作,服務端主要體現在返回response信息給客戶端的過程,而客戶端體現在發送request請求到服務端的過程。服務端的序列化處理主要由之前所提到的HBASE-11425來提供,而針對客戶端組件還沒有提供類似的池化管理功能,爲此我們引入了netty的內存池來對其進行管理。
    社區在2.0版本提供了異步RPC功能,並基於netty對客戶端代碼做了相應重構,因此異步客戶端已基於netty內存池對CellBlock做了序列化管理,但是同步客戶端尚無此功能,爲此我們提交了相關的補丁修復到社區,詳細可參考HBASE-22905。

  2. 當BucketCache採用SSD來作爲存儲媒介時(IOEngine爲file),讀塊操作依然需要有從堆外拷貝到堆內的過程。
    啓用基於文件的BucketCache緩存之後,緩存塊的讀取主要是通過調用FileIOEngine#read來進行的,在對RS進程做profile時發現,有近80%的內存申請是由FileIOEngine#read操作觸發的,如圖所示:


    爲此,針對這部分內存申請,我們延用了HBASE-21879的處理方式,採用池化機制來對其進行管理,功能啓用後內存申請操作由80%下降到了5%,gc時延方面得到了近1倍的改良(改善後的火焰圖可參考HBASE-22802)。

  3. 開啓CacheOnWrite特性時,塊數據的緩存操作需要申請臨時字節數組來做數據暫存。
    同HBASE-22802的分析處理過程相類似,我們依然採用池化管理機制來規避這類問題,啓用堆外內存池管理之後,臨時空間的申請佔比由45%下降到了6%,相關的火焰圖可參考HBASE-23107中的附件。

  4. memstore執行flush操作生成HFile文件時,針對DFSPacket的寫入默認同樣沒有采用池化管理機制,每次都需要申請臨時的字節空間。
    針對DFSPacket的池化管理,HDFS已經內置了一個輕量級的內存池管理工具ByteArrayManager#Impl,但是默認是不開啓的,爲此我們需要在RS端調整dfs.client.write.byte-array-manager.enabled的參數值爲true。啓用ByteArrayManage之後DFSPacke的內存申請佔比從30%下降到了1%。

以上便是有關GC鏈路的一些優化處理,核心思想主要是採用池化管理機制來降低臨時對象的空間申請與釋放行爲,代碼層面主要是通過ByteBuffer池來進行空間管理並配合Unsafe的使用來跳過一些邊界檢查行爲。

批量查詢加大併發處理粒度

在實際應用中,爲了提升與服務端的交互能力,我們通常會將多個請求先彙總成一個批次,然後在統一發送到服務端去進行處理,通過降低與服務端的RPC交互頻率來換取對應的吞吐能力。典型的應用場景比如圖數據庫Janusgraph在查詢目標頂點的鄰接表信息時,便是向服務端發送一個multiget請求。

然而針對該類型的請求(multiget),服務端並沒有提供與之相對應的併發處理模型,請求到達服務端之後針對每個multiget將會採用單一的handler線程來串行處理其中的每一個get,如圖所示。

因此,針對批處理請求數量較小但是請求批次很大的場景,服務端資源並不能得到有效充分的利用。爲此我們可以針對multiget請求引入一個新的線程池模型,將批次中的每一個get請求分發到對應的線程池中去做處理,以此來增加multiget請求在服務端的併發處理粒度。啓用該功能以後,multiget的請求時延可以達到將近40%的性能提升,目前補丁已經提交至社區,相關的代碼邏輯可參考HBASE-23063。

YCSB壓測情況

爲了衡量HBase的吞吐能力效果,我們採用了統一基準測試YCSB對集羣進行了壓測,測試環境如下。

  1. 機器規模 測試過程中使用了2臺RS,每臺RS內存分配如下:堆內32G,堆外64G(其中memstore分配15G,L1層的BucketCache分配40G,堆外內存池分配5G),同時爲每臺RS掛載一塊SSD充當L2緩存,緩存容量爲2TB。

  2. 數據規模 通過YCSB向集羣導入20億行數據,每行10個KV,單KV大小爲100個字節,HFileBlock大小設置爲32KB,持久化到HDFS之後,單副本存儲容量約3TB,由於啓用了緩存預熱功能,數據導入成功後每臺RS的緩存使用如下:L1使用4.3G,L2使用1.6T。

測試過程主要針對multiget請求以及隨機get點讀兩種場景來進行,其中針對multiget請求我們對YCSB做了相應的定製處理,對應的測試結果如下。

隨機get點讀測試

單客戶端開啓40個線程併發執行1億次get,測試結果如下。

multiget批量讀測試

單客戶端開啓30個線程併發執行1000萬次multiget請求,每個multiget返回50行數據,測試結果如下:

(1)客戶端視角的端到端監控如下

從端到端的監控結果來看,P999時延可以穩定控制在50ms之內,由於每個multiget請求會返回50行數據,因此單行數據(每行10個KV,數據總量1KB)平均下來可達1ms。

(2) 服務端視角的監控如下

從服務端視角來看,單機get吞吐量達到6萬時,每秒GC時間平均可控制在6.5毫秒上下,且GC的整體表現非常平穩,P999時延不在受到GC尖刺的影響。

本文作者 陳旭,感謝來稿及對HBase社區做出的卓越貢獻。原文發表於chenxu的博客,文章鏈接https://chenxu14.github.io/2020/04/13/hbase-perfomance-improve.html(點擊閱讀原文進入)

往期推薦  點擊標題可跳轉

實時數倉 | 你想要的數倉分層設計與技術選型

HBase原理 | HBase內存管理之MemStore進化論

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章