HBase 內部探險
《HBase 不睡覺書》是一本讓人看了不會睡著的 HBase 技術書籍,寫的非常不錯,為了加深記憶,決定把書中重要的部分整理成讀書筆記,便於後期查閱,同時希望為初學 HBase 的同學帶來一些幫助。
目錄
-
第一章 - 初識 HBase
-
第二章 - 讓 HBase 跑起來
-
第三章 - HBase 基本操作
-
第四章 - 客戶端 API 入門
-
第五章 - HBase 內部探險
-
第六章 - 客戶端 API 的高階用法
-
第七章 - 客戶端 API 的管理功能
-
第八章 - 再快一點
-
第九章 - 當 HBase 遇上 MapReduce
本文內容略長,看的時候需要一些耐心。文章首先回顧了 HBase 的資料模型和資料層級結構,對資料的每個層級的作用和構架均進行了詳細闡述;隨後介紹了資料寫入和讀取的詳細流程;最後介紹了老版本到新版本 Region 查詢的演進。
一、資料模型
1、重要概念回顧
-
Namespace(表名稱空間):將多個表分到一個組進行統一管理。
-
Table(表):一個表由一個或者多個列族組成;資料屬性比如:超時時間(TTL),壓縮演算法(COMPRESSION)等,都在列族的定義中定義;定義完列族後表是空的,只有添加了行,表才有資料。
-
Row(行):一個行包含了多個列,這些列通過列族來分類;行中的資料所屬列族只能從該表所定義的列族中選取;由於 HBase 是一個列式資料庫,所以一個行中的資料可以分佈在不同的伺服器上。
-
Column Family(列族):列族是多個列的集合,HBase 會盡量把同一個列族的列放到同一個伺服器上,這樣可以提高存取效能,並且可以批量管理有關聯的一堆列;所有的資料屬性都是定義在列族上;在 HBase 中,建表定義的不是列,而是列族。
-
Column Qualifier (列):多個列組成一個行,列族和列經常用
Column Family: Column Qualifier
來一起表示,列是可以隨意定義的,一個行中的列不限名字、不限數量。 -
Cell(單元格):一個列中可以儲存多個版本的資料,而每個版本就稱為一個單元格(Cell),所以在 HBase 中的單元格跟傳統關係型資料庫的單元格概念不一樣;HBase 中的資料細粒度比傳統資料結構更細一級,同一個位置的資料還細分成多個版本。
-
Timestamp(時間戳/版本號):既可以把它稱為是時間戳,也可以稱為是版本號,因為它是用來標定同一個列中多個單元格的版本號的。不指定版本號的時候,系統會自動採用當前的時間戳來作為版本號;當手動定義了一個數字來當作版本號的時候,這個 Timestamp 就真的是隻有版本號的意義了。
2、幾個小問題
HBase是否支援表關聯?
官方給出的答案是乾脆的,那就是“不支援”。如果想實現資料之間的關聯,就必須自己去實現了,這是挑選 NoSQL 資料庫必須付出的代價。
HBase 是否支援 ACID?
ACID 就是 Atomicity(原子性)、Consistency(一致性)、Isolation(隔離性)、Durability(永續性)的首字母縮寫,ACID 是事務正確執行的保證,HBase 部分支援 了 ACID。
表名稱空間有什麼用?
表名稱空間主要是用來對錶分組,那麼對錶分組有什麼用?名稱空間可以填補 HBase 無法在一個例項上分庫的缺憾,通過名稱空間我們可以像關係型資料庫一樣將表分組,對於不同的組進行不同的環境設定,比如配額管理、安全管理等。
HBase 中有兩個保留表空間是預先定義好的:
-
hbase:系統表空間,用於組織 HBase 內部表;
-
default:那些沒有定義表空間的表都被自動分配到這個表空間下。
二、HBase 的儲存資料方式
1、架構回顧
一個 HBase 叢集由一個 Master(也可以把兩個 Master 做成 HighAvailable)和多個 RegionServer 組成。
-
Master:負責啟動的時候分配 Region 到具體的 RegionServer,執行各種管理操作,比如 Region 的分割和合並。HBase 中的 Master 的角色功能比其他型別叢集弱很多,HBase 的 Master 很特別,因為資料的讀取和寫入都跟它沒什麼關係,它掛了業務系統照樣執行。當然 Master 也不能宕機太久,有很多必要的操作,比如建立表、修改列族配置,以及更重要的分割和合並都需要它的操作。
-
RegionServer:RegionServer 上有一個或者多個 Region,我們讀寫的資料就儲存在 Region 上。如果你的 HBase 是基於 HDFS 的(單機 HBase 可基於本地磁碟),那麼 Region 所有資料存取操作都是呼叫了 HDFS 的客戶端介面來實現的。
-
Region:表的一部分資料,HBase 是一個會自動分片的資料庫,一個 Region 就相當於關係型資料庫中分割槽表的一個分割槽,或者 MongoDB 的一個分片。
-
HDFS:HBase 並不直接跟伺服器的硬碟互動,而是跟 HDFS 互動,所以 HDFS 是真正承載資料的載體。
-
ZooKeeper :ZooKeeper 在 HBase 中的比 Master 更重要,把 Master 關掉業務系統照樣跑,能讀能寫;但是把 ZooKeeper 關掉,就不能讀取資料了,因為讀取資料所需要的元資料表
hbase:meata
的位置儲存在 ZooKeeper 上。

