Apache HBase內核深度剖析

前面一篇文章介紹了Kafka的具體內容,今天講述一下HBase相關的知識。首先HBase作為大數據發展初期伴隨Google三大論文問世的一個組件,在今天依舊被廣泛的應用,今天我們來仔細的分析一下HBase的內部原理,了解一下HBase的具體內幕,以便在工作中更好使用它。以下內容涉及到的源碼基於HBase 的Master分支編譯出的最新的3.0.0版本。

HBase相關算法與數據結構基礎知識

跳躍表

暫時先不說跳躍表是什麼,在Java裏面有一個Map叫:ConcurrentSkipListMap,通過對HBase的源碼跟蹤,我們發現這些地方使用了它:

簡單的列了幾個,但是觀察這幾個類所在的模塊就可以發現,HBase從客戶端,到請求處理,到元數據再到文件存儲貫穿HBase的整個生命周期中的各個重要環節,都能看到它的身影,Map那麼多,為何偏偏HBase選擇了這個?接下來我們仔細分析下。

在算法概念裏面有一種數據結構叫跳躍表,顧名思義,之所以叫跳躍表就是因為在查找的時候可以快速的跳過部分列表,用來提升查找效率,跳躍表的查找效率可以和二叉樹相比為O(log(N)),這個算法的實現在Java中的ConcurrentSkipListMap就是實現跳躍表的相關算法。

首先我們看一個有序的全量表:

(有序全量鏈表)

假設我們要從中找出可以發現需要比較的次數(比較次數不是循環次數)為<3,5,8>共計16次,可以看到對這樣一個有序鏈表進行查找比較的次數會非常多,那麼有沒有辦法對這種查找做優化。當然是有的,對於這種查找耳熟能詳從數據結構的基礎課程開始大家就知道二叉樹,折半查找,值查找都屬於解決這類問題的方法,自然跳躍表也是解決這類問題的方法之一。

跳躍表的思路和如今大部分大數據組件像kylin對海量數據下的快速查找的解決思路非常相似,都是通過某種邏輯提前將部分數據做預處理,然後查找的時候進行快速匹配,典型的空間換時間,那麼對於跳躍表來說,它的預處理的方式如下:

(跳躍表)

可以看到,跳躍表是按照層次構造的,最底層是一個全量有序鏈表,依次向上都是它的精簡版,而在上層鏈表中節點的後續下一步的節點是以隨機化的方式進行的,因此在上層鏈表中可以跳過部分列表,也叫跳躍表,特點如下:

  • 鏈表層從上往下查找
  • 跳躍表由很多層組成
  • 每一層都是一個有序鏈表
  • 最底層是全量有序鏈表
  • 如果一個元素出現在上層的某個節點那麼它一定會出現在下層的鏈表
  • 每一個元素都有兩個指針,一個指向當前鏈表的下一個元素,一個指向下一層鏈表的相同節點

假設根據前面的圖表我們要查詢G這個字母,那麼在上面的跳躍表中經過的路徑如下:

(跳躍表查找路徑)

其中紅色代表查找所走過的路徑。

LSM樹

前面講到了跳躍表的原理,在HBase中較大規模的使用了跳躍表,就是為了增快其查找效率,除了跳躍表之外HBase還使用到了LSM樹,LSM樹本質上和B+相似,是一種存儲在磁盤上的數據的索引格式,但是差異點在於LSM對寫入非常高效,實現來說就是無論什麼樣的寫入LSM都是當成一次順序寫入,這一點和HDFS的優點正好契合,HDFS不支持隨機寫,支持順序寫。LSM數據存儲在兩個地方,一個是磁盤上一個是內存中,內存中同樣使用的跳躍表,內存中是多個有序的文件。

(跳躍表與HFile關係)

HBase對LSM的應用採用了如上的結構方式,對於HBase具體的存儲文件的分析,在後面專門針對HBase的存儲部分進行深入的分析。

布隆過濾器

布隆過濾器解決的問題是,如何快速的發現一個元素是否存在於某個集合裏面,最簡單的辦法就是在集合或者鏈表上查找一遍,但是考慮到在大數據場景下,數據量非常大,即便不考慮性能,也不見得會有足夠多的機器來加載對應的集合。所以需要一種新的思路去解決此類問題,那麼布隆過濾器就是一種,它的思想為:

  • 由一個長度為N的數組構成,每一個元素為0或者1,默認都為0
  • 對集合的每個元素做K次哈希,到第i次的時候對N取一個模,會得到一個index
  • 將數組中的array[index]變為1

