HBase的split分析

    HBase在新建一個表的時候,默認會把所有數據都會放在一個HRegion上,主節點HMaster根據一定的策略把HRegion分配到不同的HRegionServer從節點上,客戶端在進行讀寫操作的時候,就會訪問對應HRegionServer的HRegion。當HRegion的數據量超過閥值的時候,爲了防止單個熱點訪問帶來的壓力,HBase就會對HRegion進行split操作,一個父HRegion分爲兩個子HRegion,後續的數據寫入操作就會分配到兩個HRegion裏,減輕了單個熱點的負載。由此可以看到split是一種動態的負載平衡機制。如果對數據庫知識有過一定了解,就會發現這就是數據庫常用的sharding技術,只不過HBase提供了自動化處理,減輕維護開銷。另外split會產生額外的IO,實際實踐中也有手動關閉該特性,按照規模進行預先分配HRegion的做法。

具體實現

   split是把數據平均分配的操作:按照父HRegion的SplitKey(約爲rowKey範圍的中間值)把父HRegion分成兩個子HRegion後,同時會把父HRegion的數據也會重新寫入到兩個子HRegion上。另外子HRegion的rowKey範圍是父HRegion的各自一半,這樣後面的rowKey就會按照範圍插入對應的HRegion。split在執行過程中採取一些做法避免影響讀寫請求,由於split過程需要較長時間大量的IO操作,如果發生故障就需要有效的failover機制,防止數據處於不一致的狀態。整個步驟可以參考文章的這個圖,其中有些細節不同,但大致過程基本一致。


split入口

    當memstore flush操作後,HRegion寫入新的HFile或者HStore剛剛進行完compact操作後,這兩個操作都有可能產生較大的HFile,HBase就會調用CompactSplitThread.requestSplit判斷是否需要split操作。這個判斷如下:

  • 判斷整個HRegionServer所有的HRegion數量是否超過hbase.regionserver.regionSplitLimit(默認Integer.MAX_VALUE,即沒有限制)。
  • 當前HRegion所有HStore中包含的HFile最小數是否>=1
  • 嘗試獲取SplitKey:hbase:meta表(記錄HRegion信息的HBase表,只有單個HRegion)、或是正在恢復狀態的HRegion返回null。然後利用設置的策略判斷是否需要split操作。一般使用兩種策略:ConstantSizeRegionSplitPolicy以及IncreasingToUpperBoundRegionSplitPolicy(默認)。 
  • ConstantSizeRegionSplitPolicy:如果某個不包含Reference文件的HStore(Reference文件是split後產生的臨時引用文件,見後述),總大小(包含HFile的總大小)超過hbase.hregion.max.filesize(默認10G),則返回true。 IncreasingToUpperBoundRegionSplitPolicy:對於HRegionServer內所有屬於同一個表的HRegion的數n,如果某個不包含Reference文件的HStore,總大小超過[n*n*n*2*MemStoreFlushSize和hbase.hregion.max.filesize(10G)之間最小值],則返回true。例如,對於如果n=3,則split大小爲3^3*2*128M=6912M。可見如果Region數比較少的時候的可以儘早採取split。
  • 返回SplitPoint。返回HRegion裏總大小最大HStore的最大HFile的中間rowKey值。
split執行

    獲得SplitPoint後,CompactSplitThread就把split請求放到線程池執行。整個過程的每一個步驟都會有具體的日誌記錄,方便在split過程中失敗的回滾。具體過程如下:

1、獲取zk上的表的全局讀鎖,默認等待600s。這是爲了避免和修改表發生衝突。修改表的操作會發送到HMaster執行,HMaster會獲取zk上表的全局寫鎖,這樣兩個操作就會互斥避免衝突。

    zk的table全局鎖實現:簡單來說,就是實現鎖請求隊列,隊列頭節點就是獲取了鎖的節點,readLock和writeLock按照自己在隊列的位置判斷是否獲取鎖。具體實現大致爲,以某個指定父節點,創建類型爲EPHEMERAL_SEQUENTIAL(EPHEMERAL指斷開zk連接會自動刪除節點,SEQUENTIAL會在子節點名字最後增加單調遞增的序號)的子節點,其名字如果readLock以read-作爲前綴,writeLock則以write-作爲前綴。接着獲取這個父節點的所有子節點,進行判斷:對於readLock,如果名字最後的序號比自己小的含有write-(表示前面有writeLock請求),則監聽這個序號最大的節點,如果被刪除了,則證明自己獲取了readLock。對於writeLock,如果自己不是序號最小的節點,則監聽比自己小的最大序號節點,如果被刪除,則自己獲取了writeLock。

2、創建split後的daughter region,A和B的HRegionInfo對象,用於後續操作。A和B的HRegion在split後對應父HRegion的每個HFile,都有一個Reference文件,內容爲指示Top或Bottom的標記,表示被劃分文件上/下部分,以及SplitKey。

3、創建Daughter Region。這個過程包含兩部分,一個是stepsBeforePONR;另一個是修改hbase:meta表內容。stepsBeforePONR中的PONR指point of no return,也就是不可逆,在這個過程中HBase的操作都會認爲是可逆的,在這之後的修改hbase:meta表內容就是非可逆的。