2、RegionServer 內部架構
一個 RegionServer 包含有:
-
一個 WAL:預寫日誌,WAL 是 Write-Ahead Log 的縮寫,就是:預先寫入。當操作到達 Region 的時候,HBase 先把操作寫到 WAL 裡面去,HBase 會把資料放到基於記憶體實現的 Memstore 裡,等資料達到一定的數量時才刷寫(flush)到最終儲存的 HFile 內,而如果在這個過程中伺服器宕機或者斷電了,那麼資料就丟失了。WAL 是一個保險機制,資料在寫到 Memstore 之前,先被寫到 WAL 了,這樣當故障恢復的時候依舊可以從 WAL 中恢復資料。
-
多個 Region:Region 相當於一個數據分片,每一個 Region 都有起始 rowkey 和結束 rowkey,代表了它所儲存的 row 範圍。

3、Region 內部架構
每一個 Region 內都包含有多個 Store 例項, 一個 Store 對應一個列族的資料 ,如果一個表有兩個列族,那麼在一個 Region 裡面就有兩個 Store,Store 內部有 MemStore 和 HFile 這兩個組成部分。

4、預寫日誌(WAL)
預寫日誌(Write-ahead log,WAL)就是設計來解決宕機之後的操作恢復問題的,資料到達 Region 的時候是先寫入 WAL,然後再被載入到 Memstore,就算 Region 的機器宕掉了,由於 WAL 的資料是儲存在 HDFS 上的,所以資料並不會丟失。
WAL 是預設開啟的,可以通過下面的程式碼關閉 WAL。
Mutation.setDurability(Durability.SKIP_WAL);
Put、Append、Increment、Delete 都是 Mutation 的子類,所以他們都有 setDurability 方法,這樣可以讓該資料操作快一點, 但是最好不要這樣做 ,因為當伺服器宕機時,資料就會丟失。
如果你實在想不惜通過關閉 WAL 來提高效能,可以選擇 非同步寫入 WAL 。
Mutation.setDurability(Durability.ASYNC WAL);
這樣設定後 Region 會等到條件滿足的時候才把操作寫入 WAL,這裡提到的條件主要指的是時間間隔 hbase.regionserver.optionallogflushinterval
,這個時間間隔的意思是 HBase 間隔多久會把操作從記憶體寫入 WAL,預設值是 1s。
如果你的系統對效能要求極高、對資料一致性要求不高,並且系統的效能瓶頸出現在 WAL 上的時候,你可以考慮使用非同步寫入 WAL,否則,使用預設的配置即可。
5、WAL 滾動
WAL 是一個環狀的滾動日誌結構,因為這種結構寫入效果最高,而且可以保證空間不會持續變大。
WAL 的檢查間隔由 hbase.regionserver.logroll.period
定義,預設值為 1h。檢查的內容是把當前 WAL 中的操作跟實際持久化到 HDFS 上的操作比較,看哪些操作已經被持久化了,被持久化的操作就會被移動到 .oldlogs
資料夾內(這個資料夾也是在 HDFS 上的)。
一個 WAL 例項包含有多個 WAL 檔案,WAL 檔案的最大數量通過 hbase.regionserver.maxlogs
(預設是 32)引數來定義。
其他的觸發滾動的條件是:
-
當 WAL 檔案所在的塊(Block)快要滿了;
-
當WAL所佔的空間大於或者等於某個閥值,該閥值的計算公式是:
hbase.regionserver.hlog.blocksize * hbase.regionserver.logroll.multiplier
; -
hbase.regionserver.hlog.blocksize
是標定儲存系統的塊(Block)大小的,你如果是基於 HDFS 的,那麼只需要把這個值設定成 HDFS 的塊大小即可; -
hbase.regionserver.logroll.multiplier
是一個百分比,預設設定成 0.95,意思是 95%,如果 WAL 檔案所佔的空間大於或者等於 95% 的塊大小,則這個 WAL 檔案就會被歸檔到.oldlogs
資料夾內。
WAL 檔案被創建出來後會放在 /hbase/.log
下(這裡說的路徑都是基於 HDFS),一旦 WAL 檔案被判定為要歸檔,則會被移動到 /hbase/.oldlogs
資料夾。Master 會負責定期地去清理 .oldlogs
資料夾,判斷的條件是“沒有任何引用指向這個 WAL 檔案”。目前有兩種服務可能會引用 WAL 檔案:
-
TTL 程序 :該程序會保證 WAL 檔案一直存活直到達到
hbase.master.logcleaner.ttl
定義的超時時間(預設 10 分鐘)為止; -
備份(replication)機制:如果開啟了 HBase 的備份機制,那麼 HBase 要保證備份叢集已經完全不需要這個 WAL 檔案了,才會刪除這個 WAL 檔案。這裡提到的 replication 不是檔案的備份數,而是 0.90 版本加入的特性,這個特性用於把一個叢集的資料實時備份到另外一個叢集。
6、Store 內部結構
在 Store 中有兩個重要組成部分:
-
MemStore:每個 Store 中 有一個 MemStore 例項 ,資料寫入 WAL 之後就會被放入 MemStore。MemStore 是記憶體的儲存物件,只有當 MemStore 滿了的時候才會將資料刷寫(flush)到 HFile 中;
-
HFile:在 Store 中 有多個 HFile ,當 MemStore 滿了之後 HBase 就會在 HDFS 上生成一個新的 HFile,然後把 MemStore 中的內容寫到這個 HFile 中。HFile 直接跟 HDFS 打交道,它是資料的儲存實體。