(布隆過濾器,來自網絡)

上圖是長度為18,進行3次哈希得到的結果,那麼在HBase中是如何利用布隆過濾器的呢,首先從操作來說,HBase的Get就經過布隆過濾器,同時HBase支持度對不同的列設置不同的布隆過濾器。

(布隆過濾器類型)

可以看到對HBase來講可以啟用或者禁用過濾器,對於不同的過濾器的實現分別在不同的類中,在查詢的時候分別根據不同的過濾器採用不同的實現類:

(布隆過濾器選擇邏輯)

所以可以通過如上的代碼找到對應的過濾器實現,甚至可以新增自己的過濾器。


HBase讀寫操作

前面提到HBase的相關算法,現在我們講一下HBase的整個操作的讀寫流程。首先,擺出HBase的架構圖,如下所示:

(HBase架構圖,來自網絡)

從這個圖可以看到,HBase的一個寫操作,大的流程會經過三個地方:1. 客戶端,2. RegionServer 3. Memstore刷新到磁盤。也就是說對於HBase的一次寫入操作來講,數據落到Memstore就算寫入完成,那麼必然需要考慮一個問題,那就是沒有落盤的數據,萬一機器發生故障,這部分數據如何保障不丟失。解析來我們逐步分解一下這三個部分。

客戶端:HBase的客戶端和服務器並不是單一的鏈接,而是在封裝完數據後,通過請求HMaster獲取該次寫入對應的RegionServer的地址,然後直接鏈接RegionServer,進行寫入操作,對於客戶端的數據封裝來講,HBase支持在客戶端設置本地緩存,也就是批量提交還是實時提交。因為HBase的hbase:meta表中記錄了RegionServer的信息,HBase的數據均衡是根據rowkey進行分配,因此客戶端會根據rowkey查找到對應的RegionServer,定義在Connection中:

而實現在:AsyncRegionLocator

RegionServer寫入:當客戶端拿到對應的RegionServer後,便和HMaster沒有關係了,開始了直接的數據傳輸,我們前面提到一個問題,那就是HBase如何防止數據丟失,畢竟HBase的寫入是到內存,一次請求就返回了,解決這個問題是通過WAL日誌文件來解決的,任何一次寫入操作,首先寫入的是WAL,這類日誌存儲格式和Kafka類似的順序追加,但是具有時效性,也就是當數據落盤成功,並且經過檢查無誤之後,這部分日誌會清楚,以保障HBase具有一個較好的性能,當寫完日誌文件後,再寫入Memstore。

那麼在RegionServer的寫入階段會發生什麼呢?首先我們知道,HBase是具有鎖的能力的,也就是行鎖能力,對於HBase來講,HBase使用行鎖保障對同一行的數據的更新要麼都成功要麼都失敗,所以在RegionServer階段,會經過以下步驟:

  1. 申請行鎖,用來保障本次寫入的事務性
  2. 更新LATEST_TIMESTAMP字段,HBase默認會保留歷史的所有版本,但是查詢過濾的時候始終只顯示最新的數據,然後進行寫入前提條件的檢查:

以上相關操作的代碼都在HRegion,RegionAsTable中,可以以此作為入口去查看,所以這裡就不貼大部分的代碼了。

  1. 寫入WAL日誌文件,在WALProvider中定義了兩個方法:

append用來對每一次的寫入操作進行日誌追蹤,因為有事物機制,所以HBase會將一次操作中的所有的key value變成一條日誌信息寫入日誌文件,aync用來同步將該日誌文件落盤到HDFS的文件系統,入場中間發生失敗,則立即回滾。

  1. 寫入Memstore,釋放鎖,本次寫入成功。

所以可以看到對於HBase來講寫入通過日誌文件再加Memstore進行配合,最後HBase自身再通過對數據落盤,通過這樣一系列的機制來保障了寫入的一套動作。

講完了HBase的寫入操作,再來看看HBase的讀取流程。

對於讀來講,客戶端的流程和寫一樣,HBase的數據不會經過Master進行轉發,客戶端通過Master查找到元信息,再根據元信息拿到meta表,找到對應的Region Sever直接取數據。對於讀操作來講,HBase內部歸納下來有兩種操作,一種是GET,一種是SCAN。GET為根據rowkey直接獲取一條記錄,而SCAN則是根據某個條件進行掃描,然後返回多條數據的過程。可以看到GET經過一系列的判斷,例如檢查是否有coprocessor hook後,直接返回了存儲數據集的List:

那麼我們再看SCAN就不那麼一樣了,可以看到,對於SCAN的操作來講並不是一次的返回所有數據,而是返回了一個Scanner,也就是說在HBase裏面,對於Scan操作,將其分成了多個RPC操作,類似於數據的ResultSet,通過next來獲取下一行數據。


HBase文件格式

前面講了HBase的操作流程,現在我們看下HBase的存儲機制,首先HBase使用的HDFS存儲,也就是在文件系統方面沒有自身的文件管理系統,所以HBase僅僅需要設計的是文件格式,在HBase裏面,最終的數據都是存儲在HFile裏面,HFile的實現借鑒了BigTable的SSTable和Hadoop的TFile,一張圖先展示HFile的邏輯結構:

(HFile文件格式-圖來自網絡)

可以看到HFie主要由四個部分構成:

  • Scanned block section:顧名思義,表示順序掃描HFile時所有的數據塊將會被讀取,包括Leaf Index Block和Bloom Block。
  • Non-scanned block section:表示在HFile順序掃描的時候數據不會被讀取,主要包括Meta Block和* Intermediate Level Data Index Blocks兩部分。
  • Load-on-open-section:這部分數據在HBase的region server啟動時,需要加載到內存中。包括FileInfo、Bloom filter block、data block index和meta block index。
  • Trailer:這部分主要記錄了HFile的基本信息、各個部分的偏移值和尋址信息。

對於一個HFile文件來講,最終落盤到磁盤上的時候會將一個大的HFile拆分成多個小文件,每一個叫做block塊,和HDFS的塊相似,每一個都可以自己重新設定大小,在HBase裏面默認為64KB,對於較大的塊,在SCAN的時候可以在連續的地址上讀取數據,因此對於順序SCAN的查詢會非常高效,對於小塊來講則更有利於隨機的查詢,所以塊大小的設置,也是HBase的調參的一個挑戰,相關的定義在源碼裏面使用的HFileBlock類中,HFileBlock的結構如下所示:

(HFileBlock結構,來自網絡)

每一個block塊支持兩種類型,一種是支持Checksum的,一種是不支持Checksum的,通過參數usesHBaseChecksum在創建block的時候進行設置:

HFileBlock主要包含兩個部分,一個是Header一個是Data,如下圖所示:

(HFileBlock結構,來自網絡)

BlockHeader主要存儲block元數據,BlockData用來存儲具體數據。前面提到一個大的HFile會被切分成多個小的block,每一個block的header都相同,但是data不相同,主要是通過BlockType字段來進行區分,也就是HFile把文件按照不同使用類型,分成多個小的block文件,具體定義在BlockType中,定義了支持的Type類型:

下面我們仔細分解一下HBase的Data部分的存儲,HBase是一個K-V的數據庫,並且每條記錄都會默認保留,通過時間戳進行篩選,所以HBase的K-V的格式在磁盤的邏輯架構如下所示:

(DataBlock結構,來自網絡)

每個KeyValue都由4個部分構成,而Key又是一個複雜的結構,首先是rowkey的長度,接着是rowkey,然後是ColumnFamily的長度,再是ColumnFamily,之後是ColumnQualifier,最後是時間戳和KeyType(keytype有四種類型,分別是Put、Delete、 DeleteColumn和DeleteFamily),而value相對簡單,是一串純粹的二進制數據。

最開始的時候我們介紹了布隆過濾器,布隆過濾器會根據條件減少和跳過部分文件,以增加查詢速度:

(布隆過濾器,來自網絡)

每一個HFile有自己的布隆過濾器的數組,但是我們也會發現,這樣的一個數組,如果HBase的塊數足夠多,那麼這個數組會更加的長,也就意味着資源消耗會更多,為了解決這個問題,在HFile裏面又定義了布隆過濾器的塊,用來檢索對應的Key需要使用哪個數組:

(布隆過濾器結構,來自網絡)

一次get請求進來,首先會根據key在所有的索引條目中進行二分查找,查找到對應的Bloom Index Entry,就可以定位到該key對應的位數組,加載到內存進行過濾判斷。


HBase RegionServer

聊完了HBase的流程和存儲格式,現在我們來看一下HBase的RegionServer,RegionServer是HBase響應用戶讀寫操作的服務器,內部結構如下所示:

(RegionServer結構)