stepsBeforePONR

    (1)創建split對應的zk節點。zk創建/hbase/region-in-transition/regionName節點,節點的data爲split相關數據,type爲RS_ZK_REQUEST_REGION_SPLIT。不斷輪詢等待HMaster更換狀態爲RS_ZK_REGION_SPLITTING。就是利用ZK傳遞region split的信息給HMaster,讓HMaster獲知正在執行split操作。
    (2)在region的目錄裏創建.splits目錄 
    (3)關閉當前region。停止flush和compact操作,並等待進行中的flush和compact完成。如果所有memstore大於5mb,則flush。並行關閉所屬的HStore。
    (4)並行split所有的HFiles。 分別創建daughterRegion A和B的Reference文件(不能被split,compact的時候會被刪除),文件路徑.splits/daughterRegionName/familyName/storefileName.RegionEncodedName,然後用PB格式寫入Top/Bottom枚舉以及splitKey。Reference文件會在後面的compact操作中被刪除,然後纔會真正把split的內容寫入daughterRegion,這樣延遲寫入的操作可以避免發生故障需要回滾刪除文件從而造成IO浪費,Reference文件非常小,不會造成很明顯的影響。
    (5)創建daughter region對象。 同時把A和B的regionInfo寫到路徑.regioninfo下,然後把之前的.splits/daughterRegionName移動到table/region目錄,這樣daughterRegion就和parent同級目錄。然後創建A和B兩個HRegion對象。

修改hbase:meta表內容

    發送請求到hbase:meta表的HRegion,修改hbase:meta表的內容,表示原HRegion下線,daughterRegion A和B準備上線(暫時沒有location信息,因爲仍沒open)。另外如果修改失敗,則會進行回滾操作,把之前創建的.split目錄刪除,然後HRegionServer會終止服務,然後HMaster會負責清理其餘的狀態。

4、並行打開A,B兩個HRegion。創建對應的HStore,讀取Reference(Reference文件有專門的Scanner和reader來限制讀取對應HFile的範圍),從所有HFile裏讀取最大的MemStoreTS、SequenceId。HRegion成功打開後,就更新hbase:meta表中A和B的location。然後根據每個HFile的MaxSequenceId進行replay WALEdit,就是把內容重新寫入HStore的memstore。如果WALEdit的logNum裏小於MaxSequenceId則表明已經寫入HFile(每條記錄對應一個SequenceId),就會跳過。然後會請求一個異步Major compact,從Reference生成真正的HFile(異步Major compact會清理所有的Reference,參考這裏)。這個時候,這兩個daughterRegion已經可以對外提供讀服務。

5、 更新zk節點狀態。在zk上之前創建的/hbase/region-in-transition/regionName節點的type改爲RS_ZK_REGION_SPLIT,通知HMaster完成split操作。另外要注意,由於zk可能會丟失消息,因此這裏需要不斷循環節點狀態,當節點被HMaster刪除當時候才表明HMaster收到通知。

6、釋放表的全局讀鎖

     這樣,在CompactSplitThread的split操作就完成,在這個時候,兩個daughterRegion對外提供讀請求,父HRegion的文件仍然存在。等待major compact請求完成之後,兩個daughterRegion的Refernece文件就會被刪除。HMaster會定期監控hbase:meta表,一旦發現存在parentRegion且daughterRegion已經沒有Reference文件,則會刪除parentRegion的相關內容。這樣就最終完成了整個split的操作。

預Split操作

    在實際實踐過程中,如果可以根據業務大致確認表的數據規模,則可以使用預Split把表分成指定多個HRegion。這樣做可以避免Split過程帶來的IO開銷,並且在大批量導入數據的時候,可以讓集羣的多個節點分流寫請求,加快導入效率。具體做法如下:

    首先是關閉Split操作。從Split的過程可以看到,在判斷是否需要split的時候,有兩種可選的策略,我們這裏採取ConstantSizeRegionSplitPolicy,然後把hbase.hregion.max.filesize設置到一個非常大的值,這樣HBase就幾乎不會對任何HRegion採取split操作。

    然後就是在create表的時候,需要指定多個splitKey範圍,如:

hbase(main):015:0> create 'test_table', 'f1', SPLITS=> ['a', 'b', 'c']
   這樣就會創建4個HRegion,rowKey範圍分別爲[負無窮,a)、[a, b)、[b, c)、[c, 正無窮)。在導入數據的時候,客戶端就會根據自己的rowKey插入到對應的範圍裏。當然也要注意某些熱點數據會導致某個HRegion特別大,最好要監控好HRegion的讀寫請求數(可以從master提供的web頁面查看狀態)。如果發現某些熱點數據,可以利用命令行手動設置這個HRegion的split操作,HBase就會對這個HRegion進行split,保持負載平衡。

總結

    split操作提供了表自動sharding的功能,但這是以額外的IO消耗爲代價的。我們可以根據自己的業務需求進行預split或者手動split等操作。無論是哪種,這些split操作都相對較方便,能夠免除維護sharding帶來的一系列數據同步問題。

    split的操作依賴zookeeper保留transition info以及master和regionserver的通信。在zk之前曾經使用heartbeat向Master彙報狀態,但在不同地方出現了很多狀態不一致的問題。後來改用zk保證狀態一致性,但zk是one-time-trigger,即觸發監聽後必須重新設置監聽,重新監聽過程中可能產生消息丟棄(例如SplitTransaction.transitionZKNode採用多次循環判斷zk的節點狀態,確認HMaster接受信息)。另外,hbase的zk節點允許第三方app查看狀態,有安全性問題。目前社區也有Master中實現一個zab、raft、paxos等一致性庫的想法,以避免依賴zk產生的問題。具體可以參考https://issues.apache.org/jira/browse/HBASE-10296

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