WAL 是儲存在 HDFS 上的,Memstore 是儲存在記憶體中的,HFile 又是儲存在 HDFS 上的;資料是先寫入 WAL,再被放入 Memstore,最後被持久化到 HFile 中。資料在進入 HFile 之前已經被儲存到 HDFS 一次了,為什麼還需要被放入 Memstore?
這是因為 HDFS 上的檔案只能建立、追加、刪除,但是不能修改。對於一個數據庫來說,按順序地存放資料是非常重要的,這是效能的保障,所以我們不能按照資料到來的順序來寫入硬碟。
可以使用記憶體先把資料整理成順序存放,然後再一起寫入硬碟,這就是 Memstore 存在的意義。雖然 Memstore 是儲存在記憶體中的,HFile 和 WAL 是儲存在 HDFS 上的,但由於資料在寫入 Memstore 之前,要先被寫入 WAL,所以 增加 Memstore 的大小並不能加速寫入速度。Memstore 存在的意義是維持資料按照 rowkey 順序排列,而不是做一個快取。
7、MemStore
設計 MemStore 的原因有以下幾點:
-
由於 HDFS 上的檔案不可修改,為了讓資料順序儲存從而提高讀取效率,HBase 使用了 LSM 樹結構來儲存資料,資料會先在 Memstore 中 整理成 LSM 樹 ,最後再刷寫到 HFile 上。
-
優化資料的儲存,比如一個數據新增後就馬上刪除了,這樣在刷寫的時候就可以直接不把這個資料寫到 HDFS 上。
不過不要想當然地認為讀取也是先讀取 Memstore 再讀取磁碟喲!讀取的時候是有專門的快取叫 BlockCache,這個 BlockCache 如果開啟了,就是先讀 BlockCache,讀不到才是讀 HFile+Memstore。
8、HFile(StoreFile)
HFile 是資料儲存的實際載體,我們建立的所有表、列等資料都儲存在 HFile 裡面。HFile 是由一個一個的塊組成的,在 HBase 中一個塊的大小預設為 64KB,由列族上的 BLOCKSIZE 屬性定義。這些塊區分了不同的角色:
-
Data:資料塊。每個 HFile 有多個 Data 塊,我們儲存在 HBase 表中的資料就在這裡,Data 塊其實是 可選的 ,但是幾乎很難看到不包含 Data 塊的 HFile;
-
Meta:元資料塊。Meta 塊是 可選的 ,Meta 塊只有在檔案關閉的時候才會寫入。Meta 塊儲存了該 HFile 檔案的元資料資訊,在 v2 之前布隆過濾器(Bloom Filter)的資訊直接放在 Meta 裡面儲存,v2 之後分離出來單獨儲存;
-
FileInfo:檔案資訊,其實也是一種資料儲存塊。FileInfo 是 HFile 的必要組成部分,是 必選的 ,它只有在檔案關閉的時候寫入,儲存的是這個檔案的資訊,比如最後一個 Key(LastKey),平均的 Key 長度(AvgKeyLen)等;
-
DataIndex:儲存 Data 塊索引資訊的塊檔案。索引的資訊其實也就是 Data 塊的偏移值(offset),DataIndex 也是 可選的 ,有 Data 塊才有 DataIndex;
-
MetaIndex:儲存 Meta 塊索引資訊的塊檔案。MetaIndex 塊也是 可選的 ,有 Meta 塊才有 MetaIndex;
-
Trailer: 必選的 ,它儲存了 FileInfo、DataIndex、MetaIndex 塊的偏移值。