一個RegionServer由一個HLog,一個BlockCache和多個Region組成,HLog保障數據寫入的可靠性,BlockCache緩存查詢的熱點數據提升效率,每一個Region是HBase中的數據表的一個分片,一個RegionServer會承擔多個Region的讀寫,而每一個Region又由多個store組成。store中存儲着列簇的數據。例如一個表包含兩個列簇的話,這個表的所有Region都會包含兩個Store,每個Store又包含Mem和Hfile兩部分,寫入的時候先寫入Mem,根據條件再落盤成Hfile。

RegionServer管理的HLog的文件格式如下所示:

(RegionServer管理HLog架構,來自網絡)

HLog的日誌文件存放在HDFS中,hbase集群默認會在hdfs上創建hbase文件夾,在該文件夾下有一個WAL目錄,其中存放着所有相關的HLog,HLog並不會永久存在,在整個HBase總HLog會經歷如下過程:

  • HLog構建:任何寫入操作都會先記錄到HLog,因此在發生寫入操作的時候會先構建HLog。
  • HLog滾動:因為HLog會不斷追加,所以整個文件會越來越大,因此需要支持滾動日誌文件存儲,所以HBase後台每間隔一段時間(默認一小時)會產生一個新的HLog文件,歷史HLog標記為歷史文件。
  • HLog失效:一旦數據進入到磁盤,形成HFile後,HLog中的數據就沒有存在必要了,因為HFile存儲在HDFS中,HDFS文件系統保障了其可靠性,因此當該HLog中的數據都落地成磁盤後,該HLog會變為失效狀態,對應的操作是將該文件從WAL移動到oldWAl目錄,此時文件依舊存在,並未進行刪除。
  • HLog刪除:hbase有一個後台進程,默認每間隔一分鐘會對失效日誌文件進行判斷,如果沒有任何引用操作,那麼此時的文件會被徹底的從物理刪除。

對於RegionServer來講,每一個RegionServer都是一個獨立的讀寫請求服務,因此HBase可以水平增加多個RegionServer來達到水平擴展的效果,但是多個RegionServer之間並不存在信息共享,也就是如果一個海量任務計算失敗的時候,客戶端重試後,鏈接新的RegionServer後,整個計算會重新開始。


HBase怎麼用

雖然HBase目前使用非常廣泛,並且默認情況下,只要機器配置到位,不需要特別多的操作,HBase就可以滿足大部分情況下的海量數據處理,再配合第三方工具像phoenix,可以直接利用HBase構建一套OLAP系統,但是我們還是要認識到HBase的客觀影響,知道其對應的細節差異,大概來說如果我們使用HBase,有以下點需要關心一下:

  1. 因為HBase在RegionServer對寫入的檢查機制,會導致客戶端在符合條件的情況下出現重試的情況,所以對於較為頻繁的寫入操作,或者較大數據量的寫入操作,推薦使用直接產生HFlie然後load到HBase中的方式,不建議直接使用HBase的自身的Put API。
  2. 從使用來講如果業務場景導致HBase中存儲的列簇對應的數據量差異巨大,那麼不建議創建過多的列簇,因為HBase的存儲機制會導致不同列簇的數據存儲在同一個HBase的HFile中,但是split機制在數據量增加較大的情況下,會發生拆分,則會導致小數據量的列簇被頻繁的split,反而降低了查詢性能。
  3. RegionServer是相互獨立的,所以如果想要讓集群更加的穩定高效,例如如果想實現RegionServer集群,達到信息共享,任務增量計算,需要自己修改RegionServer的代碼。
  4. 對於HBase來講,很多場景下,像如果Region正在Split,或者Mem正在Dump,則無法進行對應的操作,此時錯誤信息會被以異常的形式返回到客戶端,再由客戶端進行重試,因此在使用過程中,需要結合我們的應用場景,考慮如何設置類似於buffer大小的參數,以儘可能少的降低因為內部操作引起的客戶端重試,特別是在使用類似opentsdb的這類集成hhbase的數據的情況下。

結尾

HBase有着非常龐大的架構體系,和較為不錯的使用體驗,因此使用一篇文章通常很難講述清楚整個HBase內幕,但是我們可以根據理解逐步滲透到HBase內部,了解這個組件背後的原理,這樣當我們在使用它的時候就會變得更加的得心應手。經驗也不是一日構成,需要我們日復一日的不斷練習,在HBase不斷推出的新版下,琢磨和理解它的原理和架構。Apache下有非常多的組件可以實現差不多的功能,但是每一個組件又有着自己獨特的特點,本章我們介紹了HBase,後續會逐步分解介紹像Kylin,HDFS,Yarn,以及Atlas等組件。