其實叫 HFile 或者 StoreFile 都沒錯,在物理儲存上我們管 MemStore 刷寫而成的檔案叫 HFile,StoreFile 就是 HFile 的抽象類而已。
9、Data 資料塊
Data 資料塊的第一位儲存的是塊的型別,後面儲存的是多個 KeyValue 鍵值對,也就是單元格(Cell)的實現類,Cell 是一個介面,KeyValue 是它的實現類。

10、KeyValue 類
一個 KeyValue 類裡面最後一個部分是儲存資料的 Value,而前面的部分都是儲存跟該單元格相關的元資料資訊。如果你儲存的 value 很小,那麼這個單元格的絕大部分空間就都是 rowkey、column family、column 等的元資料,所以大家的列族和列的名字如果很長,大部分的空間就都被拿來儲存這些資料了。
不過如果採用適當的壓縮演算法就可以極大地節省儲存列族、列等資訊的空間了,所以在實際的使用中,可以通過指定壓縮演算法來壓縮這些元資料。不過壓縮和解壓必然帶來效能損耗,所以使用壓縮也需要根據實際情況來取捨。如果你的資料主要是歸檔資料,不太要求讀寫效能,那麼壓縮演算法就比較適合你。

三、增刪查改的真正面目
HBase 是一個可以隨機讀寫的資料庫,而它所基於的持久化層 HDFS 卻是要麼新增,要麼整個刪除,不能修改的系統。那 HBase 怎麼實現我們的增刪查改的?真實的情況是這樣的:HBase 幾乎總是在做新增操作。
-
當你新增一個單元格的時候,HBase 在 HDFS 上新增一條資料;
-
當你修改一個單元格的時候,HBase 在 HDFS 又新增一條資料,只是版本號比之前那個大(或者你自己定義);
-
當你刪除一個單元格的時候,HBase 還是新增一條資料!只是這條資料沒有 value,型別為 DELETE,這條資料叫墓碑標記(Tombstone)。
由於資料庫在使用過程中積累了很多增刪查改操作,資料的連續性和順序性必然會被破壞。為了提升效能,HBase 每間隔一段時間都會進行一次合併(Compaction),合併的物件為 HFile 檔案。
合併分為 minor compaction 和 major compaction,在 HBase 進行 major compaction 的時候,它會把多個 HFile 合併成 1 個 HFile,在這個過程中,一旦檢測到有被打上墓碑標記的記錄,在合併的過程中就忽略這條記錄,這樣在新產生的 HFile 中,就沒有這條記錄了,自然也就相當於被真正地刪除了。
四、HBase 資料結構總結
HBase 資料的內部結構大體如下:
-
一個 RegionServer 包含多個 Region,劃分規則是:一個表的一段鍵值在一個 RegionServer 上會產生一個 Region。不過當某一行的資料量太大了(要非常大),HBase 也會把這個 Region 根據列族切分到不同的機器上去;
-
一個 Region 包含多個 Store,劃分規則是:一個列族分為一個 Store,如果一個表只有一個列族,那麼這個表在這個機器上的每一個 Region 裡面都只有一個 Store;
-
一個 Store 裡面只有一個 Memstore;
-
一個 Store 裡面有多個 HFile,每次 Memstore 的刷寫(flush)就產生一個新的 HFile 出來。

五、KeyValue 的寫入和讀出
1、寫入
一個 KeyValue 被持久化到 HDFS 的過程的如下:

-
WAL :資料被髮出之後第一時間被寫入 WAL,由於 WAL 是基於 HDFS 來實現的,所以也可以說現在單元格就已經被持久化了,但是 WAL 只是一個暫存的日誌,它是不區分 Store 的,這些資料是不能被直接讀取和使用;
-
Memstore :資料隨後會立即被放入 Memstore 中進行整理,Memstore 會負責按照 LSM 樹的結構來存放資料,這個過程就像我們在打牌的時候,抓牌之後在手上對牌進行整理的過程;
-
HFile :最後,當 Memstore 太大了達到尺寸上的閥值,或者達到了刷寫時間間隔閥值的時候,HBaes 會把這個 Memstore 的內容刷寫到 HDFS 系統上,稱為一個儲存在硬碟上的 HFile 檔案。至此,我們可以稱為資料真正地被持久化到硬碟上,就算宕機,斷電,資料也不會丟失了。
2、讀出
由於有 MemStore(基於記憶體)和 HFile(基於HDFS)這兩個機制,你一定會立馬想到先讀取 MemStore,如果找不到,再去 HFile 中查詢。這是顯而易見的機制,可惜 HBase 在處理讀取的時候並不是這樣的。實際的讀取順序是先從 BlockCache 中找資料,找不到了再去 Memstore 和 HFile 中查詢資料。
墓碑標記和資料不在一個地方,讀取資料的時候怎麼知道這個資料要刪除呢?如果這個資料比它的墓碑標記更早被讀到,那在這個時間點真是不知道這個資料會被刪 除,只有當掃描器接著往下讀,讀到墓碑標記的時候才知道這個資料是被標記為刪除的,不需要返回給使用者。
所以 HBase 的 Scan 操作在取到所需要的所有行鍵對應的資訊之後還會繼續掃描下去,直到被掃描的資料大於給出的限定條件為止,這樣它才能知道哪些資料應該被返回給使用者,而哪些應該被捨棄。所以 你增加過濾條件也無法減少 Scan 遍歷的行數,只有縮小 STARTROW 和 ENDROW 之間的行鍵範圍才可以明顯地加快掃描的速度 。
在 Scan 掃描的時候 store 會建立 StoreScanner 例項,StoreScanner 會把 MemStore 和 HFile 結合起來掃描,所以具體從 MemStore 還是 HFile 中讀取資料,外部的呼叫者都不需要知道具體的細節。當 StoreScanner 開啟的時候,會先定位到起始行鍵(STARTROW)上,然後開始往下掃描。

其中紅色塊部分都是屬於指定 row 的資料,Scan 要把所有符合條件的 StoreScanner 都掃描過一遍之後才會返回資料給使用者。
六、Region 的定位
Region 的查詢,早期的設計(0.96.0)之前是被稱為三層查詢架構:
-
Region:查詢的資料所在的 Region;
-
.META. :是一張元資料表,它儲存了所有 Region 的簡要資訊,
.META.
表中的一行記錄就是一個 Region,該行記錄了該 Region 的起始行、結束行和該 Region 的連線資訊,這樣客戶端就可以通過這個來判斷需要的資料在哪個 Region 上; -
-ROOT- :是一張儲存
.META.
表的表,.META.
可以有很多張,而-ROOT-
就是儲存了.META.
表在什麼 Region 上的資訊(.META.
表也是一張普通的表,也在 Region 上)。通過兩層的擴充套件最多可以支援約 171 億個 Region。
-ROOT-
表記錄在 ZooKeeper 上,路徑為: /hbase/root-region-server
;Client 查詢資料的流程從巨集觀角度來看是這樣的:
-
使用者通過查詢 zk(ZooKeeper)的
/hbase/root-regionserver
節點來知道-ROOT-
表在什麼 RegionServer 上; -
訪問
-ROOT-
表,看需要的資料在哪個.META.
表上,這個.META.
表在什麼 RegionServer 上; -
訪問
.META.
表來看要查詢的行鍵在什麼 Region 範圍裡面; -
連線具體的資料所在的 RegionServer,這回就真的開始用 Scan 來遍歷 row 了。

從 0.96 版本之後這個三層查詢架構被改成了二層查詢架構, -ROOT-
表被去掉了,同時 zk 中的 /hbase/root-region-server
也被去掉了,直接把 .META.
表所在的 RegionServer 資訊儲存到了 zk 中的 /hbase/meta-region-server
。再後來引入了 namespace, .META.
表被修改成了 hbase:meta
。
新版 Region 查詢流程:
-
客戶端先通過 ZooKeeper 的
/hbase/meta-region-server
節點查詢到哪臺 RegionServer 上有hbase:meta
表。 -
客戶端連線含有
hbase:meta
表的 RegionServer,hbase:meta
表儲存了所有 Region 的行鍵範圍資訊,通過這個表就可以查詢出要存取的 rowkey 屬於哪個 Region 的範圍裡面,以及這個 Region 又是屬於哪個 RegionServer; -
獲取這些資訊後,客戶端就可以直連其中一臺擁有要存取的 rowkey 的 RegionServer,並直接對其操作;
-
客戶端會把 meta 資訊快取起來,下次操作就不需要進行以上載入
hbase:meta
的步驟了。

Any Code,Code Any!
掃碼關注『AnyCode』,程式設計路上,一起前行。
